Правки по независимому аудиту: --restore (сертификат домена, xver при сбое, LINK_HOST), REALITY-2 при панели по домену, резервная копия базы через SQLite, ответы kit-sub как у nginx, меньше параллельность подбора сайта, перемешивание запасных сайтов, нейтральные тексты

This commit is contained in:
maxdmitrov
2026-10-05 16:51:26 +03:00
parent 00319a6655
commit 7425c672b8
8 changed files with 85 additions and 40 deletions
+2
View File
@@ -1 +1,3 @@
*.pyc
.DS_Store
__pycache__/
+1 -1
View File
@@ -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), спасибо.
+5 -5
View File
@@ -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.
+2 -2
View File
@@ -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`.
## Резервная копия
+24 -13
View File
@@ -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"
Binary file not shown.
+18 -7
View File
@@ -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"<html>\r\n<head><title>{code} {phrase}</title></head>\r\n<body>\r\n<center><h1>{code} {phrase}</h1></center>\r\n"
"<hr><center>nginx</center>\r\n</body>\r\n</html>\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)
+33 -12
View File
@@ -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 ;;