- релиз bins: sing-box/ipt2socks для всех архитектур + versions + install.sh (https://git.n1ko.dev/Niko/nikovpn_openwrt/releases/download/bins/<file>) - install.sh/nikovpn.py/wrappers: источники по порядку — релиз -> f.n1ko.dev (резерв) -> локальный bin/ - статус: traffic_total это всего потрачено, а не лимит (traffic_limit=0 значит безлимит)
886 lines
No EOL
33 KiB
Bash
Executable file
886 lines
No EOL
33 KiB
Bash
Executable file
#!/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() {
|
||
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 <<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 |