nikovpn_openwrt/install.sh

888 lines
No EOL
33 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
REL_URL="https://git.n1ko.dev/Niko/nikovpn_openwrt/releases/download/bins"
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() {
# ВАЖНО: не использовать tr '[:space:]' — старые busybox не знают
# POSIX-классы и вырезают литеральные буквы из кредов
if [ -n "$NIKOVPN_CREDS" ]; then
CRED=$(printf '%s' "$NIKOVPN_CREDS" | tr -d ' \t\r\n')
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 ' \t\r\n')
[ -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 <<EOF
{
"type": "hysteria2",
"tag": "main",
"server": "$(q "$HOST")",
"server_port": $PORT,
$passwordj,
"tls": { "enabled": true, "server_name": "$(q "$sni")"$([ "$ins" = "1" ] && echo ', "insecure": true') }
}
EOF
}
out_tuic() {
local cc urm sni
get_param congestion_control; cc="$P"; [ -z "$cc" ] && cc="bbr"
get_param udp_relay_mode; urm="$P"; [ -z "$urm" ] && urm="native"
get_param sni; sni="$P"; [ -z "$sni" ] && sni="$HOST"
cat <<EOF
{
"type": "tuic",
"tag": "main",
"server": "$(q "$HOST")",
"server_port": $PORT,
"uuid": "$(q "$USER")",
"password": "$(q "$PASS")",
"congestion_control": "$(q "$cc")",
"udp_relay_mode": "$(q "$urm")",
"tls": { "enabled": true, "server_name": "$(q "$sni")" }
}
EOF
}
out_naive() {
local quic
case "$LINK" in naive+quic:*) quic=true ;; *) quic=false ;; esac
cat <<EOF
{
"type": "naive",
"tag": "main",
"server": "$(q "$HOST")",
"server_port": $PORT,
"username": "$(q "$USER")",
"password": "$(q "$PASS")",
"quic": $quic,
"tls": { "enabled": true, "server_name": "$(q "$HOST")" }
}
EOF
}
out_vless() {
local sec sni flow fp pbk sid uuidj realityj
get_param security; sec="$P"
get_param sni; sni="$P"; [ -z "$sni" ] && sni="$HOST"
get_param flow; flow="$P"
get_param fp; fp="$P"; [ -z "$fp" ] && fp="chrome"
get_param pbk; pbk="$P"
get_param sid; sid="$P"
uuidj="\"uuid\": \"$(q "$USER")\""
[ -n "$flow" ] && uuidj="$uuidj,
\"flow\": \"$(q "$flow")\""
realityj=""
if [ "$sec" = "reality" ]; then
realityj=",
\"reality\": { \"enabled\": true, \"public_key\": \"$(q "$pbk")\", \"short_id\": \"$(q "$sid")\" }"
fi
cat <<EOF
{
"type": "vless",
"tag": "main",
"server": "$(q "$HOST")",
"server_port": $PORT,
$uuidj,
"tls": {
"enabled": true,
"server_name": "$(q "$sni")",
"utls": { "enabled": true, "fingerprint": "$(q "$fp")" }$realityj
}
}
EOF
}
out_ss() {
local dec method sspass plugin plugin_opts pluginj
have base64 || return 1
dec=$(printf '%s' "$UI" | base64 -d 2>/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 <<EOF
{
"type": "shadowsocks",
"tag": "main",
"server": "$(q "$HOST")",
"server_port": $PORT,
"method": "$(q "$method")",
"password": "$(q "$sspass")"$pluginj
}
EOF
}
gen_config() {
local main inbound
case "$PROTOCOL" in
hysteria2) main=$(out_hysteria2) ;;
tuic) main=$(out_tuic) ;;
naive) main=$(out_naive) ;;
vless) main=$(out_vless) ;;
shadowsocks) main=$(out_ss) || die "не удалось разобрать shadowsocks-ссылку" ;;
*) die "неизвестный протокол $PROTOCOL" ;;
esac
if [ "$TUNNEL" = "singbox" ]; then
inbound=$(cat <<EOF
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "0.0.0.0",
"listen_port": $TP_PORT
}
EOF
)
else
inbound=$(cat <<EOF
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": $SOCKS_PORT
}
EOF
)
fi
cat > "$WORK_DIR/config.json" <<EOF
{
"log": { "level": "info", "timestamp": true },
"inbounds": [
$inbound
],
"outbounds": [
$main,
{ "type": "direct", "tag": "direct" }
],
"route": {
"final": "main",
"rules": [
{ "ip_cidr": ["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"], "outbound": "direct" }
]
}
}
EOF
"$SB_PATH" check -c "$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 "$REL_URL/sing-box-$SB_ARCH.gz" "$TMP/sing-box.gz" || \
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 "$REL_URL/ipt2socks-$IPT_ARCH.gz" "$TMP/ipt2socks.gz" || \
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" <<EOF
SB_VER='$SB_VER'
IPT_VER='$IPT_VER'
ARCH='$SB_ARCH'
SB_ARCH='$SB_ARCH'
IPT_ARCH='$IPT_ARCH'
NAIVE_OK='$NAIVE_OK'
PROTOCOL='$PROTOCOL'
LINK='$LINK'
HOST='$HOST'
PORT='$PORT'
SNI='$SNI'
MODE='$MODE'
FW='$FW'
TUNNEL='$TUNNEL'
SOCKS_PORT='$SOCKS_PORT'
TP_PORT='$TP_PORT'
EOF
}
write_fw() {
cat > "$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" <<EOF2
table inet nikovpn {
set bypass4 { type ipv4_addr; flags interval;
elements = { $NFT_PRIVATE } }
set vpn_ports { type inet_service; elements = { $VPN_PORT } }
EOF2
if [ "$TUNNEL" = "singbox" ]; then
cat >> "$NFT_FILE" <<EOF2
chain prerouting_tcp {
type filter hook prerouting priority -1; policy accept;
ip daddr @bypass4 return
tcp dport 53 return
meta l4proto tcp tproxy to :$TP_PORT meta mark set 1
}
chain prerouting_udp {
type filter hook prerouting priority -1; policy accept;
ip daddr @bypass4 return
udp dport 53 return
meta l4proto udp tproxy to :$TP_PORT meta mark set 1
}
}
EOF2
else
cat >> "$NFT_FILE" <<EOF2
chain prerouting_tcp {
type nat hook prerouting priority 0; policy accept;
ip daddr @bypass4 return
tcp dport 53 return
meta l4proto tcp redirect to :$TP_PORT
}
chain output_tcp {
type nat hook output priority 0; policy accept;
ip daddr @bypass4 return
fib daddr type local return
tcp dport 53 return
tcp dport @vpn_ports return
meta l4proto tcp redirect to :$TP_PORT
}
EOF2
if [ "$MODE" = "full" ]; then
cat >> "$NFT_FILE" <<EOF2
chain prerouting_udp {
type filter hook prerouting priority -1; policy accept;
ip daddr @bypass4 return
udp dport 53 return
meta l4proto udp tproxy to :$TP_PORT meta mark set 1
}
}
EOF2
else
echo '}' >> "$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 "Проверяю обновления ..."
dl "$REL_URL/versions" "$TMP/versions" || \
dl "$BASE_URL/versions" "$TMP/versions" || \
cp "$REPO_DIR/versions" "$TMP/versions" 2>/dev/null
[ -s "$TMP/versions" ] || die "не могу скачать 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