Files
NeoTachyon/install.sh
T

2553 lines
82 KiB
Bash
Executable File

#!/bin/sh
# shellcheck shell=dash
REPO_OWNER="ushan0v"
REPO_NAME="podkop-plus"
REQUIRED_SPACE_KB=15360
PKG_IS_APK=0
FETCHER=""
TMP_DIR=""
PODKOP_WAS_ENABLED=0
PODKOP_WAS_RUNNING=0
TARGET_ARCH=""
ZAPRET_ARCH=""
ZAPRET_ARCH_CANDIDATES=""
ZAPRET_ALREADY_PRESENT=0
ZAPRET_INSTALLED=0
ZAPRET_UP_TO_DATE=0
ZAPRET_INSTALLED_VERSION=""
ZAPRET_REQUESTED=0
ZAPRET_SKIPPED_REASON=""
ZAPRET_INSTALL_CHOICE="${PODKOP_PLUS_INSTALL_ZAPRET:-${INSTALL_ZAPRET:-}}"
BYEDPI_ARCH=""
BYEDPI_ALREADY_PRESENT=0
BYEDPI_INSTALLED=0
BYEDPI_UP_TO_DATE=0
BYEDPI_INSTALLED_VERSION=""
BYEDPI_REQUESTED=0
BYEDPI_SKIPPED_REASON=""
BYEDPI_INSTALL_CHOICE="${PODKOP_PLUS_INSTALL_BYEDPI:-${INSTALL_BYEDPI:-}}"
AWG_ALREADY_PRESENT=0
AWG_INSTALLED=0
AWG_UP_TO_DATE=0
AWG_REQUESTED=0
AWG_SKIPPED_REASON=""
AWG_INSTALL_CHOICE="${PODKOP_PLUS_INSTALL_AWG:-${INSTALL_AWG:-}}"
AWG_OPENWRT_VERSION=""
AWG_TARGET=""
AWG_SUBTARGET=""
AWG_ARCH=""
AWG_VERSION=""
AWG_RELEASE_TAG_RESOLVED=""
AWG_BASE_URL=""
AWG_PACKAGE_EXT=""
AWG_LUCI_PACKAGE_NAME=""
AWG_PACKAGE_FILES=""
AWG_PACKAGE_VERSION=""
SING_BOX_INSTALL_CHOICE="${PODKOP_PLUS_SING_BOX:-${SING_BOX_FLAVOR:-}}"
SING_BOX_ACTION="keep"
SING_BOX_CURRENT_VERSION=""
SING_BOX_CURRENT_IS_EXTENDED=0
SING_BOX_EXTENDED_RELEASE_JSON=""
SING_BOX_EXTENDED_RELEASE_TAG=""
SING_BOX_EXTENDED_ARCH_SUFFIX=""
SING_BOX_EXTENDED_ASSET_NAME=""
SING_BOX_EXTENDED_ASSET_URL=""
SING_BOX_EXTENDED_BACKUP_FILE=""
SING_BOX_EXTENDED_WORK_DIR=""
PODKOP_PLUS_I18N_REQUESTED=0
PODKOP_PLUS_RELEASE_JSON=""
PODKOP_PLUS_RELEASE_TAG=""
PODKOP_PLUS_BACKEND_URL=""
PODKOP_PLUS_BACKEND_NAME=""
PODKOP_PLUS_BACKEND_FILE=""
PODKOP_PLUS_APP_URL=""
PODKOP_PLUS_APP_NAME=""
PODKOP_PLUS_APP_FILE=""
PODKOP_PLUS_I18N_URL=""
PODKOP_PLUS_I18N_NAME=""
PODKOP_PLUS_I18N_FILE=""
PODKOP_PLUS_PACKAGE_VERSION=""
PODKOP_PLUS_SUPPORTED_MIGRATION_VERSION="0.7.17-8"
PODKOP_PLUS_INSTALLED_VERSION=""
PODKOP_PLUS_RESET_CONFIG=0
ZAPRET_RELEASE_JSON=""
ZAPRET_RELEASE_TAG_RESOLVED=""
ZAPRET_BUNDLE_URL=""
ZAPRET_BUNDLE_NAME=""
ZAPRET_PACKAGE_NAME=""
ZAPRET_PACKAGE_FILE=""
ZAPRET_PACKAGE_VERSION=""
BYEDPI_RELEASE_JSON=""
BYEDPI_RELEASE_TAG_RESOLVED=""
BYEDPI_PACKAGE_URL=""
BYEDPI_PACKAGE_NAME=""
BYEDPI_PACKAGE_FILE=""
BYEDPI_PACKAGE_VERSION=""
OPTIONAL_INSTALLED_VERSION=""
INSTALLER_LANG="en"
command -v apk >/dev/null 2>&1 && PKG_IS_APK=1
msg() {
printf '\033[32;1m%s\033[0m\n' "$1"
}
warn() {
printf '\033[33;1m%s\033[0m\n' "$1"
}
fail() {
printf '\033[31;1m%s\033[0m\n' "$1" >&2
exit 1
}
usage() {
cat <<EOF
Usage: $0 [--with-zapret|--without-zapret] [--with-byedpi|--without-byedpi] [--with-awg|--without-awg] [--sing-box=stock|extended|keep]
Options:
--with-zapret Install the optional external zapret provider package.
--without-zapret Install Podkop Plus without the zapret provider.
--with-byedpi Install the optional external ByeDPI provider package.
--without-byedpi Install Podkop Plus without the ByeDPI provider.
--with-awg Install optional AmneziaWG OpenWrt packages.
--without-awg Install Podkop Plus without AmneziaWG packages.
--sing-box=stock Use the regular stable sing-box package from OpenWrt.
--sing-box=extended Install or refresh sing-box-extended for XHTTP support.
--sing-box=keep Keep the current sing-box flavor.
EOF
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--with-zapret)
ZAPRET_INSTALL_CHOICE=1
;;
--without-zapret)
ZAPRET_INSTALL_CHOICE=0
;;
--with-byedpi)
BYEDPI_INSTALL_CHOICE=1
;;
--without-byedpi)
BYEDPI_INSTALL_CHOICE=0
;;
--with-awg)
AWG_INSTALL_CHOICE=1
;;
--without-awg)
AWG_INSTALL_CHOICE=0
;;
--with-sing-box-extended)
SING_BOX_INSTALL_CHOICE="extended"
;;
--with-stock-sing-box)
SING_BOX_INSTALL_CHOICE="stock"
;;
--sing-box=*)
SING_BOX_INSTALL_CHOICE="${1#--sing-box=}"
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown installer option: $1"
;;
esac
shift
done
}
cleanup() {
[ -n "$TMP_DIR" ] && rm -rf "$TMP_DIR"
[ -n "$SING_BOX_EXTENDED_WORK_DIR" ] && rm -rf "$SING_BOX_EXTENDED_WORK_DIR"
}
clear_zapret_download_state() {
ZAPRET_RELEASE_JSON=""
ZAPRET_RELEASE_TAG_RESOLVED=""
ZAPRET_BUNDLE_URL=""
ZAPRET_BUNDLE_NAME=""
ZAPRET_PACKAGE_NAME=""
ZAPRET_PACKAGE_FILE=""
ZAPRET_PACKAGE_VERSION=""
ZAPRET_ARCH=""
ZAPRET_INSTALLED=0
ZAPRET_UP_TO_DATE=0
ZAPRET_INSTALLED_VERSION=""
}
clear_byedpi_download_state() {
BYEDPI_RELEASE_JSON=""
BYEDPI_RELEASE_TAG_RESOLVED=""
BYEDPI_PACKAGE_URL=""
BYEDPI_PACKAGE_NAME=""
BYEDPI_PACKAGE_FILE=""
BYEDPI_PACKAGE_VERSION=""
BYEDPI_ARCH=""
BYEDPI_INSTALLED=0
BYEDPI_UP_TO_DATE=0
BYEDPI_INSTALLED_VERSION=""
}
clear_awg_download_state() {
AWG_OPENWRT_VERSION=""
AWG_TARGET=""
AWG_SUBTARGET=""
AWG_ARCH=""
AWG_VERSION=""
AWG_RELEASE_TAG_RESOLVED=""
AWG_BASE_URL=""
AWG_PACKAGE_EXT=""
AWG_LUCI_PACKAGE_NAME=""
AWG_PACKAGE_FILES=""
AWG_PACKAGE_VERSION=""
AWG_INSTALLED=0
AWG_UP_TO_DATE=0
}
read_openwrt_release_value() {
key="$1"
[ -f /etc/openwrt_release ] || return 0
sed -n "s/^${key}='\(.*\)'/\1/p" /etc/openwrt_release 2>/dev/null | head -n 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
init_tmp_dir() {
TMP_DIR="$(mktemp -d /tmp/podkop-plus.XXXXXX 2>/dev/null || true)"
if [ -z "$TMP_DIR" ]; then
TMP_DIR="/tmp/podkop-plus.$$"
mkdir -p "$TMP_DIR" || fail "Failed to create temporary directory: $TMP_DIR"
fi
}
detect_fetcher() {
if command_exists wget; then
FETCHER="wget"
return 0
fi
if command_exists curl; then
FETCHER="curl"
return 0
fi
fail "wget or curl is required to download Podkop Plus"
}
http_get() {
case "$FETCHER" in
wget)
wget -qO- "$1"
;;
curl)
curl -fsSL "$1"
;;
*)
return 1
;;
esac
}
download_file_once() {
case "$FETCHER" in
wget)
wget -q -O "$2" "$1"
;;
curl)
curl -fsSL "$1" -o "$2"
;;
*)
return 1
;;
esac
}
download_with_retry() {
url="$1"
output_path="$2"
label="$3"
attempt=1
max_attempts=3
while [ "$attempt" -le "$max_attempts" ]; do
msg "Downloading $label ($attempt/$max_attempts)"
if download_file_once "$url" "$output_path" && [ -s "$output_path" ]; then
return 0
fi
rm -f "$output_path"
warn "Retrying $label"
attempt=$((attempt + 1))
done
return 1
}
pkg_list_installed_names() {
if [ "$PKG_IS_APK" -eq 1 ]; then
apk info 2>/dev/null
else
opkg list-installed 2>/dev/null | awk '{print $1}'
fi
}
pkg_is_installed() {
pkg_name="$1"
if [ "$PKG_IS_APK" -eq 1 ]; then
apk info -e "$pkg_name" >/dev/null 2>&1
else
opkg list-installed 2>/dev/null | grep -Eq "^${pkg_name}([[:space:]-]|$)"
fi
}
get_installed_package_version() {
pkg_name="$1"
if [ "$PKG_IS_APK" -eq 1 ]; then
apk info -e "$pkg_name" >/dev/null 2>&1 || return 0
apk info -v "$pkg_name" 2>/dev/null | sed "s/^${pkg_name}-//" | sed -n '1p'
else
opkg list-installed 2>/dev/null | awk -v pkg="$pkg_name" '$1 == pkg && $2 == "-" {print $3; exit}'
fi
}
get_zapret_installed_version() {
version="$(get_installed_package_version "zapret")"
if [ -z "$version" ] && [ -x /opt/zapret/nfq/nfqws ]; then
version="$(/opt/zapret/nfq/nfqws --version 2>/dev/null | sed -n '1s/^.*version[[:space:]]*//p' | awk '{ print $1; exit }')"
fi
printf '%s\n' "$version"
}
get_byedpi_installed_version() {
version="$(get_installed_package_version "byedpi")"
if [ -z "$version" ] && [ -x /usr/bin/ciadpi ]; then
version="$(/usr/bin/ciadpi --version 2>/dev/null | awk 'NF { print $1; exit }')"
fi
printf '%s\n' "$version"
}
get_optional_installed_version() {
package_name="$1"
case "$package_name" in
zapret) get_zapret_installed_version ;;
byedpi) get_byedpi_installed_version ;;
*) get_installed_package_version "$package_name" ;;
esac
}
normalize_zapret_version() {
printf '%s\n' "$1" | sed 's/^v//;s/-r[0-9][0-9]*$//;s/+.*$//;s/[[:space:]].*$//'
}
normalize_optional_package_version() {
package_name="$1"
version="$2"
case "$package_name" in
zapret) normalize_zapret_version "$version" ;;
*) printf '%s\n' "$version" ;;
esac
}
package_version_newer() {
candidate_version="$1"
installed_version="$2"
apk_compare_result=""
[ -n "$candidate_version" ] || return 1
[ -n "$installed_version" ] || return 1
[ "$candidate_version" != "$installed_version" ] || return 1
if [ "$PKG_IS_APK" -eq 1 ] && command_exists apk; then
apk_compare_result="$(apk version -t "$candidate_version" "$installed_version" 2>/dev/null || true)"
[ "$apk_compare_result" = ">" ]
return $?
fi
if command_exists opkg; then
opkg compare-versions "$candidate_version" ">" "$installed_version" >/dev/null 2>&1
return $?
fi
version_ge "$candidate_version" "$installed_version"
}
optional_package_update_needed() {
provider_label="$1"
package_name="$2"
candidate_version="$3"
candidate_compare_version=""
installed_compare_version=""
if [ -z "$candidate_version" ]; then
warn "$provider_label release was found, but the package version could not be parsed."
return 2
fi
OPTIONAL_INSTALLED_VERSION="$(get_optional_installed_version "$package_name")"
if [ -z "$OPTIONAL_INSTALLED_VERSION" ]; then
warn "$provider_label is already present, but the installed package version could not be detected."
return 2
fi
candidate_compare_version="$(normalize_optional_package_version "$package_name" "$candidate_version")"
installed_compare_version="$(normalize_optional_package_version "$package_name" "$OPTIONAL_INSTALLED_VERSION")"
if package_version_newer "$candidate_compare_version" "$installed_compare_version"; then
msg "Found $provider_label update: $OPTIONAL_INSTALLED_VERSION -> $candidate_version"
return 0
fi
msg "$provider_label is already up to date ($OPTIONAL_INSTALLED_VERSION)"
return 1
}
is_zapret_present() {
pkg_is_installed "zapret" || [ -x /opt/zapret/nfq/nfqws ]
}
is_byedpi_present() {
pkg_is_installed "byedpi" || [ -x /usr/bin/ciadpi ] || [ -x /etc/init.d/byedpi ]
}
is_awg_present() {
pkg_is_installed "kmod-amneziawg" ||
pkg_is_installed "amneziawg-tools" ||
pkg_is_installed "luci-proto-amneziawg" ||
pkg_is_installed "luci-app-amneziawg" ||
command_exists awg ||
command_exists awg-quick ||
[ -d /sys/module/amneziawg ]
}
warn_zapret_unavailable() {
reason="$1"
if [ "$ZAPRET_ALREADY_PRESENT" -eq 1 ]; then
warn "$reason Keeping the existing zapret provider."
else
warn "$reason Continuing without zapret provider."
fi
}
warn_byedpi_unavailable() {
reason="$1"
if [ "$BYEDPI_ALREADY_PRESENT" -eq 1 ]; then
warn "$reason Keeping the existing ByeDPI provider."
else
warn "$reason Continuing without ByeDPI provider."
fi
}
warn_awg_unavailable() {
reason="$1"
if [ "$AWG_ALREADY_PRESENT" -eq 1 ]; then
warn "$reason Keeping the existing AmneziaWG installation."
else
warn "$reason Continuing without AmneziaWG packages."
fi
}
pkg_list_update() {
if [ "$PKG_IS_APK" -eq 1 ]; then
apk update </dev/null
else
opkg update </dev/null
fi
}
pkg_install_name() {
pkg_name="$1"
if [ "$PKG_IS_APK" -eq 1 ]; then
apk add "$pkg_name" </dev/null
else
opkg install "$pkg_name" </dev/null
fi
}
pkg_remove_if_installed() {
pkg_name="$1"
if ! pkg_is_installed "$pkg_name"; then
return 0
fi
if [ "$PKG_IS_APK" -eq 1 ]; then
apk del "$pkg_name" >/dev/null 2>&1 </dev/null || true
else
opkg remove --force-depends "$pkg_name" >/dev/null 2>&1 </dev/null || true
fi
}
pkg_remove_matching_prefix() {
prefix="$1"
for pkg_name in $(pkg_list_installed_names | grep "^$prefix" 2>/dev/null); do
pkg_remove_if_installed "$pkg_name"
done
}
pkg_install_files() {
if [ "$PKG_IS_APK" -eq 1 ]; then
apk add --allow-untrusted "$@" </dev/null
else
opkg install --force-overwrite --force-downgrade "$@" </dev/null
fi
}
ensure_bootstrap_tool() {
tool_name="$1"
package_name="$2"
if command_exists "$tool_name"; then
return 0
fi
msg "Installing bootstrap dependency: $package_name"
pkg_install_name "$package_name" || fail "Failed to install $package_name"
}
sync_time() {
current_year=""
if ! command_exists ntpd; then
return 0
fi
current_year="$(date +%Y 2>/dev/null || true)"
case "$current_year" in
''|*[!0-9]*) current_year=0 ;;
esac
if [ "$current_year" -ge 2024 ]; then
return 0
fi
ntpd -q \
-p 194.190.168.1 \
-p 216.239.35.0 \
-p 216.239.35.4 \
-p 162.159.200.1 \
-p 162.159.200.123 >/dev/null 2>&1 || true
}
check_root() {
if command_exists id && [ "$(id -u)" != "0" ]; then
fail "Please run this installer as root"
fi
}
check_system() {
release=""
major=""
model=""
available_space=""
[ -f /etc/openwrt_release ] || fail "This installer supports OpenWrt only"
model="$(cat /tmp/sysinfo/model 2>/dev/null || true)"
[ -n "$model" ] && msg "Router model: $model"
release="$(read_openwrt_release_value "DISTRIB_RELEASE")"
major="$(printf '%s' "$release" | sed 's/[^0-9].*$//' | cut -d. -f1)"
if [ -n "$major" ] && [ "$major" -lt 24 ]; then
fail "Podkop Plus requires OpenWrt 24.10 or newer"
fi
available_space="$(df /overlay 2>/dev/null | awk 'NR==2 {print $4}')"
[ -n "$available_space" ] || available_space="$(df / 2>/dev/null | awk 'NR==2 {print $4}')"
if [ -n "$available_space" ] && [ "$available_space" -lt "$REQUIRED_SPACE_KB" ]; then
fail "Not enough free flash space. Available: $((available_space / 1024)) MB, required: $((REQUIRED_SPACE_KB / 1024)) MB"
fi
}
confirm_prompt() {
prompt_text="$1"
answer=""
if [ ! -t 0 ]; then
msg "$prompt_text [y/N]: y (non-interactive)"
return 0
fi
while :; do
printf '%s [y/N]: ' "$prompt_text"
read -r answer || return 1
case "$answer" in
y|Y)
return 0
;;
n|N|"")
return 1
;;
*)
warn "Please answer y or n."
;;
esac
done
}
localized_prompt() {
prompt_id="$1"
case "${INSTALLER_LANG:-en}:$prompt_id" in
ru:remove_dns_proxy)
printf '%s' "Удалить конфликтующий пакет https-dns-proxy и продолжить?"
;;
*:remove_dns_proxy)
printf '%s' "Remove the conflicting https-dns-proxy package and continue?"
;;
ru:install_awg)
printf '%s' "Установить службу AmneziaWG?"
;;
*:install_awg)
printf '%s' "Install the AmneziaWG service?"
;;
ru:install_zapret)
printf '%s' "Установить службу Zapret?"
;;
*:install_zapret)
printf '%s' "Install the Zapret service?"
;;
ru:install_byedpi)
printf '%s' "Установить службу ByeDPI?"
;;
*:install_byedpi)
printf '%s' "Install the ByeDPI service?"
;;
ru:install_sing_box_extended)
printf '%s' "Установить неофициальный sing-box-extended для поддержки XHTTP вместо обычного стабильного sing-box?"
;;
*:install_sing_box_extended)
printf '%s' "Install unofficial sing-box-extended for XHTTP support instead of the regular stable sing-box?"
;;
ru:replace_sing_box_extended)
printf '%s' "Заменить установленный sing-box-extended на обычный стабильный sing-box?"
;;
*:replace_sing_box_extended)
printf '%s' "Replace the installed sing-box-extended with the regular stable sing-box?"
;;
ru:reset_unsupported_config)
printf '%s' "Миграция конфигурации Podkop Plus из установленной версии не поддерживается. Удалить /etc/config/podkop-plus и продолжить установку с чистой конфигурацией?"
;;
*:reset_unsupported_config)
printf '%s' "Podkop Plus config migration from the installed version is not supported. Remove /etc/config/podkop-plus and continue with a clean configuration?"
;;
esac
}
get_luci_main_lang() {
command_exists uci || return 0
uci -q get luci.main.lang 2>/dev/null || true
}
sanitize_semver() {
printf '%s\n' "$1" | sed 's/^v//;s/-.*$//;s/[^0-9.].*$//'
}
version_ge() {
lhs_major=0
lhs_minor=0
lhs_patch=0
rhs_major=0
rhs_minor=0
rhs_patch=0
lhs_version="$(sanitize_semver "$1")"
rhs_version="$(sanitize_semver "$2")"
old_ifs="$IFS"
IFS='.'
set -- $lhs_version
IFS="$old_ifs"
[ -n "$1" ] && lhs_major="$1"
[ -n "$2" ] && lhs_minor="$2"
[ -n "$3" ] && lhs_patch="$3"
IFS='.'
set -- $rhs_version
IFS="$old_ifs"
[ -n "$1" ] && rhs_major="$1"
[ -n "$2" ] && rhs_minor="$2"
[ -n "$3" ] && rhs_patch="$3"
if [ "$lhs_major" -gt "$rhs_major" ]; then
return 0
fi
if [ "$lhs_major" -lt "$rhs_major" ]; then
return 1
fi
if [ "$lhs_minor" -gt "$rhs_minor" ]; then
return 0
fi
if [ "$lhs_minor" -lt "$rhs_minor" ]; then
return 1
fi
[ "$lhs_patch" -ge "$rhs_patch" ]
}
normalize_podkop_plus_release_version() {
printf '%s\n' "$1" | sed 's/^v//;s/-r\([0-9][0-9]*\)$/-\1/'
}
podkop_plus_release_version_parts() {
version="$(normalize_podkop_plus_release_version "$1")"
release=0
case "$version" in
*-*)
release="${version##*-}"
version="${version%-*}"
;;
*.*.*.*)
release="${version##*.}"
version="${version%.*}"
;;
esac
case "$version" in ''|*[!0-9.]*|.*|*.) return 1 ;; esac
case "$release" in ''|*[!0-9]*) return 1 ;; esac
old_ifs="$IFS"
IFS='.'
set -- $version
IFS="$old_ifs"
[ -n "$1" ] || return 1
case "$1" in *[!0-9]*) return 1 ;; esac
case "${2:-0}" in *[!0-9]*) return 1 ;; esac
case "${3:-0}" in *[!0-9]*) return 1 ;; esac
printf '%s %s %s %s\n' "$1" "${2:-0}" "${3:-0}" "$release"
}
podkop_plus_release_version_lt() {
lhs_parts="$(podkop_plus_release_version_parts "$1")" || return 1
rhs_parts="$(podkop_plus_release_version_parts "$2")" || return 1
set -- $lhs_parts
lhs_major="$1"
lhs_minor="$2"
lhs_patch="$3"
lhs_release="$4"
set -- $rhs_parts
rhs_major="$1"
rhs_minor="$2"
rhs_patch="$3"
rhs_release="$4"
[ "$lhs_major" -lt "$rhs_major" ] && return 0
[ "$lhs_major" -gt "$rhs_major" ] && return 1
[ "$lhs_minor" -lt "$rhs_minor" ] && return 0
[ "$lhs_minor" -gt "$rhs_minor" ] && return 1
[ "$lhs_patch" -lt "$rhs_patch" ] && return 0
[ "$lhs_patch" -gt "$rhs_patch" ] && return 1
[ "$lhs_release" -lt "$rhs_release" ]
}
extract_package_version() {
package_name="$1"
case "$package_name" in
podkop-plus_*.ipk)
printf '%s\n' "$package_name" | sed 's/^podkop-plus_//;s/_[^_]*\.ipk$//'
;;
podkop-plus_*.apk)
printf '%s\n' "$package_name" | sed 's/^podkop-plus_//;s/\.apk$//'
;;
podkop-plus-*.ipk)
printf '%s\n' "$package_name" | sed 's/^podkop-plus-//;s/-[^-]*\.ipk$//'
;;
podkop-plus-*.apk)
printf '%s\n' "$package_name" | sed 's/^podkop-plus-//;s/\.apk$//'
;;
luci-app-podkop-plus_*.ipk)
printf '%s\n' "$package_name" | sed 's/^luci-app-podkop-plus_//;s/_[^_]*\.ipk$//'
;;
luci-app-podkop-plus_*.apk)
printf '%s\n' "$package_name" | sed 's/^luci-app-podkop-plus_//;s/\.apk$//'
;;
luci-app-podkop-plus-*.ipk)
printf '%s\n' "$package_name" | sed 's/^luci-app-podkop-plus-//;s/-[^-]*\.ipk$//'
;;
luci-app-podkop-plus-*.apk)
printf '%s\n' "$package_name" | sed 's/^luci-app-podkop-plus-//;s/\.apk$//'
;;
zapret_*.ipk)
printf '%s\n' "$package_name" | sed 's/^zapret_//;s/_[^_]*\.ipk$//'
;;
zapret-*.apk)
printf '%s\n' "$package_name" | sed 's/^zapret-//;s/\.apk$//'
;;
byedpi_*.ipk)
printf '%s\n' "$package_name" | sed 's/^byedpi_//;s/_[^_]*\.ipk$//'
;;
byedpi_*.apk)
printf '%s\n' "$package_name" | sed 's/^byedpi_//;s/\.apk$//'
;;
byedpi-*.ipk)
printf '%s\n' "$package_name" | sed 's/^byedpi-//;s/-[^-]*\.ipk$//'
;;
byedpi-*.apk)
printf '%s\n' "$package_name" | sed 's/^byedpi-//;s/\.apk$//'
;;
*)
printf '%s\n' "$package_name"
;;
esac
}
extract_arch_package_version() {
package_name="$1"
package_arch="$2"
version=""
version="$(printf '%s\n' "$package_name" | sed 's/\.ipk$//;s/\.apk$//')"
case "$version" in
zapret_*)
version="${version#zapret_}"
;;
zapret-*)
version="${version#zapret-}"
;;
byedpi_*)
version="${version#byedpi_}"
;;
byedpi-*)
version="${version#byedpi-}"
;;
esac
if [ -n "$package_arch" ]; then
case "$version" in
*_$package_arch)
version="${version%_$package_arch}"
;;
*-$package_arch)
version="${version%-$package_arch}"
;;
esac
fi
printf '%s\n' "$version"
}
extract_zapret_bundle_version() {
bundle_name="$1"
version=""
version="$(basename "$bundle_name" | sed -n 's/^zapret_v\([^_][^_]*\)_.*/\1/p')"
[ -n "$version" ] || version="$(basename "$bundle_name" | sed -n 's/^zapret_\([^_][^_]*\)_.*/\1/p')"
printf '%s\n' "$version" | sed 's/^v//'
}
fetch_github_release_json() {
owner="$1"
repo="$2"
response=""
message=""
url="https://api.github.com/repos/${owner}/${repo}/releases/latest"
response="$(http_get "$url" 2>/dev/null || true)"
[ -n "$response" ] || fail "Failed to query GitHub release metadata for ${owner}/${repo}"
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || fail "GitHub returned an invalid response for ${owner}/${repo}"
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"*|*"rate limit exceeded"*)
fail "GitHub API rate limit reached. Try again later."
;;
"Not Found")
fail "No published releases found for ${owner}/${repo}"
;;
esac
printf '%s' "$response"
}
fetch_github_releases_json() {
owner="$1"
repo="$2"
response=""
message=""
url="https://api.github.com/repos/${owner}/${repo}/releases?per_page=50"
response="$(http_get "$url" 2>/dev/null || true)"
[ -n "$response" ] || fail "Failed to query GitHub releases metadata for ${owner}/${repo}"
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || fail "GitHub returned an invalid response for ${owner}/${repo}"
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"*|*"rate limit exceeded"*)
fail "GitHub API rate limit reached. Try again later."
;;
"Not Found")
fail "No published releases found for ${owner}/${repo}"
;;
esac
printf '%s' "$response"
}
select_latest_podkop_plus_release_json() {
asset_ext="$1"
releases_file=""
best_tag=""
tag=""
releases_file="$(mktemp)"
cat > "$releases_file"
for tag in $(jq -r --arg ext "$asset_ext" '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| select(any(.assets[]?; ((.name | startswith("podkop-plus_")) or (.name | startswith("podkop-plus-"))) and (.name | endswith("." + $ext))))
| select(any(.assets[]?; ((.name | startswith("luci-app-podkop-plus_")) or (.name | startswith("luci-app-podkop-plus-"))) and (.name | endswith("." + $ext))))
| .tag_name // empty
' "$releases_file"); do
podkop_plus_release_version_parts "$tag" >/dev/null 2>&1 || continue
if [ -z "$best_tag" ] || podkop_plus_release_version_lt "$best_tag" "$tag"; then
best_tag="$tag"
fi
done
if [ -n "$best_tag" ]; then
jq -c --arg tag "$best_tag" '.[] | select(.tag_name == $tag)' "$releases_file" | sed -n '1p'
fi
rm -f "$releases_file"
}
resolve_podkop_plus_release() {
asset_ext="ipk"
[ "$PKG_IS_APK" -eq 1 ] && asset_ext="apk"
PODKOP_PLUS_RELEASE_JSON="$(fetch_github_releases_json "$REPO_OWNER" "$REPO_NAME" | select_latest_podkop_plus_release_json "$asset_ext")"
PODKOP_PLUS_RELEASE_TAG="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r '.tag_name // empty')"
[ -n "$PODKOP_PLUS_RELEASE_TAG" ] || fail "Failed to detect the Podkop Plus release tag"
PODKOP_PLUS_BACKEND_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("podkop-plus_")) or (.name | startswith("podkop-plus-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
[ -n "$PODKOP_PLUS_BACKEND_URL" ] || fail "The Podkop Plus release does not contain a podkop-plus .$asset_ext package"
PODKOP_PLUS_APP_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("luci-app-podkop-plus_")) or (.name | startswith("luci-app-podkop-plus-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
[ -n "$PODKOP_PLUS_APP_URL" ] || fail "The Podkop Plus release does not contain a luci-app-podkop-plus .$asset_ext package"
PODKOP_PLUS_BACKEND_NAME="$(basename "$PODKOP_PLUS_BACKEND_URL")"
PODKOP_PLUS_APP_NAME="$(basename "$PODKOP_PLUS_APP_URL")"
PODKOP_PLUS_PACKAGE_VERSION="$(extract_package_version "$PODKOP_PLUS_BACKEND_NAME")"
PODKOP_PLUS_I18N_URL=""
PODKOP_PLUS_I18N_NAME=""
if [ "$PODKOP_PLUS_I18N_REQUESTED" -eq 1 ]; then
PODKOP_PLUS_I18N_URL="$(printf '%s' "$PODKOP_PLUS_RELEASE_JSON" | jq -r --arg ext "$asset_ext" '.assets[] | select(((.name | startswith("luci-i18n-podkop-plus-ru_")) or (.name | startswith("luci-i18n-podkop-plus-ru-"))) and (.name | endswith("." + $ext))) | .browser_download_url' | sed -n '1p')"
[ -n "$PODKOP_PLUS_I18N_URL" ] || fail "The Podkop Plus release does not contain a luci-i18n-podkop-plus-ru .$asset_ext package"
PODKOP_PLUS_I18N_NAME="$(basename "$PODKOP_PLUS_I18N_URL")"
fi
}
remove_conflicting_dns_proxy() {
if ! pkg_is_installed "https-dns-proxy"; then
return 0
fi
warn "Detected conflicting package: https-dns-proxy"
confirm_prompt "$(localized_prompt remove_dns_proxy)" || fail "Please remove https-dns-proxy manually and run the installer again"
pkg_remove_if_installed "luci-app-https-dns-proxy"
pkg_remove_if_installed "https-dns-proxy"
pkg_remove_matching_prefix "luci-i18n-https-dns-proxy"
}
remove_old_sing_box_if_needed() {
installed_version=""
required_version="1.12.4"
pkg_is_installed "sing-box" || return 0
command_exists sing-box || return 0
installed_version="$(sing-box version 2>/dev/null | head -n 1 | awk '{print $3}')"
[ -n "$installed_version" ] || return 0
if version_ge "$installed_version" "$required_version"; then
return 0
fi
warn "sing-box $installed_version is older than the required version $required_version. Removing the old package first."
[ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus stop >/dev/null 2>&1 || true
[ -x /etc/init.d/podkop ] && /etc/init.d/podkop stop >/dev/null 2>&1 || true
pkg_remove_if_installed "sing-box"
}
get_sing_box_binary_version() {
command_exists sing-box || return 0
sing-box version 2>/dev/null | head -n 1 | awk '{print $NF}'
}
sing_box_version_is_extended() {
case "$1" in
*extended*)
return 0
;;
esac
return 1
}
normalize_sing_box_extended_version() {
printf '%s\n' "$1" | sed 's/^v//;s/+.*$//;s/[[:space:]].*$//'
}
detect_sing_box_flavor() {
SING_BOX_CURRENT_VERSION="$(get_sing_box_binary_version)"
SING_BOX_CURRENT_IS_EXTENDED=0
if [ -n "$SING_BOX_CURRENT_VERSION" ] && sing_box_version_is_extended "$SING_BOX_CURRENT_VERSION"; then
SING_BOX_CURRENT_IS_EXTENDED=1
fi
}
normalize_sing_box_choice() {
case "$1" in
extended|sing-box-extended|xhttp|yes|YES|true|TRUE|y|Y|1)
printf 'extended'
;;
stock|regular|stable|original|official|sing-box|no|NO|false|FALSE|n|N|0)
printf 'stock'
;;
keep|"")
printf 'keep'
;;
*)
fail "Unknown sing-box flavor: $1"
;;
esac
}
decide_sing_box_installation() {
choice="$(normalize_sing_box_choice "$SING_BOX_INSTALL_CHOICE")"
detect_sing_box_flavor
case "$choice" in
extended)
SING_BOX_ACTION="install_extended"
return 0
;;
stock)
SING_BOX_ACTION="install_stock"
return 0
;;
keep)
;;
esac
if [ "$SING_BOX_CURRENT_IS_EXTENDED" -eq 1 ]; then
msg "Detected sing-box-extended $SING_BOX_CURRENT_VERSION."
if [ ! -t 0 ]; then
SING_BOX_ACTION="preserve_extended"
msg "Keeping sing-box-extended in non-interactive mode."
return 0
fi
if confirm_prompt "$(localized_prompt replace_sing_box_extended)"; then
SING_BOX_ACTION="install_stock"
else
SING_BOX_ACTION="preserve_extended"
msg "Keeping sing-box-extended after Podkop Plus installation."
fi
return 0
fi
if [ -n "$SING_BOX_CURRENT_VERSION" ]; then
msg "Detected regular sing-box $SING_BOX_CURRENT_VERSION."
if [ ! -t 0 ]; then
SING_BOX_ACTION="keep"
return 0
fi
if confirm_prompt "$(localized_prompt install_sing_box_extended)"; then
SING_BOX_ACTION="install_extended"
fi
return 0
fi
msg "sing-box is not installed yet."
if [ ! -t 0 ]; then
SING_BOX_ACTION="keep"
msg "Using the regular sing-box dependency in non-interactive mode."
return 0
fi
if confirm_prompt "$(localized_prompt install_sing_box_extended)"; then
SING_BOX_ACTION="install_extended"
else
SING_BOX_ACTION="keep"
msg "The regular sing-box package will be installed as a dependency."
fi
}
decide_unsupported_config_migration() {
[ -f /etc/config/podkop-plus ] || return 0
PODKOP_PLUS_INSTALLED_VERSION="$(get_installed_package_version "podkop-plus")"
[ -n "$PODKOP_PLUS_INSTALLED_VERSION" ] || return 0
if ! podkop_plus_release_version_lt "$PODKOP_PLUS_INSTALLED_VERSION" "$PODKOP_PLUS_SUPPORTED_MIGRATION_VERSION"; then
return 0
fi
warn "Podkop Plus config migration from $PODKOP_PLUS_INSTALLED_VERSION is not supported."
warn "Supported in-place migration starts from $PODKOP_PLUS_SUPPORTED_MIGRATION_VERSION. /etc/config/podkop-plus will be removed and recreated clean."
warn "Миграция конфигурации Podkop Plus из версии $PODKOP_PLUS_INSTALLED_VERSION не поддерживается."
warn "Поддерживаемая миграция начинается с $PODKOP_PLUS_SUPPORTED_MIGRATION_VERSION. /etc/config/podkop-plus будет удален и создан заново."
if [ ! -t 0 ]; then
fail "Unsupported config migration requires interactive confirmation. Run the installer from a TTY and answer y to reset /etc/config/podkop-plus."
fi
if confirm_prompt "$(localized_prompt reset_unsupported_config)"; then
PODKOP_PLUS_RESET_CONFIG=1
return 0
fi
fail "Installation stopped because unsupported config migration was not confirmed."
}
resolve_sing_box_extended_arch_suffix() {
host_arch="$(uname -m 2>/dev/null || true)"
distrib_arch=""
if [ -f "/etc/openwrt_release" ]; then
distrib_arch=$(. /etc/openwrt_release 2>/dev/null && echo "$DISTRIB_ARCH")
case "$distrib_arch" in
*mipsel*|*mipsle*) host_arch="mipsel" ;;
*mips64el*|*mips64le*) host_arch="mips64el" ;;
esac
fi
case "$host_arch" in
aarch64) SING_BOX_EXTENDED_ARCH_SUFFIX="arm64" ;;
armv7*) SING_BOX_EXTENDED_ARCH_SUFFIX="armv7" ;;
armv6*) SING_BOX_EXTENDED_ARCH_SUFFIX="armv6" ;;
x86_64) SING_BOX_EXTENDED_ARCH_SUFFIX="amd64" ;;
i386|i686) SING_BOX_EXTENDED_ARCH_SUFFIX="386" ;;
mips) SING_BOX_EXTENDED_ARCH_SUFFIX="mips-softfloat" ;;
mipsel|mipsle) SING_BOX_EXTENDED_ARCH_SUFFIX="mipsle-softfloat" ;;
mips64) SING_BOX_EXTENDED_ARCH_SUFFIX="mips64" ;;
mips64el|mips64le) SING_BOX_EXTENDED_ARCH_SUFFIX="mips64le" ;;
riscv64) SING_BOX_EXTENDED_ARCH_SUFFIX="riscv64" ;;
s390x) SING_BOX_EXTENDED_ARCH_SUFFIX="s390x" ;;
*)
fail "Unsupported sing-box-extended architecture: ${host_arch:-unknown}"
;;
esac
}
resolve_sing_box_extended_release() {
response=""
tag=""
asset_pattern=""
resolve_sing_box_extended_arch_suffix
response="$(http_get "https://api.github.com/repos/shtorm-7/sing-box-extended/releases?per_page=30" 2>/dev/null || true)"
[ -n "$response" ] || fail "Failed to query sing-box-extended release metadata"
printf '%s' "$response" | jq -e . >/dev/null 2>&1 || fail "GitHub returned an invalid response for sing-box-extended"
tag="$(printf '%s' "$response" | jq -r '
[
.[]
| select(((.prerelease // false) | not) and ((.draft // false) | not))
| .tag_name // empty
| select(((ascii_downcase | contains("alpha")) or (ascii_downcase | contains("beta")) or (ascii_downcase | contains("rc"))) | not)
][0] // empty
')"
[ -n "$tag" ] || fail "No stable sing-box-extended release was found"
SING_BOX_EXTENDED_RELEASE_TAG="$tag"
SING_BOX_EXTENDED_RELEASE_JSON="$(fetch_github_release_json "shtorm-7" "sing-box-extended")"
if [ "$(printf '%s' "$SING_BOX_EXTENDED_RELEASE_JSON" | jq -r '.tag_name // empty')" != "$tag" ]; then
SING_BOX_EXTENDED_RELEASE_JSON="$(http_get "https://api.github.com/repos/shtorm-7/sing-box-extended/releases/tags/$tag" 2>/dev/null || true)"
[ -n "$SING_BOX_EXTENDED_RELEASE_JSON" ] || fail "Failed to query sing-box-extended release $tag"
printf '%s' "$SING_BOX_EXTENDED_RELEASE_JSON" | jq -e . >/dev/null 2>&1 || fail "GitHub returned an invalid sing-box-extended release response"
fi
asset_pattern="linux-${SING_BOX_EXTENDED_ARCH_SUFFIX}.tar.gz"
SING_BOX_EXTENDED_ASSET_URL="$(printf '%s' "$SING_BOX_EXTENDED_RELEASE_JSON" | jq -r --arg pattern "$asset_pattern" '
[(.assets // [])[] | select(.name | endswith($pattern)) | .browser_download_url][0] // empty
')"
[ -n "$SING_BOX_EXTENDED_ASSET_URL" ] || fail "No sing-box-extended asset was found for architecture suffix $SING_BOX_EXTENDED_ARCH_SUFFIX"
SING_BOX_EXTENDED_ASSET_NAME="$(basename "$SING_BOX_EXTENDED_ASSET_URL")"
}
select_sing_box_extended_work_dir() {
free_ram_kb="$(awk '/MemAvailable/ {print $2; exit} /MemFree/ {print $2; exit}' /proc/meminfo 2>/dev/null)"
case "$free_ram_kb" in
''|*[!0-9]*) free_ram_kb=0 ;;
esac
if [ "$free_ram_kb" -gt 81920 ]; then
SING_BOX_EXTENDED_WORK_DIR="$TMP_DIR/sing-box-extended"
else
SING_BOX_EXTENDED_WORK_DIR="${HOME:-/root}/podkop-plus-sing-box-extended.$$"
fi
rm -rf "$SING_BOX_EXTENDED_WORK_DIR"
mkdir -p "$SING_BOX_EXTENDED_WORK_DIR" || fail "Failed to create sing-box-extended work directory"
}
stop_sing_box_dependant_services() {
[ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus stop >/dev/null 2>&1 || true
[ -x /etc/init.d/podkop ] && /etc/init.d/podkop stop >/dev/null 2>&1 || true
[ -x /etc/init.d/sing-box ] && /etc/init.d/sing-box stop >/dev/null 2>&1 || true
}
prepare_sing_box_action_before_install() {
[ "$SING_BOX_ACTION" = "preserve_extended" ] || return 0
[ -x /usr/bin/sing-box ] || return 0
SING_BOX_EXTENDED_BACKUP_FILE="$TMP_DIR/sing-box.extended.backup"
cp /usr/bin/sing-box "$SING_BOX_EXTENDED_BACKUP_FILE" || fail "Failed to back up the current sing-box-extended binary"
chmod 0755 "$SING_BOX_EXTENDED_BACKUP_FILE" 2>/dev/null || true
}
install_sing_box_extended_binary() {
archive_file=""
binary_path=""
target_binary=""
current_version=""
latest_version=""
new_version=""
resolve_sing_box_extended_release
latest_version="$(normalize_sing_box_extended_version "$SING_BOX_EXTENDED_RELEASE_TAG")"
current_version="$(get_sing_box_binary_version)"
if sing_box_version_is_extended "$current_version" &&
! package_version_newer "$latest_version" "$(normalize_sing_box_extended_version "$current_version")"; then
msg "sing-box-extended is already up to date ($current_version)"
return 0
fi
select_sing_box_extended_work_dir
archive_file="$SING_BOX_EXTENDED_WORK_DIR/$SING_BOX_EXTENDED_ASSET_NAME"
download_with_retry "$SING_BOX_EXTENDED_ASSET_URL" "$archive_file" "$SING_BOX_EXTENDED_ASSET_NAME" || fail "Failed to download sing-box-extended"
binary_path="$(tar -tzf "$archive_file" 2>/dev/null | grep -E '(^|/)sing-box$' | sed -n '1p')"
[ -n "$binary_path" ] || fail "sing-box binary was not found in the sing-box-extended archive"
target_binary="/usr/bin/.sing-box.new.$$"
if ! tar -xzf "$archive_file" -O "$binary_path" >"$target_binary"; then
rm -f "$target_binary"
fail "Failed to extract sing-box-extended archive"
fi
if [ ! -s "$target_binary" ]; then
rm -f "$target_binary"
fail "The extracted sing-box-extended binary is empty"
fi
chmod 0755 "$target_binary" || {
rm -f "$target_binary"
fail "Failed to mark sing-box-extended binary executable"
}
stop_sing_box_dependant_services
if ! mv -f "$target_binary" /usr/bin/sing-box; then
rm -f "$target_binary"
fail "Failed to install sing-box-extended binary"
fi
new_version="$(get_sing_box_binary_version)"
msg "Installed sing-box-extended ${new_version:-unknown} from shtorm-7/sing-box-extended@$SING_BOX_EXTENDED_RELEASE_TAG"
}
restore_preserved_sing_box_extended() {
current_version=""
[ -n "$SING_BOX_EXTENDED_BACKUP_FILE" ] || return 0
[ -f "$SING_BOX_EXTENDED_BACKUP_FILE" ] || return 0
current_version="$(get_sing_box_binary_version)"
if sing_box_version_is_extended "$current_version"; then
msg "sing-box-extended $current_version is still installed."
return 0
fi
stop_sing_box_dependant_services
cp "$SING_BOX_EXTENDED_BACKUP_FILE" /usr/bin/sing-box || fail "Failed to restore sing-box-extended after package installation"
chmod 0755 /usr/bin/sing-box || fail "Failed to mark restored sing-box binary executable"
msg "Restored sing-box-extended after package installation"
}
install_stock_sing_box_package() {
stop_sing_box_dependant_services
pkg_remove_if_installed "sing-box"
pkg_install_name "sing-box" || fail "Failed to install the regular sing-box package"
msg "Installed the regular sing-box package ${SING_BOX_CURRENT_VERSION:+(previously $SING_BOX_CURRENT_VERSION)}"
}
apply_sing_box_action_after_install() {
case "$SING_BOX_ACTION" in
install_extended)
install_sing_box_extended_binary
;;
preserve_extended)
restore_preserved_sing_box_extended
;;
install_stock)
install_stock_sing_box_package
;;
keep)
;;
esac
}
remember_service_state() {
service_status=""
if [ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus enabled >/dev/null 2>&1; then
PODKOP_WAS_ENABLED=1
fi
if [ -x /etc/init.d/podkop-plus ]; then
service_status="$(/etc/init.d/podkop-plus status 2>/dev/null || true)"
if [ "$service_status" = "running" ]; then
PODKOP_WAS_RUNNING=1
return 0
fi
fi
if [ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus running >/dev/null 2>&1; then
PODKOP_WAS_RUNNING=1
return 0
fi
if [ -x /usr/bin/podkop-plus ] && /usr/bin/podkop-plus get_status 2>/dev/null | grep -q '"running":1'; then
PODKOP_WAS_RUNNING=1
fi
}
stop_conflicting_services() {
[ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus stop >/dev/null 2>&1 || true
[ -x /etc/init.d/podkop-plus ] && /etc/init.d/podkop-plus disable >/dev/null 2>&1 || true
}
deactivate_original_podkop_if_present() {
[ -x /etc/init.d/podkop ] || return 0
if /etc/init.d/podkop running >/dev/null 2>&1; then
warn "Detected a running original Podkop service. Stopping it before installing Podkop Plus."
/etc/init.d/podkop stop >/dev/null 2>&1 || warn "Failed to stop the original Podkop service."
fi
if /etc/init.d/podkop enabled >/dev/null 2>&1; then
warn "Detected an enabled original Podkop autostart. Disabling it before installing Podkop Plus."
/etc/init.d/podkop disable >/dev/null 2>&1 || warn "Failed to disable original Podkop autostart."
fi
}
cleanup_legacy_installation() {
backend_package_installed=0
config_backup_file=""
pkg_is_installed "podkop-plus" && backend_package_installed=1
if [ "$PODKOP_PLUS_RESET_CONFIG" -eq 1 ]; then
rm -f /etc/config/podkop-plus || fail "Failed to remove unsupported /etc/config/podkop-plus"
msg "Removed unsupported /etc/config/podkop-plus; a clean default config will be installed."
elif [ -f /etc/config/podkop-plus ]; then
config_backup_file="$TMP_DIR/podkop-plus.config.backup"
cp /etc/config/podkop-plus "$config_backup_file" || fail "Failed to back up /etc/config/podkop-plus before upgrade"
fi
remember_service_state
stop_conflicting_services
pkg_remove_matching_prefix "luci-i18n-podkop-plus"
pkg_remove_if_installed "luci-app-podkop-plus"
if [ -n "$config_backup_file" ] && [ ! -f /etc/config/podkop-plus ]; then
cp "$config_backup_file" /etc/config/podkop-plus || fail "Failed to restore /etc/config/podkop-plus after legacy package removal"
chmod 0644 /etc/config/podkop-plus || true
fi
if [ "$backend_package_installed" -eq 0 ]; then
rm -rf /usr/lib/podkop-plus
rm -f /etc/init.d/podkop-plus
rm -f /usr/bin/podkop-plus
fi
rm -rf /www/luci-static/resources/view/podkop_plus
rm -f /usr/share/luci/menu.d/luci-app-podkop-plus.json
rm -f /usr/share/rpcd/acl.d/luci-app-podkop-plus.json
rm -f /etc/uci-defaults/50_luci-podkop-plus
rm -f /usr/lib/lua/luci/i18n/podkop_plus.ru.lmo
rm -f /usr/lib/lua/luci/i18n/podkop_plus.en.lmo
rm -f /usr/lib/lua/luci/i18n/podkop_plus.ru.lua
rm -f /usr/lib/lua/luci/i18n/podkop_plus.en.lua
}
append_arch_candidate() {
candidate="$1"
[ -n "$candidate" ] || return 0
case "$candidate" in
all|noarch)
return 0
;;
esac
case " $ZAPRET_ARCH_CANDIDATES " in
*" $candidate "*)
return 0
;;
esac
if [ -n "$ZAPRET_ARCH_CANDIDATES" ]; then
ZAPRET_ARCH_CANDIDATES="$ZAPRET_ARCH_CANDIDATES $candidate"
else
ZAPRET_ARCH_CANDIDATES="$candidate"
fi
}
append_arch_candidate_variants() {
candidate="$1"
base_candidate=""
[ -n "$candidate" ] || return 0
append_arch_candidate "$candidate"
case "$candidate" in
*+*)
append_arch_candidate "${candidate%%+*}"
;;
esac
for suffix in _musl _uclibc _glibc -musl -uclibc -glibc .musl .uclibc .glibc; do
case "$candidate" in
*"$suffix")
base_candidate="${candidate%"$suffix"}"
append_arch_candidate "$base_candidate"
;;
esac
done
}
add_arch_family_fallbacks() {
arch="$1"
append_arch_candidate_variants "$arch"
case "$arch" in
aarch64_*)
append_arch_candidate_variants "aarch64_generic"
;;
riscv64_*)
append_arch_candidate_variants "riscv64_generic"
;;
arm_cortex-a7_neon-vfpv4)
append_arch_candidate_variants "arm_cortex-a7_vfpv4"
append_arch_candidate_variants "arm_cortex-a7"
;;
arm_cortex-a7_*)
append_arch_candidate_variants "arm_cortex-a7"
;;
arm_cortex-a9_*)
append_arch_candidate_variants "arm_cortex-a9"
;;
mipsel_24kc_24kf)
append_arch_candidate_variants "mipsel_24kc"
;;
esac
}
resolve_arch_candidates() {
arch_list=""
apk_arch_list=""
release_arch=""
if [ "$PKG_IS_APK" -eq 1 ]; then
if [ -f /etc/apk/arch ]; then
apk_arch_list="$(tr '\r\n' ' ' </etc/apk/arch)"
[ -n "$apk_arch_list" ] && arch_list="$arch_list $apk_arch_list"
fi
apk_arch_list="$(apk --print-arch 2>/dev/null || true)"
[ -n "$apk_arch_list" ] && arch_list="$arch_list $apk_arch_list"
else
arch_list="$(opkg print-architecture 2>/dev/null | awk '$1 == "arch" && $2 !~ /^(all|noarch)$/ {print $2 " " $3}' | sort -k2,2nr | awk '{print $1}')"
fi
release_arch="$(read_openwrt_release_value "DISTRIB_ARCH")"
[ -n "$release_arch" ] && arch_list="$arch_list $release_arch"
if [ -z "$(printf '%s' "$arch_list" | tr -d '[:space:]')" ]; then
arch_list="$(uname -m 2>/dev/null || true)"
fi
for arch in $arch_list; do
case "$arch" in
all|noarch)
continue
;;
esac
[ -n "$TARGET_ARCH" ] || TARGET_ARCH="$arch"
add_arch_family_fallbacks "$arch"
done
[ -n "$TARGET_ARCH" ] || fail "Failed to detect the router package architecture"
msg "Detected package architecture candidates: $ZAPRET_ARCH_CANDIDATES"
}
resolve_zapret_release() {
candidate_name=""
message=""
url="https://api.github.com/repos/remittor/zapret-openwrt/releases/latest"
clear_zapret_download_state
[ "$ZAPRET_REQUESTED" -eq 1 ] || return 0
ZAPRET_RELEASE_JSON="$(http_get "$url" 2>/dev/null || true)"
if [ -z "$ZAPRET_RELEASE_JSON" ]; then
warn_zapret_unavailable "Failed to query zapret release metadata."
ZAPRET_SKIPPED_REASON="release metadata unavailable"
return 0
fi
if ! printf '%s' "$ZAPRET_RELEASE_JSON" | jq -e . >/dev/null 2>&1; then
warn_zapret_unavailable "GitHub returned an invalid zapret release response."
ZAPRET_SKIPPED_REASON="invalid release metadata"
clear_zapret_download_state
return 0
fi
message="$(printf '%s' "$ZAPRET_RELEASE_JSON" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"*|*"rate limit exceeded"*)
warn_zapret_unavailable "GitHub API rate limit reached while resolving zapret."
ZAPRET_SKIPPED_REASON="GitHub API rate limit"
clear_zapret_download_state
return 0
;;
"Not Found")
warn_zapret_unavailable "No published releases found for remittor/zapret-openwrt."
ZAPRET_SKIPPED_REASON="release not found"
clear_zapret_download_state
return 0
;;
esac
ZAPRET_RELEASE_TAG_RESOLVED="$(printf '%s' "$ZAPRET_RELEASE_JSON" | jq -r '.tag_name // empty')"
if [ -z "$ZAPRET_RELEASE_TAG_RESOLVED" ]; then
warn_zapret_unavailable "Failed to detect the zapret release tag."
ZAPRET_SKIPPED_REASON="release tag unavailable"
clear_zapret_download_state
return 0
fi
for arch in $ZAPRET_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$ZAPRET_RELEASE_JSON" | jq -r --arg arch "$arch" '[.assets[] | select(.name | endswith("_" + $arch + ".zip")) | .name][0] // empty')"
if [ -n "$candidate_name" ]; then
ZAPRET_ARCH="$arch"
ZAPRET_BUNDLE_NAME="$candidate_name"
break
fi
done
if [ -z "$ZAPRET_BUNDLE_NAME" ]; then
warn_zapret_unavailable "No zapret package was found for architecture: $TARGET_ARCH. Tried: $ZAPRET_ARCH_CANDIDATES."
ZAPRET_SKIPPED_REASON="package not found for architecture"
clear_zapret_download_state
return 0
fi
ZAPRET_BUNDLE_URL="$(printf '%s' "$ZAPRET_RELEASE_JSON" | jq -r --arg name "$ZAPRET_BUNDLE_NAME" '[.assets[] | select(.name == $name) | .browser_download_url][0] // empty')"
if [ -z "$ZAPRET_BUNDLE_URL" ]; then
warn_zapret_unavailable "Failed to resolve the zapret download URL for $ZAPRET_BUNDLE_NAME."
ZAPRET_SKIPPED_REASON="download URL unavailable"
clear_zapret_download_state
return 0
fi
ZAPRET_PACKAGE_VERSION="$(extract_zapret_bundle_version "$ZAPRET_BUNDLE_NAME")"
[ -n "$ZAPRET_PACKAGE_VERSION" ] || ZAPRET_PACKAGE_VERSION="$(printf '%s\n' "$ZAPRET_RELEASE_TAG_RESOLVED" | sed 's/^v//')"
if [ "$ZAPRET_ALREADY_PRESENT" -eq 1 ]; then
optional_package_update_needed "zapret" "zapret" "$ZAPRET_PACKAGE_VERSION"
update_status=$?
ZAPRET_INSTALLED_VERSION="$OPTIONAL_INSTALLED_VERSION"
case "$update_status" in
0)
return 0
;;
1)
ZAPRET_UP_TO_DATE=1
ZAPRET_BUNDLE_URL=""
ZAPRET_BUNDLE_NAME=""
ZAPRET_PACKAGE_VERSION=""
return 0
;;
*)
ZAPRET_SKIPPED_REASON="installed version unavailable"
clear_zapret_download_state
return 0
;;
esac
fi
}
get_openwrt_release_series() {
release="$(read_openwrt_release_value "DISTRIB_RELEASE")"
printf '%s\n' "$release" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p'
}
resolve_byedpi_release() {
candidate_name=""
message=""
asset_ext="ipk"
release_series=""
response=""
tag=""
url="https://api.github.com/repos/DPITrickster/ByeDPI-OpenWrt/releases?per_page=30"
clear_byedpi_download_state
[ "$BYEDPI_REQUESTED" -eq 1 ] || return 0
[ "$PKG_IS_APK" -eq 1 ] && asset_ext="apk"
release_series="$(get_openwrt_release_series)"
response="$(http_get "$url" 2>/dev/null || true)"
if [ -z "$response" ]; then
warn_byedpi_unavailable "Failed to query ByeDPI release metadata."
BYEDPI_SKIPPED_REASON="release metadata unavailable"
return 0
fi
if ! printf '%s' "$response" | jq -e . >/dev/null 2>&1; then
warn_byedpi_unavailable "GitHub returned an invalid ByeDPI release response."
BYEDPI_SKIPPED_REASON="invalid release metadata"
clear_byedpi_download_state
return 0
fi
message="$(printf '%s' "$response" | jq -r 'if type == "object" then (.message // empty) else empty end' 2>/dev/null)"
case "$message" in
*"API rate limit"*|*"rate limit exceeded"*)
warn_byedpi_unavailable "GitHub API rate limit reached while resolving ByeDPI."
BYEDPI_SKIPPED_REASON="GitHub API rate limit"
clear_byedpi_download_state
return 0
;;
"Not Found")
warn_byedpi_unavailable "No published releases found for DPITrickster/ByeDPI-OpenWrt."
BYEDPI_SKIPPED_REASON="release not found"
clear_byedpi_download_state
return 0
;;
esac
if [ -n "$release_series" ]; then
tag="$(printf '%s' "$response" | jq -r --arg series "$release_series" '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| select(((.tag_name // "") | contains($series)) or ((.name // "") | contains($series)))
| .tag_name // empty
' | sed -n '1p')"
fi
if [ -z "$tag" ]; then
tag="$(printf '%s' "$response" | jq -r '
.[]
| select(((.draft // false) | not) and ((.prerelease // false) | not))
| .tag_name // empty
' | sed -n '1p')"
fi
if [ -z "$tag" ]; then
warn_byedpi_unavailable "Failed to detect the ByeDPI release tag."
BYEDPI_SKIPPED_REASON="release tag unavailable"
clear_byedpi_download_state
return 0
fi
BYEDPI_RELEASE_TAG_RESOLVED="$tag"
BYEDPI_RELEASE_JSON="$(printf '%s' "$response" | jq -c --arg tag "$tag" '.[] | select(.tag_name == $tag)' | sed -n '1p')"
for arch in $ZAPRET_ARCH_CANDIDATES; do
candidate_name="$(printf '%s' "$BYEDPI_RELEASE_JSON" | jq -r --arg arch "$arch" --arg ext "$asset_ext" '
.assets[]
| select(((.name | startswith("byedpi_")) or (.name | startswith("byedpi-"))) and (.name | endswith("." + $ext)))
| select((.name | contains("_" + $arch + "." + $ext)) or (.name | contains("-" + $arch + "." + $ext)))
| .name
' | sed -n '1p')"
if [ -n "$candidate_name" ]; then
BYEDPI_ARCH="$arch"
BYEDPI_PACKAGE_NAME="$candidate_name"
break
fi
done
if [ -z "$BYEDPI_PACKAGE_NAME" ]; then
warn_byedpi_unavailable "No ByeDPI package was found for architecture: $TARGET_ARCH. Tried: $ZAPRET_ARCH_CANDIDATES."
BYEDPI_SKIPPED_REASON="package not found for architecture"
clear_byedpi_download_state
return 0
fi
BYEDPI_PACKAGE_URL="$(printf '%s' "$BYEDPI_RELEASE_JSON" | jq -r --arg name "$BYEDPI_PACKAGE_NAME" '.assets[] | select(.name == $name) | .browser_download_url' | sed -n '1p')"
if [ -z "$BYEDPI_PACKAGE_URL" ]; then
warn_byedpi_unavailable "Failed to resolve the ByeDPI download URL for $BYEDPI_PACKAGE_NAME."
BYEDPI_SKIPPED_REASON="download URL unavailable"
clear_byedpi_download_state
return 0
fi
BYEDPI_PACKAGE_VERSION="$(extract_arch_package_version "$BYEDPI_PACKAGE_NAME" "$BYEDPI_ARCH")"
if [ "$BYEDPI_ALREADY_PRESENT" -eq 1 ]; then
optional_package_update_needed "ByeDPI" "byedpi" "$BYEDPI_PACKAGE_VERSION"
update_status=$?
BYEDPI_INSTALLED_VERSION="$OPTIONAL_INSTALLED_VERSION"
case "$update_status" in
0)
return 0
;;
1)
BYEDPI_UP_TO_DATE=1
BYEDPI_PACKAGE_URL=""
BYEDPI_PACKAGE_NAME=""
BYEDPI_PACKAGE_VERSION=""
return 0
;;
*)
BYEDPI_SKIPPED_REASON="installed version unavailable"
clear_byedpi_download_state
return 0
;;
esac
fi
}
detect_awg_release_context() {
system_board_json=""
release_target=""
clear_awg_download_state
if command_exists ubus && command_exists jsonfilter; then
system_board_json="$(ubus call system board 2>/dev/null || true)"
if [ -n "$system_board_json" ]; then
AWG_OPENWRT_VERSION="$(printf '%s' "$system_board_json" | jsonfilter -e '@.release.version' 2>/dev/null | head -n 1)"
release_target="$(printf '%s' "$system_board_json" | jsonfilter -e '@.release.target' 2>/dev/null | head -n 1)"
AWG_ARCH="$(printf '%s' "$system_board_json" | jsonfilter -e '@.release.arch' 2>/dev/null | head -n 1)"
fi
fi
[ -n "$AWG_OPENWRT_VERSION" ] || AWG_OPENWRT_VERSION="$(read_openwrt_release_value "DISTRIB_RELEASE")"
[ -n "$release_target" ] || release_target="$(read_openwrt_release_value "DISTRIB_TARGET")"
[ -n "$AWG_ARCH" ] || AWG_ARCH="$(read_openwrt_release_value "DISTRIB_ARCH")"
[ -n "$AWG_ARCH" ] || AWG_ARCH="$TARGET_ARCH"
if [ -z "$AWG_OPENWRT_VERSION" ] || [ -z "$release_target" ] || [ -z "$AWG_ARCH" ]; then
warn_awg_unavailable "Failed to detect OpenWrt release, target or package architecture for AmneziaWG."
AWG_SKIPPED_REASON="OpenWrt release context unavailable"
clear_awg_download_state
return 1
fi
case "$release_target" in
*/*)
AWG_TARGET="${release_target%%/*}"
AWG_SUBTARGET="${release_target#*/}"
;;
*)
warn_awg_unavailable "Unsupported OpenWrt target format for AmneziaWG: $release_target."
AWG_SKIPPED_REASON="unsupported OpenWrt target format"
clear_awg_download_state
return 1
;;
esac
AWG_VERSION="1.0"
AWG_LUCI_PACKAGE_NAME="luci-app-amneziawg"
if version_ge "$AWG_OPENWRT_VERSION" "24.10.3"; then
AWG_VERSION="2.0"
AWG_LUCI_PACKAGE_NAME="luci-proto-amneziawg"
fi
if [ "$PKG_IS_APK" -eq 1 ]; then
AWG_PACKAGE_EXT="apk"
else
AWG_PACKAGE_EXT="ipk"
fi
AWG_RELEASE_TAG_RESOLVED="v$AWG_OPENWRT_VERSION"
AWG_BASE_URL="https://github.com/Slava-Shchipunov/awg-openwrt/releases/download/${AWG_RELEASE_TAG_RESOLVED}"
return 0
}
append_awg_package_file() {
package_file="$1"
if [ -n "$AWG_PACKAGE_FILES" ]; then
AWG_PACKAGE_FILES="$AWG_PACKAGE_FILES $package_file"
else
AWG_PACKAGE_FILES="$package_file"
fi
}
download_awg_package() {
package_name="$1"
package_ext="$AWG_PACKAGE_EXT"
fallback_ext="ipk"
candidate_name=""
candidate_file=""
[ "$package_ext" = "ipk" ] && fallback_ext="apk"
for ext in "$package_ext" "$fallback_ext"; do
candidate_name="${package_name}_v${AWG_OPENWRT_VERSION}_${AWG_ARCH}_${AWG_TARGET}_${AWG_SUBTARGET}.${ext}"
candidate_file="$TMP_DIR/$candidate_name"
if download_with_retry "$AWG_BASE_URL/$candidate_name" "$candidate_file" "$candidate_name"; then
append_awg_package_file "$candidate_file"
return 0
fi
done
return 1
}
extract_ipk_control_field() {
package_file="$1"
field_name="$2"
control_dir="$TMP_DIR/control.$$"
value=""
[ -f "$package_file" ] || return 0
rm -rf "$control_dir"
mkdir -p "$control_dir" || return 0
if tar -xzf "$package_file" -C "$control_dir" ./control.tar.gz >/dev/null 2>&1 &&
tar -xzf "$control_dir/control.tar.gz" -C "$control_dir" ./control >/dev/null 2>&1; then
value="$(awk -v field="$field_name" '
index($0, field ":") == 1 {
sub("^[^:]*:[[:space:]]*", "")
print
exit
}
' "$control_dir/control" 2>/dev/null)"
fi
rm -rf "$control_dir"
printf '%s\n' "$value"
}
extract_apk_control_field() {
package_file="$1"
field_name="$2"
metadata_key=""
case "$field_name" in
Package)
metadata_key="name"
;;
Version)
metadata_key="version"
;;
*)
return 0
;;
esac
command_exists apk || return 0
apk adbdump "$package_file" 2>/dev/null | sed -n "s/^ ${metadata_key}:[[:space:]]*//p" | sed -n '1p'
}
extract_package_control_field() {
package_file="$1"
field_name="$2"
case "$package_file" in
*.ipk)
extract_ipk_control_field "$package_file" "$field_name"
;;
*.apk)
extract_apk_control_field "$package_file" "$field_name"
;;
esac
}
extract_downloaded_package_name() {
package_file="$1"
package_name=""
package_name="$(extract_package_control_field "$package_file" "Package")"
if [ -z "$package_name" ]; then
package_name="$(basename "$package_file" | sed 's/[._-]v\?[0-9].*$//')"
fi
printf '%s\n' "$package_name"
}
extract_downloaded_package_version() {
package_file="$1"
package_version=""
package_version="$(extract_package_control_field "$package_file" "Version")"
if [ -z "$package_version" ]; then
package_version="$(extract_package_version "$(basename "$package_file")")"
fi
printf '%s\n' "$package_version"
}
get_installed_kernel_package_version() {
if [ "$PKG_IS_APK" -eq 1 ]; then
apk info -v kernel 2>/dev/null | sed 's/^kernel-//' | sed -n '1p'
else
opkg info kernel 2>/dev/null | sed -n 's/^Version:[[:space:]]*//p' | sed -n '1p'
fi
}
validate_awg_kmod_package() {
kmod_file=""
depends=""
required_kernel=""
installed_kernel=""
[ "$PKG_IS_APK" -eq 0 ] || return 0
for package_file in $AWG_PACKAGE_FILES; do
case "$(basename "$package_file")" in
kmod-amneziawg_*.ipk)
kmod_file="$package_file"
break
;;
esac
done
[ -n "$kmod_file" ] || return 0
depends="$(extract_ipk_control_field "$kmod_file" "Depends")"
required_kernel="$(printf '%s\n' "$depends" | sed -n 's/.*kernel *( *= *\([^),]*\).*/\1/p' | sed -n '1p')"
installed_kernel="$(get_installed_kernel_package_version)"
[ -n "$required_kernel" ] || return 0
[ -n "$installed_kernel" ] || return 0
if [ "$required_kernel" != "$installed_kernel" ]; then
warn_awg_unavailable "Downloaded kmod-amneziawg requires kernel $required_kernel, but this router has $installed_kernel."
AWG_SKIPPED_REASON="kernel ABI mismatch for kmod-amneziawg"
clear_awg_download_state
return 1
fi
return 0
}
validate_awg_update_needed() {
update_needed=0
comparable_packages=0
package_name=""
candidate_version=""
installed_version=""
[ "$AWG_ALREADY_PRESENT" -eq 1 ] || return 0
for package_file in $AWG_PACKAGE_FILES; do
package_name="$(extract_downloaded_package_name "$package_file")"
candidate_version="$(extract_downloaded_package_version "$package_file")"
if [ -z "$package_name" ] || [ -z "$candidate_version" ]; then
warn_awg_unavailable "Failed to parse downloaded AmneziaWG package metadata."
AWG_SKIPPED_REASON="package metadata unavailable"
clear_awg_download_state
return 2
fi
case "$package_name" in
luci-i18n-amneziawg-*)
[ "$PODKOP_PLUS_I18N_REQUESTED" -eq 1 ] || continue
;;
esac
comparable_packages=$((comparable_packages + 1))
installed_version="$(get_installed_package_version "$package_name")"
if [ -z "$installed_version" ]; then
msg "AmneziaWG package $package_name is missing; installing $candidate_version."
update_needed=1
continue
fi
if package_version_newer "$candidate_version" "$installed_version"; then
msg "Found AmneziaWG package update for $package_name: $installed_version -> $candidate_version"
update_needed=1
fi
done
if [ "$comparable_packages" -eq 0 ]; then
warn_awg_unavailable "Failed to parse downloaded AmneziaWG package metadata."
AWG_SKIPPED_REASON="package metadata unavailable"
clear_awg_download_state
return 2
fi
if [ "$update_needed" -eq 1 ]; then
return 0
fi
msg "AmneziaWG packages are already up to date."
AWG_UP_TO_DATE=1
clear_awg_download_state
AWG_UP_TO_DATE=1
return 1
}
download_awg_packages() {
[ "$AWG_REQUESTED" -eq 1 ] || return 0
detect_awg_release_context || return 0
msg "Resolving AmneziaWG $AWG_VERSION packages from Slava-Shchipunov/awg-openwrt@$AWG_RELEASE_TAG_RESOLVED"
if ! download_awg_package "kmod-amneziawg"; then
warn_awg_unavailable "Failed to download kmod-amneziawg for OpenWrt $AWG_OPENWRT_VERSION ($AWG_ARCH, $AWG_TARGET/$AWG_SUBTARGET)."
AWG_SKIPPED_REASON="kmod-amneziawg package unavailable"
clear_awg_download_state
return 0
fi
if ! download_awg_package "amneziawg-tools"; then
warn_awg_unavailable "Failed to download amneziawg-tools for OpenWrt $AWG_OPENWRT_VERSION ($AWG_ARCH, $AWG_TARGET/$AWG_SUBTARGET)."
AWG_SKIPPED_REASON="amneziawg-tools package unavailable"
clear_awg_download_state
return 0
fi
if ! download_awg_package "$AWG_LUCI_PACKAGE_NAME"; then
warn_awg_unavailable "Failed to download $AWG_LUCI_PACKAGE_NAME for OpenWrt $AWG_OPENWRT_VERSION ($AWG_ARCH, $AWG_TARGET/$AWG_SUBTARGET)."
AWG_SKIPPED_REASON="$AWG_LUCI_PACKAGE_NAME package unavailable"
clear_awg_download_state
return 0
fi
if [ "$AWG_VERSION" = "2.0" ] && [ "$PODKOP_PLUS_I18N_REQUESTED" -eq 1 ]; then
if ! download_awg_package "luci-i18n-amneziawg-ru"; then
warn "Russian LuCI translation for AmneziaWG was not downloaded; continuing with core AmneziaWG packages."
fi
fi
validate_awg_kmod_package || return 0
validate_awg_update_needed || return 0
AWG_PACKAGE_VERSION="$AWG_OPENWRT_VERSION"
}
decide_zapret_installation() {
case "$ZAPRET_INSTALL_CHOICE" in
0|no|NO|false|FALSE|n|N)
if is_zapret_present; then
ZAPRET_ALREADY_PRESENT=1
ZAPRET_SKIPPED_REASON="provider update disabled by installer option"
warn "Detected an existing zapret provider, but zapret update is disabled by installer option."
else
ZAPRET_SKIPPED_REASON="installation disabled by installer option"
warn "Continuing without zapret provider."
fi
return 0
;;
esac
if is_zapret_present; then
ZAPRET_ALREADY_PRESENT=1
ZAPRET_REQUESTED=1
msg "Detected an existing zapret provider. Checking for an updated package."
return 0
fi
case "$ZAPRET_INSTALL_CHOICE" in
1|yes|YES|true|TRUE|y|Y)
ZAPRET_REQUESTED=1
return 0
;;
esac
if [ ! -t 0 ]; then
ZAPRET_SKIPPED_REASON="not requested; rerun with --with-zapret to install the optional provider"
warn "Continuing without zapret provider."
return 0
fi
if confirm_prompt "$(localized_prompt install_zapret)"; then
ZAPRET_REQUESTED=1
return 0
fi
ZAPRET_SKIPPED_REASON="installation declined by user"
warn "Continuing without zapret provider."
}
decide_byedpi_installation() {
case "$BYEDPI_INSTALL_CHOICE" in
0|no|NO|false|FALSE|n|N)
if is_byedpi_present; then
BYEDPI_ALREADY_PRESENT=1
BYEDPI_SKIPPED_REASON="provider update disabled by installer option"
warn "Detected an existing ByeDPI provider, but ByeDPI update is disabled by installer option."
else
BYEDPI_SKIPPED_REASON="installation disabled by installer option"
warn "Continuing without ByeDPI provider."
fi
return 0
;;
esac
if is_byedpi_present; then
BYEDPI_ALREADY_PRESENT=1
BYEDPI_REQUESTED=1
msg "Detected an existing ByeDPI provider. Checking for an updated package."
return 0
fi
case "$BYEDPI_INSTALL_CHOICE" in
1|yes|YES|true|TRUE|y|Y)
BYEDPI_REQUESTED=1
return 0
;;
esac
if [ ! -t 0 ]; then
BYEDPI_SKIPPED_REASON="not requested; rerun with --with-byedpi to install the optional provider"
warn "Continuing without ByeDPI provider."
return 0
fi
if confirm_prompt "$(localized_prompt install_byedpi)"; then
BYEDPI_REQUESTED=1
return 0
fi
BYEDPI_SKIPPED_REASON="installation declined by user"
warn "Continuing without ByeDPI provider."
}
decide_awg_installation() {
case "$AWG_INSTALL_CHOICE" in
0|no|NO|false|FALSE|n|N)
if is_awg_present; then
AWG_ALREADY_PRESENT=1
AWG_SKIPPED_REASON="package update disabled by installer option"
warn "Detected an existing AmneziaWG installation, but AmneziaWG update is disabled by installer option."
else
AWG_SKIPPED_REASON="installation disabled by installer option"
warn "Continuing without AmneziaWG packages."
fi
return 0
;;
esac
case "$AWG_INSTALL_CHOICE" in
1|yes|YES|true|TRUE|y|Y)
if is_awg_present; then
AWG_ALREADY_PRESENT=1
msg "Detected an existing AmneziaWG installation. Refreshing packages by installer option."
fi
AWG_REQUESTED=1
return 0
;;
esac
if is_awg_present; then
AWG_ALREADY_PRESENT=1
AWG_REQUESTED=1
msg "Detected an existing AmneziaWG installation. Checking for updated packages."
return 0
fi
if [ ! -t 0 ]; then
AWG_SKIPPED_REASON="not requested; rerun with --with-awg to install AmneziaWG OpenWrt packages"
warn "Continuing without AmneziaWG packages."
return 0
fi
if confirm_prompt "$(localized_prompt install_awg)"; then
AWG_REQUESTED=1
return 0
fi
AWG_SKIPPED_REASON="installation declined by user"
warn "Continuing without AmneziaWG packages."
}
decide_i18n_installation() {
luci_lang="$(get_luci_main_lang)"
case "$luci_lang" in
ru|ru_*|ru-*)
INSTALLER_LANG="ru"
PODKOP_PLUS_I18N_REQUESTED=1
msg "Выбран русский язык LuCI. Пакет русского интерфейса будет установлен."
return 0
;;
esac
INSTALLER_LANG="en"
if confirm_prompt "Установить русский язык интерфейса? (Install the Russian interface language package?)"; then
INSTALLER_LANG="ru"
PODKOP_PLUS_I18N_REQUESTED=1
return 0
fi
INSTALLER_LANG="en"
warn "Continuing without the Russian interface language package."
}
download_podkop_plus_packages() {
PODKOP_PLUS_BACKEND_FILE="$TMP_DIR/$PODKOP_PLUS_BACKEND_NAME"
PODKOP_PLUS_APP_FILE="$TMP_DIR/$PODKOP_PLUS_APP_NAME"
PODKOP_PLUS_I18N_FILE=""
download_with_retry "$PODKOP_PLUS_BACKEND_URL" "$PODKOP_PLUS_BACKEND_FILE" "$PODKOP_PLUS_BACKEND_NAME" || fail "Failed to download $PODKOP_PLUS_BACKEND_NAME"
download_with_retry "$PODKOP_PLUS_APP_URL" "$PODKOP_PLUS_APP_FILE" "$PODKOP_PLUS_APP_NAME" || fail "Failed to download $PODKOP_PLUS_APP_NAME"
if [ -n "$PODKOP_PLUS_I18N_URL" ]; then
PODKOP_PLUS_I18N_FILE="$TMP_DIR/$PODKOP_PLUS_I18N_NAME"
download_with_retry "$PODKOP_PLUS_I18N_URL" "$PODKOP_PLUS_I18N_FILE" "$PODKOP_PLUS_I18N_NAME" || fail "Failed to download $PODKOP_PLUS_I18N_NAME"
fi
}
download_and_extract_zapret_package() {
bundle_file=""
inner_package_path=""
[ -n "$ZAPRET_BUNDLE_URL" ] || return 0
bundle_file="$TMP_DIR/$ZAPRET_BUNDLE_NAME"
if ! download_with_retry "$ZAPRET_BUNDLE_URL" "$bundle_file" "$ZAPRET_BUNDLE_NAME"; then
warn_zapret_unavailable "Failed to download $ZAPRET_BUNDLE_NAME."
ZAPRET_SKIPPED_REASON="download failed"
clear_zapret_download_state
return 0
fi
if [ "$PKG_IS_APK" -eq 1 ]; then
inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E '^apk/zapret-.*\.apk$' | sed -n '1p')"
else
inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E "^zapret_.*_${ZAPRET_ARCH}\.ipk$" | sed -n '1p')"
[ -n "$inner_package_path" ] || inner_package_path="$(unzip -l "$bundle_file" | awk '{print $4}' | grep -E '^zapret_.*\.ipk$' | sed -n '1p')"
fi
if [ -z "$inner_package_path" ]; then
warn_zapret_unavailable "Failed to locate the zapret package inside $ZAPRET_BUNDLE_NAME."
ZAPRET_SKIPPED_REASON="package archive layout is unsupported"
clear_zapret_download_state
return 0
fi
ZAPRET_PACKAGE_NAME="$(basename "$inner_package_path")"
ZAPRET_PACKAGE_FILE="$TMP_DIR/$ZAPRET_PACKAGE_NAME"
if ! unzip -p "$bundle_file" "$inner_package_path" > "$ZAPRET_PACKAGE_FILE"; then
warn_zapret_unavailable "Failed to extract $ZAPRET_PACKAGE_NAME."
ZAPRET_SKIPPED_REASON="package extraction failed"
clear_zapret_download_state
return 0
fi
if [ ! -s "$ZAPRET_PACKAGE_FILE" ]; then
warn_zapret_unavailable "The extracted zapret package is empty."
ZAPRET_SKIPPED_REASON="empty package archive"
clear_zapret_download_state
return 0
fi
[ -n "$ZAPRET_PACKAGE_VERSION" ] || ZAPRET_PACKAGE_VERSION="$(extract_zapret_bundle_version "$ZAPRET_BUNDLE_NAME")"
[ -n "$ZAPRET_PACKAGE_VERSION" ] || ZAPRET_PACKAGE_VERSION="$(extract_arch_package_version "$ZAPRET_PACKAGE_NAME" "$ZAPRET_ARCH")"
}
download_byedpi_package() {
[ -n "$BYEDPI_PACKAGE_URL" ] || return 0
BYEDPI_PACKAGE_FILE="$TMP_DIR/$BYEDPI_PACKAGE_NAME"
if ! download_with_retry "$BYEDPI_PACKAGE_URL" "$BYEDPI_PACKAGE_FILE" "$BYEDPI_PACKAGE_NAME"; then
warn_byedpi_unavailable "Failed to download $BYEDPI_PACKAGE_NAME."
BYEDPI_SKIPPED_REASON="download failed"
clear_byedpi_download_state
return 0
fi
if [ ! -s "$BYEDPI_PACKAGE_FILE" ]; then
warn_byedpi_unavailable "The downloaded ByeDPI package is empty."
BYEDPI_SKIPPED_REASON="empty package"
clear_byedpi_download_state
return 0
fi
}
install_packages() {
if [ -n "$ZAPRET_PACKAGE_FILE" ]; then
if pkg_install_files "$ZAPRET_PACKAGE_FILE"; then
ZAPRET_INSTALLED=1
disable_installed_zapret_service
else
ZAPRET_SKIPPED_REASON="provider package installation failed"
ZAPRET_PACKAGE_VERSION=""
warn_zapret_unavailable "Failed to install the zapret provider package."
fi
fi
if [ -n "$BYEDPI_PACKAGE_FILE" ]; then
if pkg_install_files "$BYEDPI_PACKAGE_FILE"; then
BYEDPI_INSTALLED=1
disable_installed_byedpi_service
else
BYEDPI_SKIPPED_REASON="provider package installation failed"
BYEDPI_PACKAGE_VERSION=""
warn_byedpi_unavailable "Failed to install the ByeDPI provider package."
fi
fi
if [ -n "$AWG_PACKAGE_FILES" ]; then
if [ "$AWG_LUCI_PACKAGE_NAME" = "luci-proto-amneziawg" ]; then
pkg_remove_if_installed "luci-app-amneziawg"
else
pkg_remove_if_installed "luci-proto-amneziawg"
fi
# shellcheck disable=SC2086
if pkg_install_files $AWG_PACKAGE_FILES; then
AWG_INSTALLED=1
else
AWG_SKIPPED_REASON="package installation failed"
AWG_PACKAGE_VERSION=""
warn_awg_unavailable "Failed to install AmneziaWG packages."
fi
fi
pkg_install_files "$PODKOP_PLUS_BACKEND_FILE" || fail "podkop-plus installation failed"
pkg_install_files "$PODKOP_PLUS_APP_FILE" || fail "luci-app-podkop-plus installation failed"
if [ -n "$PODKOP_PLUS_I18N_FILE" ]; then
pkg_install_files "$PODKOP_PLUS_I18N_FILE" || fail "luci-i18n-podkop-plus-ru installation failed"
fi
}
disable_installed_zapret_service() {
[ -n "$ZAPRET_PACKAGE_FILE" ] || return 0
[ -x /etc/init.d/zapret ] || return 0
/etc/init.d/zapret stop >/dev/null 2>&1 || true
/etc/init.d/zapret disable >/dev/null 2>&1 || true
if /etc/init.d/zapret status >/dev/null 2>&1; then
warn "Standalone /etc/init.d/zapret is still running after installation. Stop it manually if you do not use standalone zapret."
fi
if /etc/init.d/zapret enabled >/dev/null 2>&1; then
warn "Standalone /etc/init.d/zapret autostart is still enabled after installation. Disable it manually if you do not use standalone zapret."
fi
}
disable_installed_byedpi_service() {
[ -n "$BYEDPI_PACKAGE_FILE" ] || return 0
[ -x /etc/init.d/byedpi ] || return 0
/etc/init.d/byedpi stop >/dev/null 2>&1 || true
/etc/init.d/byedpi disable >/dev/null 2>&1 || true
if /etc/init.d/byedpi status >/dev/null 2>&1; then
warn "Standalone /etc/init.d/byedpi is still running after installation. Stop it manually if you do not use standalone ByeDPI."
fi
if /etc/init.d/byedpi enabled >/dev/null 2>&1; then
warn "Standalone /etc/init.d/byedpi autostart is still enabled after installation. Disable it manually if you do not use standalone ByeDPI."
fi
}
post_install() {
rm -f /var/luci-indexcache* /tmp/luci-indexcache*
rm -f /tmp/podkop-plus.latest-version.cache
rm -f /var/run/podkop-plus/system-info.json
rm -f /var/run/podkop-plus/server-country-cache.json
rm -f /tmp/podkop-plus/system-info.json
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload >/dev/null 2>&1 || true
if [ "$PODKOP_WAS_ENABLED" -eq 1 ] && [ -x /etc/init.d/podkop-plus ]; then
/etc/init.d/podkop-plus enable >/dev/null 2>&1 || true
fi
if [ "$PODKOP_WAS_RUNNING" -eq 1 ] && [ -x /etc/init.d/podkop-plus ]; then
/etc/init.d/podkop-plus start >/dev/null 2>&1 || /etc/init.d/podkop-plus restart >/dev/null 2>&1 || warn "Failed to start Podkop Plus after upgrade."
fi
if [ "$ZAPRET_INSTALLED" -eq 1 ]; then
disable_installed_zapret_service
warn "The zapret provider was installed as an external upstream package."
warn "Podkop Plus does not modify /etc/config/zapret or luci-app-zapret settings."
msg "Standalone /etc/init.d/zapret service and autostart were disabled after provider installation."
fi
if [ "$BYEDPI_INSTALLED" -eq 1 ]; then
disable_installed_byedpi_service
warn "The ByeDPI provider was installed as an external upstream package."
warn "Podkop Plus manages only its own ciadpi processes for action=byedpi."
msg "Standalone /etc/init.d/byedpi service and autostart were disabled after provider installation."
fi
if [ "$AWG_INSTALLED" -eq 1 ]; then
warn "AmneziaWG was installed as regular OpenWrt VPN interface support."
warn "Import or create the AWG interface in LuCI, keep Route Allowed IPs disabled, then use it in Podkop Plus with action=vpn."
warn "The installer does not rewrite /etc/config/network or /etc/config/podkop-plus."
fi
}
main() {
trap cleanup EXIT HUP INT TERM
parse_args "$@"
check_root
init_tmp_dir
detect_fetcher
sync_time
check_system
decide_i18n_installation
decide_zapret_installation
decide_byedpi_installation
decide_awg_installation
decide_sing_box_installation
decide_unsupported_config_migration
deactivate_original_podkop_if_present
pkg_list_update || fail "Failed to update package lists"
ensure_bootstrap_tool "jq" "jq"
if [ "$ZAPRET_REQUESTED" -eq 1 ]; then
ensure_bootstrap_tool "unzip" "unzip"
fi
resolve_podkop_plus_release
remove_conflicting_dns_proxy
remove_old_sing_box_if_needed
if [ "$ZAPRET_REQUESTED" -eq 1 ] || [ "$BYEDPI_REQUESTED" -eq 1 ] || [ "$AWG_REQUESTED" -eq 1 ]; then
resolve_arch_candidates
fi
if [ "$ZAPRET_REQUESTED" -eq 1 ]; then
resolve_zapret_release
fi
if [ "$BYEDPI_REQUESTED" -eq 1 ]; then
resolve_byedpi_release
fi
if [ "$AWG_REQUESTED" -eq 1 ]; then
download_awg_packages
fi
cleanup_legacy_installation
prepare_sing_box_action_before_install
download_podkop_plus_packages
download_and_extract_zapret_package
download_byedpi_package
install_packages
apply_sing_box_action_after_install
post_install
msg "Podkop Plus $PODKOP_PLUS_PACKAGE_VERSION has been installed successfully"
msg "Source release: ${REPO_OWNER}/${REPO_NAME}@${PODKOP_PLUS_RELEASE_TAG}"
if [ "$ZAPRET_INSTALLED" -eq 1 ]; then
msg "zapret $ZAPRET_PACKAGE_VERSION installed for architecture $ZAPRET_ARCH"
msg "zapret source: remittor/zapret-openwrt@${ZAPRET_RELEASE_TAG_RESOLVED}"
elif [ "$ZAPRET_UP_TO_DATE" -eq 1 ]; then
msg "zapret is already up to date ($ZAPRET_INSTALLED_VERSION)"
elif [ -n "$ZAPRET_SKIPPED_REASON" ]; then
warn "zapret was not installed or updated: $ZAPRET_SKIPPED_REASON"
elif [ "$ZAPRET_ALREADY_PRESENT" -eq 1 ]; then
msg "Using the existing zapret installation"
fi
if [ "$BYEDPI_INSTALLED" -eq 1 ]; then
msg "ByeDPI $BYEDPI_PACKAGE_VERSION installed for architecture $BYEDPI_ARCH"
msg "ByeDPI source: DPITrickster/ByeDPI-OpenWrt@${BYEDPI_RELEASE_TAG_RESOLVED}"
elif [ "$BYEDPI_UP_TO_DATE" -eq 1 ]; then
msg "ByeDPI is already up to date ($BYEDPI_INSTALLED_VERSION)"
elif [ -n "$BYEDPI_SKIPPED_REASON" ]; then
warn "ByeDPI was not installed or updated: $BYEDPI_SKIPPED_REASON"
elif [ "$BYEDPI_ALREADY_PRESENT" -eq 1 ]; then
msg "Using the existing ByeDPI installation"
fi
if [ "$AWG_INSTALLED" -eq 1 ]; then
msg "AmneziaWG packages installed for OpenWrt $AWG_PACKAGE_VERSION ($AWG_ARCH, $AWG_TARGET/$AWG_SUBTARGET)"
msg "AmneziaWG source: Slava-Shchipunov/awg-openwrt@${AWG_RELEASE_TAG_RESOLVED}"
elif [ "$AWG_UP_TO_DATE" -eq 1 ]; then
msg "AmneziaWG packages are already up to date"
elif [ -n "$AWG_SKIPPED_REASON" ]; then
warn "AmneziaWG was not installed or updated: $AWG_SKIPPED_REASON"
elif [ "$AWG_ALREADY_PRESENT" -eq 1 ]; then
msg "Using the existing AmneziaWG installation"
fi
warn "Open LuCI and review your rules before enabling Podkop Plus"
}
main "$@"