kit net: перезапуск ядра Xray, если правка на ходу закрыла порт; kit check видит упавшее ядро и UDP
Панель 3.9.0 меняет подключение через API ядра: старое удаляет, а новое с маскировкой Hysteria2 ядро 26.6.27 не принимает, и 443/udp закрывается (kit net masq on, kit net fp, kit net on). Теперь после правки kit сверяет порты и при нужде перезапускает ядро (server/restartXrayService, запасной путь – systemctl restart x-ui). kit check: процесс Xray (дочерний у x-ui) и порты всех включённых подключений, TCP и UDP отдельно (nginx на 443/tcp больше не считается за Hysteria2). kit fix перезапускает ядро. Тест: tools/test/kit_ports_test.py.
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
|
||||
* Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед.
|
||||
* `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела – как настоящий nginx, а не его двоюродный брат.
|
||||
* Исправил `kit net masq on`: Hysteria2 после него переставала слушать порт до перезапуска панели, а `kit net fp` и `kit net on` спотыкались о то же. Панель меняет подключение на ходу, а ядро такую правку не принимает. Теперь `kit` проверяет порты после каждой правки и, если что-то закрылось, перезапускает ядро и честно об этом говорит. А `kit check` научился смотреть, запущено ли ядро Xray и слушает ли каждое подключение свой порт, TCP и UDP отдельно: nginx на 443/tcp больше не выдаёт себя за Hysteria2 на 443/udp.
|
||||
* JSON-подписка снова открывается в приложениях на Xray: там, где у vless не было `encryption`, подписка теперь дописывает `none`.
|
||||
* Отдельный установщик Hysteria2 ушёл на пенсию: Hysteria2 и так входит в основную установку, один из 11 протоколов. Кто ставил его отдельно – сервер работает как работал, а `hy2 update` вежливо расскажет, что дальше. Нужен один Hysteria2 без панели – официальный установщик: `bash <(curl -fsSL https://get.hy2.sh/)`.
|
||||
|
||||
|
||||
+6
-2
@@ -156,11 +156,12 @@ kit check --deep # то же и подключиться клиентом (R
|
||||
kit check --fix # исправить безопасное
|
||||
```
|
||||
|
||||
`kit check` проверяет службы, версии, срок сертификата, что панель не торчит наружу, права на файлы с
|
||||
`kit check` проверяет службы, что ядро Xray запущено и каждое включённое подключение слушает свой порт (TCP и UDP
|
||||
отдельно), версии, срок сертификата, что панель не торчит наружу, права на файлы с
|
||||
паролями и ключами, ответ подписки для Happ, Hiddify, v2rayN и Clash, а также что сайт маскировки отвечает по
|
||||
TLS 1.3 и HTTP/2, и подсказывает, что улучшить (сайт маскировки, лишние порты). Вывод можно просто прислать в чат, если нужна помощь.
|
||||
|
||||
`kit check --fix` лечит только безопасное: перезапускает упавшие службы, возвращает права `600`, включает
|
||||
`kit check --fix` лечит только безопасное: перезапускает упавшие службы и ядро Xray, возвращает права `600`, включает
|
||||
автообновление, перечитывает сертификат и снимает TLS у встроенной подписки 3X-UI (его включает команда
|
||||
`x-ui cert`, и подписка перестаёт отвечать). Ничего необратимого он не делает.
|
||||
|
||||
@@ -182,6 +183,9 @@ kit net panel domain # панель и подписка открываю
|
||||
kit net split on # через VPN только список (по умолчанию популярные иностранные сервисы), остальное напрямую; kit net split check | off
|
||||
```
|
||||
|
||||
Панель меняет подключения на ходу, без перезапуска. Если ядро Xray не приняло правку и порт закрылся (так бывает с
|
||||
маскировкой Hysteria2), `kit net` сам перезапускает ядро и говорит об этом: подключения прерываются на пару секунд.
|
||||
|
||||
Со своим доменом панель и подписка по умолчанию открываются по IP (`https://IP/путь/`). Если нужен адрес с доменом (например, для Telegram-бота панели
|
||||
или ссылки, которая не зависит от IP), выберите «по домену» при установке (`--panel-on domain`) или позже: `kit net panel domain`. Путь у них секретный и по домену, как и по IP.
|
||||
|
||||
|
||||
+78
-18
@@ -727,7 +727,7 @@ cmd_update() {
|
||||
|
||||
# kit check только читает и показывает, что с сервером. kit fix чинит безопасное: перезапускает
|
||||
# упавшие службы, возвращает права на файлы, включает автообновление, перечитывает сертификат.
|
||||
CHECK_BAD=0; CHECK_WARN=0; CHECK_FIX=(); CHECK_QUIET=no
|
||||
CHECK_BAD=0; CHECK_WARN=0; CHECK_FIX=(); CHECK_QUIET=no; CHECK_DEEP=no
|
||||
c_ok() { [[ $CHECK_QUIET == yes ]] || printf '%s\n' "${G}✅${N} $*"; }
|
||||
c_info() { [[ $CHECK_QUIET == yes ]] || printf '%s\n' "${D}ℹ $*${N}"; }
|
||||
c_warn() { printf '%s\n' "${Y}⚠${N} $*"; CHECK_WARN=$((CHECK_WARN + 1)); }
|
||||
@@ -736,6 +736,66 @@ c_bad() { printf '%s\n' "${R}❌${N} ${*:2}"; CHECK_BAD=$((CHECK_BAD + 1)); [[
|
||||
# Сайт маскировки отвечает по TLS 1.3 и HTTP/2 (как требует REALITY)? Проверка с самого сервера.
|
||||
sni_alive() { echo | timeout 8 openssl s_client -connect "$1:443" -servername "$1" -tls1_3 -alpn h2 2>/dev/null | grep -q 'ALPN protocol: h2'; }
|
||||
|
||||
# Порт подключения открыт? TCP и UDP смотрим отдельно: на 443/tcp сидит nginx, на 443/udp – Hysteria2.
|
||||
inbound_listens() { # порт tcp|udp|both
|
||||
[[ $1 =~ ^[0-9]+$ ]] || return 1
|
||||
case $2 in
|
||||
tcp) ss -Hltn "sport = :$1" 2>/dev/null | grep -q . ;;
|
||||
udp) ss -Hlun "sport = :$1" 2>/dev/null | grep -q . ;;
|
||||
*) inbound_listens "$1" tcp && inbound_listens "$1" udp ;;
|
||||
esac
|
||||
}
|
||||
|
||||
inbounds_down() { # [список-json] → «имя<TAB>порт<TAB>сеть» включённых подключений, чей порт никто не слушает
|
||||
local list=${1:-} name port proto net
|
||||
[[ -n $list ]] || list=$(api GET inbounds/list)
|
||||
while IFS=$'\t' read -r name port proto; do
|
||||
[[ -n $name ]] || continue
|
||||
net=$(port_net "$proto")
|
||||
inbound_listens "$port" "$net" || printf '%s\t%s\t%s\n' "$name" "$port" "${net/both/tcp+udp}"
|
||||
done < <(jq -r '.[] | select(.enable == true) | [.remark, .port, .protocol] | @tsv' <<<"$list")
|
||||
}
|
||||
|
||||
xray_restart_core() { # только ядро, панель не трогаем; если API не ответил – вся служба
|
||||
if ! (api POST server/restartXrayService >/dev/null) 2>/dev/null; then
|
||||
systemctl restart x-ui; panel_healthy || true
|
||||
fi
|
||||
sleep 3
|
||||
}
|
||||
|
||||
# Панель 3.9.0 меняет подключение на ходу через API ядра: старое удаляет, а новое с маскировкой Hysteria2
|
||||
# ядро 26.6.27 не принимает (форматы настроек у панели и ядра разошлись), и порт закрывается. Сама панель
|
||||
# ядро не перезапускает: config.json уже верный. Поэтому после правки сверяем порты и при нужде перезапускаем ядро.
|
||||
xray_heal() { # «было-не-слушало» (вывод inbounds_down до правки)
|
||||
local before=${1:-} down new
|
||||
sleep 1
|
||||
down=$(inbounds_down 2>/dev/null) || return 0
|
||||
new=$(grep -vxF -f <(printf '%s\n' "$before") <<<"$down" || true)
|
||||
[[ -n $new ]] || return 0
|
||||
say "Ядро Xray не применило изменение на ходу ($(cut -f1 <<<"$new" | paste -sd, - | sed 's/,/, /g')) – перезапускаю ядро, подключения прервутся на пару секунд."
|
||||
xray_restart_core
|
||||
down=$(inbounds_down 2>/dev/null) || return 0
|
||||
new=$(grep -vxF -f <(printf '%s\n' "$before") <<<"$down" || true)
|
||||
[[ -z $new ]] || warn "После перезапуска ядра не слушают: $(cut -f1,2 <<<"$new" | tr '\t' ':' | paste -sd' ' -). Журнал: tail -n 30 /var/log/x-ui/3xui.log"
|
||||
}
|
||||
|
||||
# Ядро Xray запущено и каждое включённое подключение слушает свой порт (при --deep – построчно).
|
||||
check_xray() {
|
||||
local pid list name port proto net total=0 bad=0
|
||||
systemctl is-active -q x-ui || return 0 # про службу сказано выше
|
||||
pid=$(systemctl show -p MainPID --value x-ui 2>/dev/null || true)
|
||||
if [[ $pid =~ ^[1-9][0-9]*$ ]] && pgrep -P "$pid" '^xray-linux-' >/dev/null 2>&1; then c_ok "ядро Xray запущено"
|
||||
else c_bad xray "ядро Xray не запущено (причина в журнале: tail -n 30 /var/log/x-ui/3xui.log)"; fi
|
||||
list=$(api GET inbounds/list 2>/dev/null) || return 0
|
||||
while IFS=$'\t' read -r name port proto; do
|
||||
[[ -n $name ]] || continue
|
||||
net=$(port_net "$proto"); net=${net/both/tcp+udp}; total=$((total + 1))
|
||||
if inbound_listens "$port" "${net/tcp+udp/both}"; then [[ $CHECK_DEEP != yes ]] || c_ok "$name: порт $port/$net слушает"
|
||||
else bad=1; c_bad xray "$name: порт $port/$net не слушает (подключение включено, но порт никто не открыл)"; fi
|
||||
done < <(jq -r '.[] | select(.enable == true) | [.remark, .port, .protocol] | @tsv' <<<"$list")
|
||||
((bad)) || [[ $CHECK_DEEP == yes ]] || c_ok "все включённые подключения слушают свои порты ($total)"
|
||||
}
|
||||
|
||||
check_services() {
|
||||
local pid u
|
||||
if systemctl is-active -q x-ui; then c_ok "служба x-ui работает"; else c_bad svc:x-ui "служба x-ui не работает (journalctl -u x-ui -n 50)"; fi
|
||||
@@ -805,7 +865,7 @@ check_exposure() {
|
||||
else c_ok "панель слушает только localhost"; fi
|
||||
fi
|
||||
if [[ ${SINGLE:-no} == yes ]]; then
|
||||
if ss -ltnH 'sport = :443' 2>/dev/null | grep -q .; then c_ok "порт 443 слушается"; else c_bad svc:nginx "порт 443 никто не слушает"; fi
|
||||
if ss -ltnH 'sport = :443' 2>/dev/null | grep -q .; then c_ok "порт 443/tcp слушается (nginx)"; else c_bad svc:nginx "порт 443/tcp никто не слушает"; fi
|
||||
if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||||
if ufw status | grep -Eq '^443/tcp +ALLOW'; then c_ok "ufw пропускает 443/tcp"; else c_bad "" "ufw не пропускает 443/tcp: ufw allow 443/tcp"; fi
|
||||
fi
|
||||
@@ -907,20 +967,12 @@ check_stealth() {
|
||||
|
||||
run_checks() {
|
||||
CHECK_BAD=0; CHECK_WARN=0; CHECK_FIX=()
|
||||
check_services; check_versions; check_cert; check_exposure; check_subscription; check_masking; check_stealth; check_system
|
||||
check_services; check_xray; check_versions; check_cert; check_exposure; check_subscription; check_masking; check_stealth; check_system
|
||||
}
|
||||
|
||||
# kit check --deep: подключения проверяются так, как это делает клиент, – с самого сервера.
|
||||
# Порты: каждое включённое подключение должно слушать. Сами подключения: настоящий клиент Xray ходит
|
||||
# через REALITY (TCP и XHTTP) на публичный адрес сервера и открывает страницу через него.
|
||||
deep_ports() {
|
||||
local list name port
|
||||
list=$(api GET inbounds/list 2>/dev/null) || { c_bad "" "не удалось получить подключения из панели"; return 0; }
|
||||
while IFS=$'\t' read -r name port; do
|
||||
if ss -Hlntu "sport = :$port" 2>/dev/null | grep -q .; then c_ok "$name: порт $port слушает"
|
||||
else c_bad "" "$name: порт $port не слушает (подключение включено, но сервис не открыл порт)"; fi
|
||||
done < <(jq -r '.[] | select(.enable == true) | [.remark, .port] | @tsv' <<<"$list")
|
||||
}
|
||||
# Порты (каждое включённое подключение должно слушать) – в check_xray, построчно. Сами подключения: настоящий клиент Xray
|
||||
# ходит через REALITY (TCP и XHTTP) и Hysteria2 на публичный адрес сервера и открывает страницу через него.
|
||||
|
||||
# Отпечаток сертификата Hysteria2 из самого подключения: в ссылке его может не быть (сертификат Let's Encrypt или свой).
|
||||
deep_hy_pin() {
|
||||
@@ -1008,8 +1060,9 @@ cmd_check() {
|
||||
[[ -z ${1:-} || $deep == yes ]] || die "Команда: kit check [--deep | --fix]"
|
||||
echo "${B}kit check${N} – проверка сервера (ничего не меняет)"
|
||||
echo
|
||||
CHECK_DEEP=$deep
|
||||
run_checks
|
||||
if [[ $deep == yes ]]; then echo; echo "${B}Подключения${N}"; deep_ports; deep_clients; fi
|
||||
if [[ $deep == yes ]]; then echo; echo "${B}Подключения${N}"; deep_clients; fi
|
||||
echo
|
||||
if ((CHECK_BAD == 0)); then
|
||||
echo "${G}${B}Всё в порядке.${N}$( ((CHECK_WARN)) && echo " Предупреждений: $CHECK_WARN." || true)"
|
||||
@@ -1026,6 +1079,7 @@ fix_action() { # код
|
||||
if nginx -t >/dev/null 2>&1; then say "Перезапускаю nginx"; systemctl restart nginx
|
||||
else warn "Конфиг nginx не проходит проверку (nginx -t) – не трогаю, чтобы не сломать сервер."; fi ;;
|
||||
svc:kit-sub) say "Перезапускаю kit-sub"; systemctl restart kit-sub; sleep 2 ;;
|
||||
xray) say "Перезапускаю ядро Xray"; xray_restart_core ;;
|
||||
subtls)
|
||||
say "Убираю сертификат у встроенной подписки 3X-UI (TLS снимает nginx, kit-sub ходит по http)"
|
||||
local all upd
|
||||
@@ -1567,13 +1621,14 @@ port_set() { # имя порт
|
||||
local ufw_on=no
|
||||
command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active' && ufw_on=yes
|
||||
[[ $ufw_on == no ]] || port_ufw allow "$new" "$net"
|
||||
local before; before=$(inbounds_down "$list")
|
||||
if ! (api POST "inbounds/update/$id" "$body" >/dev/null); then
|
||||
[[ $ufw_on == no ]] || port_ufw delete "$new" "$net"
|
||||
die "Панель не приняла новый порт. Ничего не изменилось."
|
||||
fi
|
||||
[[ $ufw_on == no ]] || port_ufw delete "$old" "$net"
|
||||
sleep 2
|
||||
if ss -Hlntu "sport = :$new" 2>/dev/null | grep -q .; then say "$name теперь на порту ${B}$new${N} (был $old)."
|
||||
xray_heal "$before"
|
||||
if inbound_listens "$new" "$net"; then say "$name теперь на порту ${B}$new${N} (был $old)."
|
||||
else warn "$name переведён на порт $new, но он пока не слушает: проверьте kit check --deep."; fi
|
||||
echo "Подписка обновит ссылки сама. Сохранённые вручную ссылки на $name нужно заменить: ${B}kit user link имя --all${N}"
|
||||
echo "Если у хостера есть свой межсетевой экран (в личном кабинете), откройте в нём порт $new ($net) и закройте $old."
|
||||
@@ -2176,12 +2231,15 @@ inbound_body_of() { # строка подключения (json) → тело д
|
||||
}
|
||||
|
||||
inbound_patch() { # id фильтр-jq [аргументы jq...]: меняет подключение, ошибка – код возврата, не выход
|
||||
local id=$1 filter=$2 row new
|
||||
local id=$1 filter=$2 row new list before
|
||||
shift 2
|
||||
row=$(api GET inbounds/list | jq -c --argjson id "$id" '.[] | select(.id == $id)')
|
||||
list=$(api GET inbounds/list)
|
||||
row=$(jq -c --argjson id "$id" '.[] | select(.id == $id)' <<<"$list")
|
||||
[[ -n $row ]] || return 1
|
||||
new=$(jq -c "$@" "$filter" <<<"$row") || return 1
|
||||
before=$(inbounds_down "$list")
|
||||
(api POST "inbounds/update/$id" "$(inbound_body_of "$new")" >/dev/null) || return 1
|
||||
xray_heal "$before"
|
||||
}
|
||||
|
||||
net_show() {
|
||||
@@ -2241,7 +2299,9 @@ net_toggle() { # on|off имя [-y]
|
||||
else
|
||||
[[ $en == true ]] && { say "$name уже включён."; return 0; }
|
||||
if [[ $listen != 127.0.0.1 ]] && command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active'; then port_ufw allow "$port" "$net"; fi
|
||||
local before; before=$(inbounds_down)
|
||||
(api POST "inbounds/setEnable/$id" '{"enable":true}' >/dev/null) || die "Панель не приняла изменение. Ничего не изменилось."
|
||||
xray_heal "$before"
|
||||
say "$name включён."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Проверки портов подключений в kit.sh (inbound_listens, inbounds_down, xray_heal): TCP и UDP отдельно,
|
||||
перезапуск ядра только если правка закрыла порт. python3 tools/test/kit_ports_test.py"""
|
||||
import json, os, re, subprocess, tempfile, unittest
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
src = open(os.path.join(root, "scripts", "kit.sh"), encoding="utf-8").read()
|
||||
funcs = "".join(re.search(r"^%s\(\) \{.*?^\}\n" % n, src, re.S | re.M).group(0)
|
||||
for n in ("port_net", "inbound_listens", "inbounds_down", "xray_heal"))
|
||||
|
||||
INBOUNDS = [
|
||||
{"remark": "REALITY", "protocol": "vless", "port": 10443, "enable": True},
|
||||
{"remark": "Hysteria2", "protocol": "hysteria", "port": 443, "enable": True},
|
||||
{"remark": "Shadowsocks", "protocol": "shadowsocks", "port": 8388, "enable": True},
|
||||
{"remark": "VMess-WS", "protocol": "vmess", "port": 10452, "enable": False},
|
||||
]
|
||||
|
||||
# ss и api – заглушки: открытые порты лежат в файле «t:443», «u:443»; перезапуск ядра открывает порты из $AFTER.
|
||||
STUBS = r'''
|
||||
set -Eeuo pipefail
|
||||
say() { echo "SAY: $*"; }; warn() { echo "WARN: $*"; }; sleep() { :; }
|
||||
ss() { local p=${1:3:1} port=${2##*:}; grep -qx "$p:$port" "$LISTEN" && echo LISTEN || true; }
|
||||
api() { cat "$LIST"; }
|
||||
xray_restart_core() { echo RESTART; cp "$AFTER" "$LISTEN"; }
|
||||
'''
|
||||
|
||||
|
||||
def run(listen, body, after=None):
|
||||
d = tempfile.mkdtemp()
|
||||
paths = {k: os.path.join(d, k) for k in ("listen", "list", "after")}
|
||||
for k, text in (("listen", "\n".join(listen) + "\n"), ("after", "\n".join(after if after is not None else listen) + "\n"),
|
||||
("list", json.dumps(INBOUNDS))):
|
||||
with open(paths[k], "w") as f:
|
||||
f.write(text)
|
||||
env = dict(os.environ, LISTEN=paths["listen"], LIST=paths["list"], AFTER=paths["after"])
|
||||
r = subprocess.run(["bash", "-c", STUBS + funcs + body], capture_output=True, text=True, env=env)
|
||||
return r.returncode, r.stdout + r.stderr
|
||||
|
||||
|
||||
ALL = ["t:10443", "u:443", "t:8388", "u:8388"]
|
||||
|
||||
|
||||
class Ports(unittest.TestCase):
|
||||
def test_all_listen(self):
|
||||
code, out = run(ALL + ["t:443"], 'inbounds_down; echo END')
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertEqual(out.strip(), "END")
|
||||
|
||||
def test_nginx_tcp_is_not_hysteria_udp(self):
|
||||
code, out = run(["t:10443", "t:443", "t:8388", "u:8388"], 'inbounds_down')
|
||||
self.assertEqual(out.strip(), "Hysteria2\t443\tudp", out)
|
||||
|
||||
def test_shadowsocks_needs_both(self):
|
||||
code, out = run(["t:10443", "u:443", "t:8388"], 'inbounds_down')
|
||||
self.assertEqual(out.strip(), "Shadowsocks\t8388\ttcp+udp", out)
|
||||
|
||||
def test_disabled_ignored(self):
|
||||
code, out = run(ALL, 'inbounds_down | grep -c VMess || true')
|
||||
self.assertEqual(out.strip(), "0")
|
||||
|
||||
def test_bad_port_value(self):
|
||||
code, out = run(ALL, 'inbound_listens "443;id" udp && echo YES || echo NO')
|
||||
self.assertEqual(out.strip(), "NO")
|
||||
|
||||
def test_heal_restarts_when_patch_closed_port(self):
|
||||
code, out = run(["t:10443", "t:8388", "u:8388"], 'xray_heal ""', after=ALL)
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("RESTART", out)
|
||||
self.assertIn("Hysteria2", out)
|
||||
self.assertNotIn("WARN", out)
|
||||
|
||||
def test_heal_quiet_when_nothing_closed(self):
|
||||
code, out = run(ALL, 'xray_heal ""')
|
||||
self.assertEqual(out.strip(), "")
|
||||
|
||||
def test_heal_ignores_port_down_before_patch(self):
|
||||
code, out = run(["t:10443", "t:8388", "u:8388"], 'xray_heal "$(printf "Hysteria2\\t443\\tudp")"')
|
||||
self.assertNotIn("RESTART", out)
|
||||
|
||||
def test_heal_warns_if_restart_did_not_help(self):
|
||||
code, out = run(["t:10443", "t:8388", "u:8388"], 'xray_heal ""')
|
||||
self.assertIn("RESTART", out)
|
||||
self.assertIn("WARN: После перезапуска ядра не слушают: Hysteria2:443", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user