diff --git a/.gitignore b/.gitignore index e43b0f9..86b8fa3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +*.pyc .DS_Store +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6d0e1..d06d9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * Панель 3X-UI обновлена до v3.9.0, а ядро Xray осталось проверенным: новое не всегда лучше, это показал тест. `kit update --panel` обновит панель без смены ядра и настроек, откатит всё при сбое и не спросит лишнего. * `kit` без аргументов открывает меню, как у x-ui: нажали цифру – готово. Всё про сеть собрано в `kit net`: порты, протоколы, сайт маскировки, отпечаток. Команды остались для скриптов, команд стало меньше, а умеет `kit` больше. * Установка теперь по шагам, а все ссылки разложены по полочкам: основные и запасные. Раньше они сыпались потоком, как лента новостей. -* Сайт-прикрытие подбирается среди соседей по подсети, а не «у всех один». Свой домен по-прежнему лучший вариант. REALITY теперь с `xtls-rprx-vision`, Hysteria2 отвечает на чужой запрос страницей сайта. +* Сайт для маскировки подбирается среди соседей по подсети, а не «у всех один». Свой домен по-прежнему лучший вариант. REALITY теперь с `xtls-rprx-vision`, Hysteria2 отвечает на чужой запрос страницей сайта. * `kit check --deep` подключается к серверу настоящим клиентом, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало. * Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо. diff --git a/README.md b/README.md index 91faa08..5f51251 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ сразу во все протоколы с общим лимитом трафика, сроком и числом устройств. - 🧭 **Меню `kit`, как у x-ui.** Пользователи, протоколы, порты, сайт маскировки, проверка и обновление – цифрой, без запоминания команд. -- 🕵️ **Сайт-прикрытие подбирается среди соседей по подсети**, а со своим доменом сервер изображает - ваш собственный сайт. +- 🌐 **Сайт для маскировки подбирается среди соседей по подсети**, а со своим доменом на сервере + стоит ваш собственный сайт. - 🔄 **Обновляется сам, но только подписанными релизами**, а `kit backup` сохраняет копию сервера, которую `--restore` поднимает на чистом VPS. - 🔒 **Сертификат Let's Encrypt на IP** выпускается и продлевается сам, панель скрыта на @@ -278,11 +278,11 @@ sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/insta 2. WireGuard и AmneziaWG приложение разрешает по обычному UDP внутри туннеля, поэтому в «Авто» они не входят (выбираются вручную). 3. Проверьте на [dnsleaktest.com](https://dnsleaktest.com) или [ipleak.net](https://ipleak.net): DNS вашего провайдера там появляться не должен. -## Если связь пропала +## Диагностика и перенос сервера 1. `kit check --deep` – проверит сервер и подключится к нему клиентом: так видно, виноват сервер или сеть. -2. `kit net fp firefox` – другой отпечаток клиента, иногда помогает одной командой (вернуть: `kit net fp chrome`). -3. `kit net site` – новый сайт-прикрытие; `kit net on reality2` – дополнительный REALITY на другом порту. +2. `kit net fp firefox` – другой отпечаток клиента одной командой (вернуть: `kit net fp chrome`). +3. `kit net site` – другой сайт для маскировки; `kit net on reality2` – дополнительный REALITY на другом порту. Один протокол не подключается – попробуйте соседний: у каждого свои сильные стороны (Hysteria2 работает по UDP, остальные по TCP). В подписке Clash «Авто» выбирает сама, в остальных приложениях – вручную. 4. Не помогло – перенесите сервер на другой адрес: `kit backup`, на новом сервере `bash 3x-ui.sh --restore файл`. Если подписка выдавалась на домене, а не на IP, клиентам достаточно сменить адрес в DNS. diff --git a/manuals/3x-ui.md b/manuals/3x-ui.md index dc3217a..20d840b 100644 --- a/manuals/3x-ui.md +++ b/manuals/3x-ui.md @@ -186,8 +186,8 @@ kit net panel domain # панель и подписка открываю или ссылки, которая не зависит от IP), выберите «по домену» при установке (`--panel-on domain`) или позже: `kit net panel domain`. Путь у них секретный и по домену, как и по IP. Лучший сайт маскировки – ваш собственный домен (`--domain` при установке): сервер изображает ваш сайт, а сертификат -и имя совпадают. Без домена установщик ищет сайт среди соседей по подсети сервера: он естественнее, чем известный -сайт на чужом IP. Основные протоколы – REALITY, XHTTP и Hysteria2; остальные запасные, их можно выключить командой +и имя совпадают. Без домена установщик подбирает сайт среди соседей по подсети сервера: около 250 коротких обращений к порту 443 +соседних адресов (не нужно – укажите `--sni сайт` или `--domain`). Основные протоколы – REALITY, XHTTP и Hysteria2; остальные запасные, их можно выключить командой `kit net off`. ## Резервная копия diff --git a/scripts/3x-ui.sh b/scripts/3x-ui.sh index 0c1df97..bf40e30 100755 --- a/scripts/3x-ui.sh +++ b/scripts/3x-ui.sh @@ -58,7 +58,7 @@ ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtpro # (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим. DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto) declare -A PORTS=([xhttp]=8443 [ws]=2053 [trojan]=2083 [vmess]=2087 [ss]=8388 [tuic]=8444 [wg]=51820 [awg]=51821 [awg3]=51822 [mtproto]=8445) -PROTOS=(); CREATED=(); OPEN=() +PROTOS=(); CREATED=(); OPEN=(); NB_ALL=() # Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost. SINGLE=no declare -A INNER=([reality]=10443 [xhttp]=10444 [mtproto]=10445 [web]=10446 [selfweb]=10447 [ws]=10451 [vmess]=10452 [trojan]=10453 [sub]=10460) @@ -119,7 +119,7 @@ SNI_RE='^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za # Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит. SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$' -# Известные сайты на чужом IP не подходят: сайт-прикрытие должен быть «своим» для подсети сервера. +# Известные сайты на чужом IP не подходят: сайт для маскировки должен быть «своим» для подсети сервера. SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$' # Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас. @@ -164,7 +164,7 @@ same_net() { # имя # Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName. nearby_scan() { # основа "a.b.c" local me; me=$(host_ip) - seq 1 254 | xargs -P 24 -I{} bash -c ' + seq 1 254 | xargs -P 8 -I{} bash -c ' ip=$1; [ "$ip" = "$2" ] && exit 0 echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \ | tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true @@ -541,9 +541,10 @@ main() { SNI=$DOMAIN elif [[ -z $SNI ]]; then echo - echo "Ищу сайт-прикрытие в подсети сервера… (около минуты)" + echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)" local -a nb=() mapfile -t nb < <(nearby_sites | shuf) + NB_ALL=("${nb[@]}") if ((${#nb[@]})); then SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-} ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный" @@ -554,7 +555,7 @@ main() { ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true [[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент." else - later "Сайт-прикрытие взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site." + later "Сайт для маскировки взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site." fi local -a pool=() mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf) @@ -573,15 +574,21 @@ main() { if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then local -a nb2=() echo - echo "Подбираю сайты для XHTTP и MTProto… (около минуты)" - mapfile -t nb2 < <(nearby_sites | shuf) + if ((${#NB_ALL[@]})); then + nb2=("${NB_ALL[@]}") + else + echo "Подбираю сайты для XHTTP и MTProto… (около 250 коротких обращений к порту 443 соседних адресов вашей подсети)" + mapfile -t nb2 < <(nearby_sites | shuf) + fi for s in "${nb2[@]}"; do [[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue if [[ -z $SNI2 ]]; then SNI2=$s; elif [[ -z $SNI3 ]]; then SNI3=$s; fi done fi # Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI. - for s in "${SNI_CANDIDATES[@]}"; do + local -a pool2=() + mapfile -t pool2 < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf) + for s in "${pool2[@]}"; do [[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi [[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; } @@ -1487,8 +1494,10 @@ server { NGX fi grep -q 'kit-stream.conf' /etc/nginx/nginx.conf || echo 'include /etc/nginx/kit-stream.conf;' >>/etc/nginx/nginx.conf - if ! nginx -t >/tmp/nginx-test.log 2>&1; then - cat /tmp/nginx-test.log >&2 + local ngx_log + ngx_log=$(mktemp) + if ! nginx -t >"$ngx_log" 2>&1; then + cat "$ngx_log" >&2; rm -f "$ngx_log" # Наш include убираем, чтобы не оставить чужой nginx сломанным. sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf rm -f /etc/nginx/kit-stream.conf @@ -1640,7 +1649,7 @@ sub_links() { # ИМЯ=значение без подстановок и команд. safe_env() { [[ -r $1 ]] || die "В копии нет файла ${1##*/}." - grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю." + grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю." return 0 } @@ -1756,6 +1765,8 @@ PY if [[ -f $tmp/etc/kit/kit.env ]]; then SINGLE=$(. "$tmp/etc/kit/kit.env"; echo "${SINGLE:-no}") SUB_PATH=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_PATH:-}") + LINK_HOST=$(. "$tmp/etc/kit/kit.env"; echo "${LINK_HOST:-}") + [[ $LINK_HOST =~ ^[A-Za-z0-9.:-]*$ ]] || LINK_HOST="" SUB_INTERNAL=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_INTERNAL:-}") [[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ ]] \ || die "В копии странные настройки kit (kit.env) – не восстанавливаю." @@ -1804,7 +1815,7 @@ PY fi # Всё на 443: nginx строит маршруты по подключениям из базы, заглушка – из копии. if [[ $SINGLE == yes ]]; then - [[ -n $DOMAIN ]] && { issue_domain_cert || domain_cert_fallback; } + [[ -n $DOMAIN ]] && { issue_domain_cert || die "Не удалось получить сертификат для $DOMAIN: порт 80 должен быть свободен, а A-запись домена указывать на этот сервер. Лог: /var/log/kit-domain-cert.log"; } install -d -m 755 /var/www/kit [[ -f $tmp/var/www/kit/index.html ]] && install -m 644 "$tmp/var/www/kit/index.html" /var/www/kit/index.html setup_nginx @@ -1815,7 +1826,7 @@ PY # Панель по домену: nginx собран заново, значит блок домена нужно вернуть (домен должен указывать на этот сервер). if grep -q '^PANEL_ON=domain' /etc/kit/kit.env; then sed -i 's/^PANEL_ON=.*/PANEL_ON=ip/' /etc/kit/kit.env - /usr/local/bin/kit net panel domain >/dev/null 2>&1 || warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain" + /usr/local/bin/kit net panel domain >/dev/null 2>&1 || { /usr/local/bin/kit __xver 0 >/dev/null 2>&1 || true; warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain"; } fi brand_xui_menu /usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto" diff --git a/scripts/__pycache__/kit-sub.cpython-313.pyc b/scripts/__pycache__/kit-sub.cpython-313.pyc deleted file mode 100644 index eb5b011..0000000 Binary files a/scripts/__pycache__/kit-sub.cpython-313.pyc and /dev/null differ diff --git a/scripts/kit-sub.py b/scripts/kit-sub.py index 43d1cdf..c145ced 100644 --- a/scripts/kit-sub.py +++ b/scripts/kit-sub.py @@ -157,7 +157,7 @@ def add_auto(clash_yaml): "interval": 300, "tolerance": 100, "lazy": True, "proxies": auto_names}) for g in groups[1:]: lst = g.get("proxies") if isinstance(g, dict) else None - if g.get("type") == "select" and isinstance(lst, list) and any(x in names for x in lst): + if isinstance(g, dict) and g.get("type") == "select" and isinstance(lst, list) and any(x in names for x in lst): g["proxies"] = [AUTO_GROUP] + lst break return yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False).encode() @@ -221,12 +221,23 @@ class Handler(http.server.BaseHTTPRequestHandler): self.wfile.write(body) def send_error(self, code, message=None, explain=None): - # Свои короткие ответы вместо страницы ошибок Python: сканеру не видно, чем отвечает сервер. + # Свои ответы вместо страницы ошибок Python, в том же виде, что у обычного nginx. self.close_connection = True - self.send_plain(code, f"{code} {self.responses.get(code, ('error',))[0].lower()}") + self.send_html_error(code) + + def send_html_error(self, code): + phrase = self.responses.get(code, ("Error",))[0] + body = (f"\r\n{code} {phrase}\r\n\r\n

{code} {phrase}

\r\n" + "
nginx
\r\n\r\n\r\n").encode() + self.send_response(code) + self.send_header("Content-Type", "text/html") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + if self.command != "HEAD": + self.wfile.write(body) def do_other(self): - self.send_plain(404, "404 page not found") + self.send_html_error(404) do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_other @@ -236,16 +247,16 @@ class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): path = self.path.split("?", 1)[0] if not path.startswith(PATH): - return self.send_plain(404, "404 page not found") + return self.send_html_error(404) sub_id = path[len(PATH):] if not SUB_ID.match(sub_id): - return self.send_plain(404, "404 page not found") + return self.send_html_error(404) ua = self.headers.get("User-Agent", "") host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", "")) accept = self.headers.get("Accept", "") code, headers, body = upstream(sub_id, ua, host, accept) if code is None: - return self.send_plain(502, "subscription backend is unavailable") + return self.send_html_error(502) clash = bool(CLASH_UA.search(ua)) and "yaml" in headers.get("content-type", "") awg = clash and not NO_AWG_UA.search(ua) diff --git a/scripts/kit.sh b/scripts/kit.sh index 4ed7044..e7ae31c 100644 --- a/scripts/kit.sh +++ b/scripts/kit.sh @@ -382,7 +382,7 @@ cmd_vision() { # имя|--all [off] cmd_toggle() { # имя true|false valid_name "$1" [[ -n $(client "$1") ]] || die "Нет пользователя $1" - update_user "$1" ".enable = \$v" --argjson v "$2" + update_user "$1" ".enable = \$v | .comment = \"kit\"" --argjson v "$2" if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен – подписка и подключения не работают."; fi } @@ -1231,7 +1231,7 @@ SNI_RE='^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za # Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит. SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$' -# Известные сайты на чужом IP не подходят: сайт-прикрытие должен быть «своим» для подсети сервера. +# Известные сайты на чужом IP не подходят: сайт для маскировки должен быть «своим» для подсети сервера. SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$' # Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас. @@ -1276,7 +1276,7 @@ same_net() { # имя # Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName. nearby_scan() { # основа "a.b.c" local me; me=$(host_ip) - seq 1 254 | xargs -P 24 -I{} bash -c ' + seq 1 254 | xargs -P 8 -I{} bash -c ' ip=$1; [ "$ip" = "$2" ] && exit 0 echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \ | tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true @@ -1398,7 +1398,16 @@ sni_rotate() { # [--nearby] [--dry-run] [сайт] local bak bak=/root/x-ui-before-sni-$(date +%Y%m%d-%H%M%S).db - install -m 600 /dev/null "$bak"; cat /etc/x-ui/x-ui.db >"$bak" + install -m 600 /dev/null "$bak" + python3 - /etc/x-ui/x-ui.db "$bak" <<'PY' || die "Не удалось сохранить копию базы панели. Ничего не менял." +import sqlite3, sys +src = sqlite3.connect("file:%s?mode=ro" % sys.argv[1], uri=True) +dst = sqlite3.connect(sys.argv[2]) +src.backup(dst) +dst.close(); src.close() +PY + # Храним три последние копии: в них ключи сервера. + ls -1t /root/x-ui-before-sni-*.db 2>/dev/null | tail -n +4 | while read -r old; do rm -f -- "$old"; done say "Копия базы панели: $bak" local -a done_i=() @@ -1545,7 +1554,7 @@ reality_add() { # [порт] settings=$(jq -nc '{clients: [], decryption: "none", fallbacks: []}') # Сайт маскировки (или свой домен) – как у основного REALITY, ключи и shortId – свои. stream=$(jq -c --argjson k "$keys" --arg sid "$sid" '(.streamSettings | if type == "string" then fromjson else . end) - | .externalProxy = [] | .tcpSettings.acceptProxyProtocol = false | .realitySettings.xver = 0 + | .externalProxy = [] | .tcpSettings.acceptProxyProtocol = false | .realitySettings.privateKey = $k.privateKey | .realitySettings.shortIds = [$sid] | .realitySettings.settings.publicKey = $k.publicKey | del(.sockopt)' <<<"$base") body=$(jq -nc --argjson port "$port" --arg s "$settings" --arg st "$stream" '{remark: "REALITY-2", enable: true, listen: "", port: $port, protocol: "vless", @@ -1799,7 +1808,7 @@ net_panel() { # [domain|ip] cur=${PANEL_ON:-ip} if [[ -z $want ]]; then if [[ $cur == domain ]]; then echo "Панель и подписка открываются по домену $domain (вернуть на IP: kit net panel ip)" - else echo "Панель и подписка открываются по IP ${HOST:-?}; домен $domain – сайт-прикрытие (открывать и по домену: kit net panel domain)"; fi + else echo "Панель и подписка открываются по IP ${HOST:-?}; домен $domain – сайт для маскировки (открывать и по домену: kit net panel domain)"; fi return 0 fi [[ $want == domain || $want == ip ]] || die "kit net panel [domain|ip]" @@ -1811,18 +1820,25 @@ net_panel() { # [domain|ip] fi tmp=$(mktemp); bak=$(mktemp); cp -p "$PANEL_CONF" "$bak" panel_conf_py render "$PANEL_CONF" "$want" "$SUB_PATH" "$panel_path" >"$tmp" || { rm -f "$tmp" "$bak"; die "Не удалось собрать настройки nginx. Ничего не изменилось."; } - rid=$(sni_targets | awk -F'\t' '$2 == "self" && $3 == "REALITY" {print $1; exit}') - [[ -n $rid ]] || { rm -f "$tmp" "$bak"; die "Не нашёл REALITY со своим доменом."; } + local -a rids=() + mapfile -t rids < <(sni_targets | awk -F'\t' '$2 == "self" {print $1}') + ((${#rids[@]})) || { rm -f "$tmp" "$bak"; die "Не нашёл REALITY со своим доменом."; } + panel_xver() { # 0|1 – у всех REALITY, чей сайт – блок домена (в том числе REALITY-2) + local r + for r in "${rids[@]}"; do + inbound_patch "$r" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$1" || return 1 + done + } # Передача настоящего IP клиента сайту домена (PROXY protocol) нужна, чтобы панель видела, кто заходит. local xv=0; [[ $want == domain ]] && xv=1 - panel_revert() { cat "$bak" >"$PANEL_CONF"; inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$((1 - xv))" || true; nginx -t >/dev/null 2>&1 && systemctl reload nginx || true; } + panel_revert() { cat "$bak" >"$PANEL_CONF"; panel_xver "$((1 - xv))" || true; nginx -t >/dev/null 2>&1 && systemctl reload nginx || true; } if [[ $want == domain ]]; then cat "$tmp" >"$PANEL_CONF" nginx -t >/dev/null 2>&1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Ничего не изменилось."; } - inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x 1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; } + panel_xver 1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; } systemctl reload nginx || { panel_revert; rm -f "$tmp" "$bak"; die "nginx не перезагрузился. Всё возвращено."; } else - inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x 0 || { rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; } + panel_xver 0 || { rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; } cat "$tmp" >"$PANEL_CONF" if ! nginx -t >/dev/null 2>&1 || ! systemctl reload nginx; then panel_revert; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Всё возвращено."; fi fi @@ -1960,7 +1976,7 @@ net_fp() { # отпечаток done < <(api GET inbounds/list | jq -r '.[] | select((.streamSettings | if type == "string" then fromjson else . end) | (.security == "reality" or .security == "tls")) | .id') say "Отпечаток $old → ${B}$f${N}, обновлено подключений: $n." echo "Подписка обновится сама; вручную сохранённые ссылки замените (kit user link имя)." - echo "Меняйте, если связь пропала сразу у многих, а не из-за одного неудачного раза." + echo "Вернуть прежний: kit net fp chrome." } cmd_net() { @@ -2228,6 +2244,11 @@ case "$cmd_key" in "reality add") shift 2; reality_add "$@" ;; "panel update") shift 2; panel_update "$@" ;; "__limit-timer on") limit_timer_on ;; + "__xver "*) # служебная: xver у всех REALITY со своим доменом (возврат после неудачного --restore) + [[ ${2:-} =~ ^[01]$ ]] || exit 1 + while read -r _id; do + inbound_patch "$_id" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$2" || true + done < <(sni_targets | awk -F'\t' '$2 == "self" {print $1}') ;; "__links "*) links_block "${2:-}" "${3:-}" ;; "update "*) shift; cmd_update "$@" ;; "backup "*) cmd_backup ;;