From 7425c672b86adf4b9a7c3b7d326df8c2f3ed8dd3 Mon Sep 17 00:00:00 2001 From: maxdmitrov <56165502+maxdmitrov@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:51:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BD=D0=B5=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D0=BC=D1=83=20=D0=B0=D1=83=D0=B4=D0=B8=D1=82=D1=83:=20--?= =?UTF-8?q?restore=20(=D1=81=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=B0,=20xver=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=81=D0=B1=D0=BE=D0=B5,=20LINK=5FHOST),?= =?UTF-8?q?=20REALITY-2=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BF=D0=BE=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=83,=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=BF=D0=B8=D1=8F=20=D0=B1=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20SQLite,=20=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D1=8B=20kit-sub=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20=D1=83=20nginx,=20=D0=BC=D0=B5=D0=BD=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=B0=D0=BB=D0=BB=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=20=D1=81=D0=B0=D0=B9=D1=82=D0=B0,=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D1=88=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D0=B0=D1=81=D0=BD=D1=8B=D1=85=20=D1=81=D0=B0?= =?UTF-8?q?=D0=B9=D1=82=D0=BE=D0=B2,=20=D0=BD=D0=B5=D0=B9=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + CHANGELOG.md | 2 +- README.md | 10 ++--- manuals/3x-ui.md | 4 +- scripts/3x-ui.sh | 37 ++++++++++------ scripts/__pycache__/kit-sub.cpython-313.pyc | Bin 24953 -> 0 bytes scripts/kit-sub.py | 25 ++++++++--- scripts/kit.sh | 45 ++++++++++++++------ 8 files changed, 85 insertions(+), 40 deletions(-) delete mode 100644 scripts/__pycache__/kit-sub.cpython-313.pyc 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 eb5b0115a01e6920ff1e67812785f200d0adb6f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24953 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\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 ;;