#!/bin/sh # ============================================================================ # NikoVPN — универсальный установщик VPN для OpenWRT # # Принцип: sing-box -> ipt2socks -> nftables/iptables (прозрачный прокси) # # Быстрая установка (прямо на роутере, нужен git): # git clone git.n1ko.dev:Niko/nikovpn_openwrt.git # cd nikovpn_openwrt # ./install.sh # # Команды: # sh install.sh установить / переустановить # sh install.sh update обновить (пересканировать протокол/сервер) # sh install.sh status показать статус # sh install.sh uninstall удалить # # Требования: # - креды "login:password" (без кавычек): файл /root/creds # на роутере или переменная окружения NIKOVPN_CREDS # - доступ в интернет (скрипт тянет всё только с f.n1ko.dev и vpnd.n1ko.dev; # если рядом с install.sh есть каталог bin/ — бинарники берутся из него) # # Скрипт сам определяет архитектуру (openwrt_release/apk/opkg/uname), # выбирает лучший протокол и сервер (Hysteria2 > TUIC > Naive > VLESS > SS) # и настраивает прозрачный прокси. # ============================================================================ BASE_URL="https://f.n1ko.dev/nikovpn_openwrt" API_URL="https://vpnd.n1ko.dev/api/v1" WORK_DIR="/etc/nikovpn" CREDS_FILE="/root/creds" SB_PATH="/usr/bin/sing-box" IPT_PATH="/usr/bin/ipt2socks" SOCKS_PORT="1080" TP_PORT="12345" RULE_TABLE="100" FW_MARK="0x1" REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd) CMD="${1:-install}" TMP="/tmp/nikovpn.$$" trap 'rm -rf "$TMP"' EXIT INT TERM # ------------------------------- helpers ----------------------------------- have() { command -v "$1" >/dev/null 2>&1; } log() { echo "[*] $*"; } warn() { echo "[!] $*"; } die() { echo "[ERROR] $*" >&2; exit 1; } need_root() { [ "$(id -u)" = "0" ] || die "нужны права root"; } # --- downloader --- DL="" for t in curl wget; do have "$t" && DL="$t" && break; done [ -n "$DL" ] || die "нужен curl или wget" dl() { # $1=url $2=out ; резерв на http:// для f.n1ko.dev local url="$1" out="$2" ok="" if [ "$DL" = curl ]; then curl -fsSL --max-time 120 -o "$out" "$url" && ok=1 [ -z "$ok" ] && case "$url" in https://f.n1ko.dev/*) curl -fsSL --max-time 120 -o "$out" "http://${url#https://}" && ok=1 ;; esac else wget -q -T 120 -O "$out" "$url" && ok=1 [ -z "$ok" ] && case "$url" in https://f.n1ko.dev/*) wget -q -T 120 -O "$out" "http://${url#https://}" && ok=1 ;; esac fi [ -n "$ok" ] } # --- API (HTTP Basic auth) --- API_AUTH=""; API_AUTH_V="" api_init_auth() { if [ "$DL" = curl ]; then API_AUTH="-u"; API_AUTH_V="$CRED" else API_AUTH="--header" API_AUTH_V="Authorization: Basic $(printf '%s' "$CRED" | base64 | tr -d '\n')" fi } # api_get $1=path $2=outfile -> rc: 0=ok,401,403,1=err api_get() { local path="$1" out="$2" code rc if [ "$DL" = curl ]; then code=$(curl -s --max-time 30 -o "$out" -w '%{http_code}' "$API_AUTH" "$API_AUTH_V" "$API_URL$path") [ -s "$out" ] || return 1 case $code in 2*) return 0 ;; 401) return 401 ;; 403) return 403 ;; *) return 1 ;; esac else wget -q -T 30 -O "$out" "$API_AUTH" "$API_AUTH_V" "$API_URL$path" 2>/dev/null rc=$? [ -s "$out" ] || return 1 [ "$rc" = "0" ] && return 0 grep -q '"unauthorized"' "$out" 2>/dev/null && return 401 grep -qE '"(account disabled|account expired|traffic limit)' "$out" 2>/dev/null && return 403 return 1 fi } read_creds() { if [ -n "$NIKOVPN_CREDS" ]; then CRED=$(printf '%s' "$NIKOVPN_CREDS" | tr -d '[:space:]') else [ -f "$CREDS_FILE" ] || die "нет кредов: запишите 'login:password' в $CREDS_FILE или задайте env NIKOVPN_CREDS" CRED=$(grep -v '^[[:space:]]*#' "$CREDS_FILE" 2>/dev/null | grep -v '^[[:space:]]*$' | head -1 | tr -d '[:space:]') [ -n "$CRED" ] || die "$CREDS_FILE пуст — запишите 'login:password'" fi case "$CRED" in *:*) ;; *) die "в кредах нет ':' — формат login:password" ;; esac api_init_auth } # ============================================================================ # ОПРЕДЕЛЕНИЕ АРХИТЕКТУРЫ # ============================================================================ SB_ARCH=""; IPT_ARCH=""; NAIVE_OK=0; UNAME_M="" map_uname() { case "$1" in x86_64|amd64) SB_ARCH=amd64; IPT_ARCH=x86_64; NAIVE_OK=1 ;; aarch64|arm64) SB_ARCH=arm64; IPT_ARCH=aarch64; NAIVE_OK=1 ;; armv7l|armv7) SB_ARCH=armv7; IPT_ARCH=armv7hf; NAIVE_OK=1 ;; armv6l|armv6) SB_ARCH=armv6; IPT_ARCH=armv6; NAIVE_OK=0 ;; armv5tel|armv5te|armv5l) SB_ARCH=armv5; IPT_ARCH=armv5; NAIVE_OK=0 ;; arm) SB_ARCH=armv7; IPT_ARCH=armv7hf; NAIVE_OK=1 ;; mips) SB_ARCH=mips; IPT_ARCH=mips; NAIVE_OK=0 ;; mipsel) SB_ARCH=mipsle; IPT_ARCH=mipsel; NAIVE_OK=1 ;; mips64) SB_ARCH=mips64; IPT_ARCH=mips64; NAIVE_OK=0 ;; mips64el) SB_ARCH=mips64le; IPT_ARCH=mips64el;NAIVE_OK=0 ;; i686|i586|i386|x86) SB_ARCH=386; IPT_ARCH=i686; NAIVE_OK=1 ;; riscv64) SB_ARCH=riscv64; IPT_ARCH=""; NAIVE_OK=1 ;; loongarch64|loong64) SB_ARCH=loong64; IPT_ARCH=""; NAIVE_OK=1 ;; ppc64le) SB_ARCH=ppc64le; IPT_ARCH=""; NAIVE_OK=0 ;; s390x) SB_ARCH=s390x; IPT_ARCH=""; NAIVE_OK=0 ;; *) SB_ARCH=""; IPT_ARCH="" ;; esac } map_openwrt() { case "$1" in x86_64) map_uname x86_64 ;; i386_*) map_uname i686 ;; aarch64_*) map_uname aarch64 ;; arm_cortex-a7*|arm_cortex-a9*|arm_cortex-a8*|arm_cortex-a5*|arm_cortex-a15*) map_uname armv7l ;; arm_arm1176jzf-s_vfp) map_uname armv6l ;; arm_arm926ej-s|arm_fa526|arm_xscale) map_uname armv5l ;; arm_*) map_uname armv7l ;; mips_*) map_uname mips ;; mipsel_*) map_uname mipsel ;; mips64_*) map_uname mips64 ;; mips64el_*) map_uname mips64el ;; riscv64_*) map_uname riscv64 ;; loongarch64_*) map_uname loong64 ;; powerpc_*) map_uname ppc64le ;; *) map_uname "$UNAME_M" ;; esac } detect_arch() { local da="" if [ -r /etc/openwrt_release ]; then . /etc/openwrt_release 2>/dev/null da="$DISTRIB_ARCH" fi if [ -z "$da" ] && have apk; then da=$(apk --print-arch 2>/dev/null) fi if [ -z "$da" ] && have opkg; then da=$(opkg print-architecture 2>/dev/null | awk '$1=="arch" && $2!="all" && $2!="noarch" {print $2; exit}') fi UNAME_M=$(uname -m 2>/dev/null) if [ -n "$da" ]; then map_openwrt "$da"; else map_uname "$UNAME_M"; fi [ -n "$SB_ARCH" ] || die "архитектура не поддерживается (${da:-$UNAME_M}). Напишите автору скрипта." } # ============================================================================ # ВЫБОР ПРОТОКОЛА И СЕРВЕРА # ============================================================================ LINK=""; PROTOCOL=""; HOST=""; PORT=""; SNI=""; FRAG=""; QUERY=""; UI=""; USER=""; PASS="" # пользовательские предпочтения (опционально /etc/nikovpn/config) PREFER_PROTOCOL=""; PREFER_SERVER="" [ -f "$WORK_DIR/config" ] && . "$WORK_DIR/config" urldecode() { printf '%b' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g')" } jesc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } q() { jesc "$(urldecode "$1")"; } get_param() { # $1=name -> $P (percent-encoded) P="" local _q="$QUERY" while [ -n "$_q" ]; do local _kv="${_q%%&*}" [ "$_q" = "$_kv" ] && _q="" || _q="${_q#*&}" case "$_kv" in "$1="*) P="${_kv#*=}"; break ;; esac done } parse_link() { # $1=uri -> HOST PORT USER PASS FRAG QUERY LINK="$1" local rest pq hp rest="${LINK#*://}" FRAG="" case "$LINK" in *\#*) FRAG="${rest##*\#}" ;; esac pq="${rest%%\#*}" QUERY="" case "$pq" in *\?*) QUERY="${pq#*\?}" ;; esac hp="${pq%%\?*}" case "$hp" in *@*) UI="${hp%%@*}"; hp="${hp#*@}" ;; *) UI="" ;; esac hp="${hp%%/*}" # срезать возможный "/" в конце (hysteria2) case "$hp" in *:*) PORT="${hp##*:}"; HOST="${hp%:*}" ;; *) HOST="$hp"; PORT="" ;; esac USER="${UI%%:*}" PASS="${UI#*:}"; [ "$PASS" = "$UI" ] && PASS="" HOST=$(urldecode "$HOST") PORT=$(urldecode "$PORT") USER=$(urldecode "$USER") PASS=$(urldecode "$PASS") } # проверка доступности хоста; QUIC-протоколы проверяем по TCP:443 того же хоста host_alive() { have nc || return 0 local h="$1" p="$2" proto="$3" case "$proto" in hysteria2|tuic) p="443" ;; esac nc -w 2 "$h" "$p" >/dev/null 2>&1 } choose() { # $1=файл со ссылками local links_file="$1" local prefs="hysteria2 tuic naive vless shadowsocks" p pat line first first_ok n for p in $prefs; do [ "$p" = naive ] && [ "$NAIVE_OK" != 1 ] && continue [ -n "$PREFER_PROTOCOL" ] && [ "$PREFER_PROTOCOL" != "$p" ] && continue case "$p" in naive) pat='^naive\+' ;; *) pat="^$p://" ;; esac grep -q "$pat" "$links_file" && { PROTOCOL="$p"; break; } done [ -n "$PROTOCOL" ] || die "ни один протокол недоступен (или предпочтение '$PREFER_PROTOCOL' не найдено)" case "$PROTOCOL" in naive) pat='^naive\+' ;; *) pat="^$PROTOCOL://" ;; esac grep "$pat" "$links_file" > "$TMP/cand" if [ -n "$PREFER_SERVER" ]; then grep -i "$PREFER_SERVER" "$TMP/cand" > "$TMP/cand2" 2>/dev/null [ -s "$TMP/cand2" ] && mv "$TMP/cand2" "$TMP/cand" fi first=""; first_ok=""; n=0 while IFS= read -r line; do n=$((n+1)) [ -z "$first" ] && first="$line" [ $n -gt 4 ] && break parse_link "$line" if host_alive "$HOST" "$PORT" "$PROTOCOL"; then first_ok="$line"; break; fi done < "$TMP/cand" if [ -n "$first_ok" ]; then LINK="$first_ok" elif [ -n "$first" ]; then LINK="$first" else LINK=$(head -1 "$TMP/cand"); fi [ -n "$LINK" ] || die "пустой список серверов от API" parse_link "$LINK" SNI="$HOST" } # ============================================================================ # ГЕНЕРАЦИЯ sing-box config.json # ============================================================================ TUNNEL="ipt2socks" # или singbox (нет ipt2socks под эту архитектуру) out_hysteria2() { local sni ins obf obfpass passwordj get_param sni; sni="$P"; [ -n "$sni" ] || sni="$HOST" get_param insecure; ins="$P" get_param obfs; obf="$P" get_param obfs-password; obfpass="$P" passwordj="\"password\": \"$(q "$USER")\"" [ -n "$obf" ] && passwordj="$passwordj, \"obfs\": { \"type\": \"$(q "$obf")\", \"password\": \"$(q "$obfpass")\" }" cat </dev/null) [ -n "$dec" ] || return 1 method="${dec%%:*}"; sspass="${dec#*:}" get_param plugin; plugin=$(urldecode "$P") plugin_opts="" case "$plugin" in *\;*) plugin_opts="${plugin#*;}"; plugin="${plugin%%;*}" ;; esac pluginj="" [ -n "$plugin" ] && pluginj=", \"plugin\": \"$(q "$plugin")\", \"plugin_opts\": \"$(q "$plugin_opts")\"" cat < "$WORK_DIR/config.json" </dev/null 2>&1 || die "сгенерированный конфиг sing-box не проходит проверку" } # ============================================================================ # УСТАНОВКА БИНАРНИКОВ # ============================================================================ install_bins() { local skip_sb=0 skip_ipt=0 cur_sb cur_ipt mkdir -p "$(dirname "$SB_PATH")" "$(dirname "$IPT_PATH")" cur_sb=""; cur_ipt="" [ -f "$WORK_DIR/state" ] && . "$WORK_DIR/state" 2>/dev/null && { cur_sb="$SB_VER"; cur_ipt="$IPT_VER"; } if [ "$UPDATE" = 1 ] && [ -x "$SB_PATH" ] && [ "$cur_sb" = "$SB_VER" ]; then skip_sb=1; fi if [ "$UPDATE" = 1 ] && [ -x "$IPT_PATH" ] && [ "$cur_ipt" = "$IPT_VER" ]; then skip_ipt=1; fi if [ "$skip_sb" != 1 ]; then log "Получаю sing-box ($SB_ARCH) ..." dl "$BASE_URL/bin/sing-box-$SB_ARCH.gz" "$TMP/sing-box.gz" || \ cp "$REPO_DIR/bin/sing-box-$SB_ARCH.gz" "$TMP/sing-box.gz" 2>/dev/null [ -s "$TMP/sing-box.gz" ] || die "не удалось скачать sing-box" gzip -dc "$TMP/sing-box.gz" > "$TMP/sing-box" 2>/dev/null || die "повреждённый архив sing-box" chmod +x "$TMP/sing-box" "$TMP/sing-box" version >/dev/null 2>&1 || die "sing-box не запускается на этой системе (архитектура?)" mv "$TMP/sing-box" "$SB_PATH" rm -f "$TMP/sing-box.gz" fi if [ "$TUNNEL" = "ipt2socks" ] && [ "$skip_ipt" != 1 ]; then log "Получаю ipt2socks ($IPT_ARCH) ..." dl "$BASE_URL/bin/ipt2socks-$IPT_ARCH.gz" "$TMP/ipt2socks.gz" || \ cp "$REPO_DIR/bin/ipt2socks-$IPT_ARCH.gz" "$TMP/ipt2socks.gz" 2>/dev/null [ -s "$TMP/ipt2socks.gz" ] || die "не удалось скачать ipt2socks" gzip -dc "$TMP/ipt2socks.gz" > "$TMP/ipt2socks" 2>/dev/null || die "повреждённый архив ipt2socks" chmod +x "$TMP/ipt2socks" "$TMP/ipt2socks" -V >/dev/null 2>&1 || die "ipt2socks не запускается на этой системе (архитектура?)" mv "$TMP/ipt2socks" "$IPT_PATH" rm -f "$TMP/ipt2socks.gz" fi } # ============================================================================ # СОСТОЯНИЕ / FW / INIT # ============================================================================ write_state() { cat > "$WORK_DIR/state" < "$WORK_DIR/fw.sh" <<'FWE' #!/bin/sh # NikoVPN firewall (генерируется install.sh) WORK_DIR=/etc/nikovpn STATE="$WORK_DIR/state" [ -f "$STATE" ] && . "$STATE" MODE="${MODE:-tcp}" FW="${FW:-nft}" TUNNEL="${TUNNEL:-ipt2socks}" TP_PORT="${TP_PORT:-12345}" VPN_PORT="${PORT:-0}" MARK="0x1" TABLE="100" NFT_FILE="/etc/nftables.d/10-nikovpn.nft" PRIVATE="0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4" NFT_PRIVATE="$(echo $PRIVATE | tr ' ' ',')" have() { command -v "$1" >/dev/null 2>&1; } ip_rules_add() { ip rule add fwmark $MARK table $TABLE 2>/dev/null ip route add local default dev lo table $TABLE 2>/dev/null true } ip_rules_del() { ip rule del fwmark $MARK table $TABLE 2>/dev/null ip route del local default dev lo table $TABLE 2>/dev/null true } nft_write() { cat > "$NFT_FILE" <> "$NFT_FILE" <> "$NFT_FILE" <> "$NFT_FILE" <> "$NFT_FILE" fi fi } fw_start() { if [ "$FW" = "nft" ]; then have nft || return 1 nft_write nft delete table inet nikovpn 2>/dev/null nft -f "$NFT_FILE" || return 1 if [ "$MODE" = full ] || [ "$TUNNEL" = singbox ]; then ip_rules_add; fi else have iptables || return 1 if [ "$TUNNEL" = singbox ]; then iptables -t mangle -N NIKOVPN_TP 2>/dev/null iptables -t mangle -F NIKOVPN_TP for n in $PRIVATE; do iptables -t mangle -A NIKOVPN_TP -d $n -j RETURN; done iptables -t mangle -A NIKOVPN_TP -m addrtype --dst-type LOCAL -j RETURN iptables -t mangle -A NIKOVPN_TP -p tcp --dport 53 -j RETURN iptables -t mangle -A NIKOVPN_TP -p tcp -j TPROXY --on-port $TP_PORT --tproxy-mark $MARK/$MARK iptables -t mangle -C PREROUTING -p tcp -j NIKOVPN_TP 2>/dev/null || iptables -t mangle -A PREROUTING -p tcp -j NIKOVPN_TP else iptables -t nat -N NIKOVPN 2>/dev/null iptables -t nat -F NIKOVPN for n in $PRIVATE; do iptables -t nat -A NIKOVPN -d $n -j RETURN; done iptables -t nat -A NIKOVPN -m addrtype --dst-type LOCAL -j RETURN iptables -t nat -A NIKOVPN -p tcp --dport 53 -j RETURN iptables -t nat -A NIKOVPN -p tcp --dport $VPN_PORT -j RETURN iptables -t nat -A NIKOVPN -p tcp -j REDIRECT --to-ports $TP_PORT iptables -t nat -C PREROUTING -p tcp -j NIKOVPN 2>/dev/null || iptables -t nat -A PREROUTING -p tcp -j NIKOVPN iptables -t nat -C OUTPUT -p tcp -m addrtype ! --dst-type LOCAL -j NIKOVPN 2>/dev/null || iptables -t nat -A OUTPUT -p tcp -m addrtype ! --dst-type LOCAL -j NIKOVPN fi if [ "$MODE" = full ] || [ "$TUNNEL" = singbox ]; then iptables -t mangle -N NIKOVPN_UDP 2>/dev/null iptables -t mangle -F NIKOVPN_UDP for n in $PRIVATE; do iptables -t mangle -A NIKOVPN_UDP -d $n -j RETURN; done iptables -t mangle -A NIKOVPN_UDP -m addrtype --dst-type LOCAL -j RETURN iptables -t mangle -A NIKOVPN_UDP -p udp --dport 53 -j RETURN iptables -t mangle -A NIKOVPN_UDP -p udp -j TPROXY --on-port $TP_PORT --tproxy-mark $MARK/$MARK iptables -t mangle -C PREROUTING -p udp -j NIKOVPN_UDP 2>/dev/null || iptables -t mangle -A PREROUTING -p udp -j NIKOVPN_UDP ip_rules_add fi grep -q 'nikovpn/fw.sh start' /etc/firewall.user 2>/dev/null || echo "sh /etc/nikovpn/fw.sh start" >> /etc/firewall.user fi } fw_stop() { if [ "$FW" = "nft" ]; then nft delete table inet nikovpn 2>/dev/null rm -f "$NFT_FILE" else iptables -t nat -D PREROUTING -p tcp -j NIKOVPN 2>/dev/null iptables -t nat -D OUTPUT -p tcp -m addrtype ! --dst-type LOCAL -j NIKOVPN 2>/dev/null iptables -t mangle -D PREROUTING -p tcp -j NIKOVPN_TP 2>/dev/null iptables -t mangle -D PREROUTING -p udp -j NIKOVPN_UDP 2>/dev/null iptables -t nat -F NIKOVPN 2>/dev/null; iptables -t nat -X NIKOVPN 2>/dev/null iptables -t mangle -F NIKOVPN_TP 2>/dev/null; iptables -t mangle -X NIKOVPN_TP 2>/dev/null iptables -t mangle -F NIKOVPN_UDP 2>/dev/null; iptables -t mangle -X NIKOVPN_UDP 2>/dev/null sed -i '\#nikovpn/fw.sh start#d' /etc/firewall.user 2>/dev/null fi ip_rules_del } case "$1" in start) fw_start ;; stop) fw_stop ;; *) echo "usage: $0 start|stop"; exit 1 ;; esac FWE chmod +x "$WORK_DIR/fw.sh" } write_init() { cat > "/etc/init.d/nikovpn" <<'INIE' #!/bin/sh /etc/rc.common START=95 STOP=10 USE_PROCD=1 WORK_DIR=/etc/nikovpn STATE="$WORK_DIR/state" [ -f "$STATE" ] && . "$STATE" start_service() { [ -f "$WORK_DIR/config.json" ] || return 1 procd_open_instance singbox procd_set_param command /usr/bin/sing-box run -c "$WORK_DIR/config.json" procd_set_param respawn 3600 5 5 procd_set_param file /var/log/nikovpn.log procd_set_param pidfile /var/run/nikovpn-singbox.pid procd_close_instance if [ "$TUNNEL" = "ipt2socks" ]; then IPT_FLAGS="-R" [ "$MODE" = "tcp" ] && IPT_FLAGS="-R -T" procd_open_instance ipt2socks procd_set_param command /usr/bin/ipt2socks $IPT_FLAGS -l $TP_PORT -s 127.0.0.1 -p $SOCKS_PORT -b 0.0.0.0 procd_set_param respawn 3600 5 5 procd_set_param file /var/log/nikovpn-ipt2socks.log procd_set_param pidfile /var/run/nikovpn-ipt2socks.pid procd_close_instance fi } service_started() { "$WORK_DIR/fw.sh" start } stop_service() { "$WORK_DIR/fw.sh" stop } INIE chmod +x /etc/init.d/nikovpn } # ============================================================================ # ПРОВЕРКА ВОЗМОЖНОСТИ FULL-РЕЖИМА (TCP+UDP через TPROXY) # ============================================================================ probe_full() { have ip || return 1 local ok=1 if [ "$FW" = nft ]; then nft add table inet nikovpn_test 2>/dev/null nft add chain inet nikovpn_test t '{ type filter hook prerouting priority -1; policy accept; }' 2>/dev/null nft add rule inet nikovpn_test t 'meta l4proto udp tproxy to :12345' 2>/dev/null || ok=0 nft delete table inet nikovpn_test 2>/dev/null else iptables -t mangle -N NIKOVPN_TEST 2>/dev/null iptables -t mangle -A NIKOVPN_TEST -p udp -j TPROXY --on-port 12345 --tproxy-mark 0x1/0x1 2>/dev/null || ok=0 iptables -t mangle -F NIKOVPN_TEST 2>/dev/null iptables -t mangle -X NIKOVPN_TEST 2>/dev/null fi [ $ok = 0 ] && return 1 ip rule add fwmark 0x1 table 101 2>/dev/null || return 1 ip rule del fwmark 0x1 table 101 2>/dev/null ip route add local 192.0.2.1/32 dev lo table 101 2>/dev/null || return 1 ip route del local 192.0.2.1/32 dev lo table 101 2>/dev/null return 0 } # ============================================================================ # СТАТУС # ============================================================================ status() { [ -f "$WORK_DIR/state" ] || die "NikoVPN не установлен (нет $WORK_DIR/state)" . "$WORK_DIR/state" echo "=== NikoVPN ===" echo "Протокол: $PROTOCOL" echo "Сервер: $HOST:$PORT" echo "Архитектура: $ARCH (sing-box $SB_VER, ipt2socks $IPT_VER)" echo "Режим: $MODE / $TUNNEL / $FW" pgrep -x sing-box >/dev/null 2>&1 && echo "sing-box: RUNNING" || echo "sing-box: STOPPED" if [ "$TUNNEL" = ipt2socks ]; then pgrep -x ipt2socks >/dev/null 2>&1 && echo "ipt2socks: RUNNING" || echo "ipt2socks: STOPPED" fi if [ -r /var/log/nikovpn.log ]; then echo "--- последние строки /var/log/nikovpn.log ---" tail -n 5 /var/log/nikovpn.log 2>/dev/null fi read_creds 2>/dev/null || return 0 if api_get "/me" "$TMP/me"; then local tu tt td tu=$(sed -n 's/.*"traffic_used": *\([0-9]*\).*/\1/p' "$TMP/me") tt=$(sed -n 's/.*"traffic_total": *\([0-9]*\).*/\1/p' "$TMP/me") td=$(sed -n 's/.*"traffic_day": *\([0-9]*\).*/\1/p' "$TMP/me") echo "Трафик: использовано $tu, за сутки $td, лимит $tt байт" fi } # ============================================================================ # УДАЛЕНИЕ # ============================================================================ uninstall() { need_root if [ -x /etc/init.d/nikovpn ]; then /etc/init.d/nikovpn stop 2>/dev/null /etc/init.d/nikovpn disable 2>/dev/null fi nft delete table inet nikovpn 2>/dev/null rm -f /etc/nftables.d/10-nikovpn.nft for c in NIKOVPN NIKOVPN_TP NIKOVPN_UDP; do iptables -t nat -D PREROUTING -p tcp -j "$c" 2>/dev/null iptables -t mangle -D PREROUTING -p udp -j "$c" 2>/dev/null done ip rule del fwmark 0x1 table 100 2>/dev/null ip route del local default dev lo table 100 2>/dev/null rm -f /etc/init.d/nikovpn /etc/rc.d/S95nikovpn /etc/rc.d/K10nikovpn sed -i '\#nikovpn/fw.sh start#d' /etc/firewall.user 2>/dev/null rm -rf "$WORK_DIR" rm -f "$SB_PATH" "$IPT_PATH" echo "NikoVPN удалён." } # ============================================================================ # УСТАНОВКА # ============================================================================ install() { need_root detect_arch read_creds log "Архитектура: $SB_ARCH (sing-box), ipt2socks: ${IPT_ARCH:-нет}" if [ -z "$IPT_ARCH" ]; then warn "для этой архитектуры нет ipt2socks — использую встроенный tproxy-вход sing-box" TUNNEL="singbox" fi mkdir -p "$WORK_DIR" "$TMP" log "Проверяю обновления с f.n1ko.dev ..." dl "$BASE_URL/versions" "$TMP/versions" || cp "$REPO_DIR/versions" "$TMP/versions" 2>/dev/null [ -s "$TMP/versions" ] || die "не могу скачать $BASE_URL/versions (нет интернета?)" SB_VER=""; IPT_VER="" while IFS='=' read -r k v; do case "$k" in sing-box) SB_VER="$v" ;; ipt2socks) IPT_VER="$v" ;; esac done < "$TMP/versions" [ -n "$SB_VER" ] && [ -n "$IPT_VER" ] || die "неверный файл versions на сервере" log "Версии: sing-box $SB_VER, ipt2socks $IPT_VER" log "Проверяю аккаунт ..." api_get "/me" "$TMP/me"; rc=$? case $rc in 0) ;; 401) die "неверный логин/пароль в $CREDS_FILE" ;; 403) die "аккаунт заблокирован/истёк/превышен лимит — проверьте подписку" ;; *) die "не удалось связаться с $API_URL" ;; esac log "Получаю список серверов ..." api_get "/config?format=links" "$TMP/links"; rc=$? [ $rc = 0 ] && [ -s "$TMP/links" ] || die "не удалось получить список серверов (код $rc)" choose "$TMP/links" log "Выбран протокол: $PROTOCOL" log "Выбран сервер: $HOST:$PORT${FRAG:+ ($FRAG)}" if have nft; then FW=nft elif have iptables; then FW=ipt else die "не найдено ни nftables, ни iptables"; fi MODE=tcp if [ "$TUNNEL" = singbox ]; then MODE=full else case "$PROTOCOL" in naive) MODE=tcp ;; *) if probe_full; then MODE=full; else warn "UDP/TPROXY недоступно — режим только TCP"; MODE=tcp; fi ;; esac fi # остановить старый сервис перед перезаписью if [ -x /etc/init.d/nikovpn ]; then /etc/init.d/nikovpn stop 2>/dev/null fi install_bins log "Генерирую конфиг ..." gen_config write_state write_fw write_init cp "$0" "$WORK_DIR/install.sh" 2>/dev/null chmod +x "$WORK_DIR/install.sh" log "Запускаю сервис ..." /etc/init.d/nikovpn enable 2>/dev/null /etc/init.d/nikovpn start 2>/dev/null sleep 3 if pgrep -x sing-box >/dev/null 2>&1; then log "sing-box запущен. Прозрачный прокси активен." else warn "sing-box не запустился. Смотрите: cat /var/log/nikovpn.log" fi log "Готово. Проверка: sh /etc/nikovpn/install.sh status" } # ============================================================================ # MAIN # ============================================================================ UPDATE=0 case "$CMD" in update) UPDATE=1; install ;; status) status ;; uninstall) uninstall ;; install|-i|reinstall) install ;; *) echo "usage: $0 [install|update|status|uninstall]"; exit 1 ;; esac exit 0