Hysteria2: заглушка байт в байт как у nginx

$(cat …) срезал последний перевод строки, и страница masquerade была на
байт короче той, что отдаёт nginx. Читаю файл целиком (установщик и
kit net masq on), запасная запись страницы – без лишнего перевода строки.
This commit is contained in:
maxdmitrov
2026-10-06 18:40:01 +03:00
parent 387d6ae66e
commit 25df91e79a
4 changed files with 29 additions and 4 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
* Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранных сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт, проверено клиентами Mihomo и Xray. Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя.
* Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу. А файлы настроек из копии `--restore` теперь читает строго по списку, как охранник на входе: лишнее имя – и копия не принимается, ничего не меняется. Охранник теперь смотрит и в лицо: каждое значение проверяется по своему формату (порт – цифры, путь – буквы и цифры, адрес – адрес), а странное – повод не пустить копию, а не молча что-то поправить.
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
* Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед.
* Сайт-заглушка теперь у каждого сервера своя: при установке она собирается из двадцати тем и пяти вёрсток со случайным названием, текстами и цветами. Пекарня, переводчик или мастерская по велосипедам – как повезёт. Раньше у многих серверов был один и тот же «облачный» сосед. Hysteria2 показывает ту же страницу байт в байт: раньше теряла по дороге последний перевод строки – мелочь, а близнецы должны быть одинаковыми.
* `kit-sub` на своём порту (`--multi-port`) отвечает по HTTP/1.1 и с `Server: nginx` без лишнего пробела, а на бессмыслицу вместо запроса – честным `400 Bad Request` со строкой статуса. Ещё он говорит `Connection: keep-alive` и соглашается на `http/1.1` при знакомстве по TLS. Как настоящий nginx, а не его двоюродный брат.
* Исправил `kit net masq on`: Hysteria2 после него переставала слушать порт до перезапуска панели, а `kit net fp` и `kit net on` спотыкались о то же. Панель меняет подключение на ходу, а ядро такую правку не принимает. Теперь `kit` проверяет порты после каждой правки и, если что-то закрылось, перезапускает ядро и честно об этом говорит. А `kit check` научился смотреть, запущено ли ядро Xray и слушает ли каждое подключение свой порт, TCP и UDP отдельно: nginx на 443/tcp больше не выдаёт себя за Hysteria2 на 443/udp.
* `--restore` на новый IP, где Let's Encrypt не выдал сертификат (лимит или закрытый порт 80), раньше говорил «Готово», а ядро Xray не запускалось: подключения ждали сертификат, которого нет. Теперь я ставлю самоподписанный, закрепляю его отпечаток в ссылках и прямо говорю об этом. А если ядро всё же не поднялось, «Готово» не будет: покажу, где смотреть. Копию с пустой папкой сертификата `--restore` теперь отклоняет сразу, а не на полпути установки.
+3 -2
View File
@@ -691,7 +691,8 @@ main() {
install -d -m 755 /var/www/kit
[[ -s /var/www/kit/index.html ]] || stub_site >/var/www/kit/index.html
chmod 644 /var/www/kit/index.html
STUB_HTML=$(cat /var/www/kit/index.html)
# Байт в байт как у nginx: $(…) срезал бы последний перевод строки, и длина ответа Hysteria2 отличалась бы.
STUB_HTML=$(cat /var/www/kit/index.html; printf x); STUB_HTML=${STUB_HTML%x}
local p
for p in "${PROTOS[@]}"; do "proto_$p"; done
# Первый пользователь – сразу на всех протоколах (как «kit user add»).
@@ -1413,7 +1414,7 @@ setup_nginx() {
fi
install -d -m 755 /var/www/kit
[[ -f /var/www/kit/index.html ]] || { if [[ -n ${STUB_HTML:-} ]]; then printf '%s\n' "$STUB_HTML"; else stub_site; fi; } >/var/www/kit/index.html
[[ -f /var/www/kit/index.html ]] || { if [[ -n ${STUB_HTML:-} ]]; then printf '%s' "$STUB_HTML"; else stub_site; fi; } >/var/www/kit/index.html
# Маршруты – из текущих подключений панели: сайты REALITY, пути WebSocket, сервисы gRPC.
local list reality_sni xhttp_sni mt_sni steal_domain locs="" kind path port
+2 -1
View File
@@ -1764,7 +1764,8 @@ hy_masq() { # on|off
id=$(api GET inbounds/list | jq -r '[.[] | select(.protocol == "hysteria")][0].id // empty')
[[ -n $id ]] || die "На сервере нет Hysteria2."
if [[ $act == on ]]; then
page=$(cat /var/www/kit/index.html 2>/dev/null || true)
# Байт в байт как у nginx (с последним переводом строки).
page=$(cat /var/www/kit/index.html 2>/dev/null && printf x || true); page=${page%x}
# Страницы нет только у старых установок с отдельными портами – тогда простая страница без названия.
[[ -n $page ]] || page='<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Welcome</title></head><body style="font-family:sans-serif;text-align:center;margin-top:20vh"><h1>Welcome</h1><p>This site is being updated. Please check back soon.</p></body></html>'
inbound_patch "$id" '.streamSettings |= ((if type == "string" then fromjson else . end) | .hysteriaSettings.masquerade = {type: "string", content: $c, statusCode: 200, headers: {"content-type": "text/html; charset=utf-8"}})' --arg c "$page" \
+23
View File
@@ -73,5 +73,28 @@ class Stub(unittest.TestCase):
self.assertLess(ends.count(top), N * 0.08, (top, ends.count(top)))
class SamePage(unittest.TestCase):
"""Hysteria2 (masquerade) отдаёт заглушку байт в байт как nginx – с последним переводом строки."""
def read(self, line, var):
import tempfile
d = tempfile.mkdtemp()
f = os.path.join(d, "index.html")
with open(f, "wb") as fh:
fh.write(b"<html>\n<p>x</p>\n\n")
line = line.replace("/var/www/kit/index.html", f)
r = subprocess.run(["bash", "-c", line + '\nprintf %s "$' + var + '"'], capture_output=True)
return r.stdout
def test_installer(self):
line = re.search(r"^ STUB_HTML=\$\(cat /var/www/kit/index\.html.*$", src, re.M).group(0)
self.assertEqual(self.read(line, "STUB_HTML"), b"<html>\n<p>x</p>\n\n")
def test_kit_masq(self):
kit = open(os.path.join(root, "scripts", "kit.sh"), encoding="utf-8").read()
line = re.search(r"^ page=\$\(cat /var/www/kit/index\.html.*$", kit, re.M).group(0)
self.assertEqual(self.read(line, "page"), b"<html>\n<p>x</p>\n\n")
if __name__ == "__main__":
unittest.main()