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:
maxdmitrov
2026-10-06 16:50:04 +03:00
parent 159c1b271d
commit 972757ed75
4 changed files with 171 additions and 20 deletions
+78 -18
View File
@@ -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
}