Skip to content

v1.10.3

Latest

Choose a tag to compare

@sleep3r sleep3r released this 04 Aug 11:56
73569be
🇷🇺 Что нового (RU)

Коротко

v1.10.3 — три исправления, найденные при переносе живого прокси на новый сервер. Каждое проверено на реальной системе, а не выведено из чтения кода.

Главное: из-за неверного чтения /proc сборка с аппаратным AES (x86_64_v3) не выбиралась никогда и ни у кого — все установки на x86_64 молча работали на базовой сборке с программным AES. Обновление рекомендуется всем; при use_middle_proxy = true эффект на нагрузку CPU от видео особенно заметен.

Все чтения /proc возвращали пустоту

sys.readFileAllocAbsolute/Cwd использовали Reader.allocRemaining, который берёт размер результата из stat().size. Все файлы procfs сообщают размер 0, поэтому чтение возвращало пустой срез. Замерено на Linux:

(a) allocRemaining, пустой буфер:  0 байт
(b) allocRemaining, реальный буфер: 0 байт
(c) stat size = 0
(d) цикл readSliceShort:           1087 байт   ← реальное содержимое

Следствие: supportsV3() никогда не видел строку flags и всегда возвращал false, поэтому кандидат x86_64_v3 в release.zig даже не пробовался, и каждая установка на x86_64 тихо откатывалась на базовую сборку — ту самую, о которой прокси потом предупреждает:

AES backend is software-only for this build/target. MiddleProxy video traffic will be CPU-heavy.

После исправления тот же хост выбирает mtproto-proxy-linux-x86_64_v3, и предупреждение исчезает.

Тот же шаблон исправлен в monitoring.zig (VmRSS через /proc/self/status, секунды CPU через /proc/self/stat и лимиты памяти cgroup — всё это молча возвращало пустоту, то есть метрики памяти и CPU не работали) и в резервном пути /proc/meminfo в main.zig, который был замаскирован тем, что обычно первым отвечает sysinfo(2).

Чтение теперь падает с StreamTooLong, а не обрезает данные, — иначе частично прочитанный config.toml разобрался бы как корректный, но более короткий документ. Из-за этого лимит на /proc/cpuinfo впервые стал значимым и поднят с 256 KiB до 4 MiB: cpuinfo повторяет ~1.1 KiB на каждый логический процессор, и старый лимит отвечал бы «нет v3» уже после ~235 потоков — ровно на тех машинах, где аппаратный AES нужнее всего.

Проверка fronting-домена неверно читала вывод OpenSSL 3.5.x

classifyOpenSslOutput искал только строку Server Temp Key. OpenSSL 3.5.5 для гибрида ML-KEM её не печатает вовсе, выводя вместо неё Negotiated TLS1.3 group: <группа> — поэтому по-настоящему PQ-совместимый домен помечался как непригодный (HRR).

Теперь разбирается значение строки, и это принципиально: сервер, у которого нет общей группы с нашим предложением, всё равно печатает эту строку — со значением <NULL> (проверено на wb.ru и mail.ru, предпочитающих secp521r1; рукопожатие завершается с Cipher is (NONE)). Реакция на одно лишь наличие строки пометила бы их как «однораундный x25519» — ошибка в противоположную сторону.

Проверено вживую на OpenSSL 3.5.5:

ozon.ru → negotiates X25519MLKEM768 (post-quantum) — хороший fronting-домен
wb.ru   → HelloRetryRequest / не-x25519 группа — несовпадение

TCPMSS сообщал об успехе, которого не было, и пропадал после перезагрузки (#383)

Сообщено в #383 на Debian 12/13 и воспроизведено здесь на чистом хосте: после того как установщик напечатал TCPMSS clamping applied, правила в mangle/OUTPUT не оказалось, а /etc/iptables/rules.v4 остался нулевого размера.

Причины: правило применялось по принципу «выстрелил и забыл» (_ = sys.exec(...) catch {}) с безусловным сообщением об успехе; наличие правила не проверялось (в отличие от nfqws); и передавалось голое "iptables", тогда как nfqws.zig намеренно разрешает абсолютные пути, потому что в минимальном root-окружении /usr/sbin может отсутствовать в PATH.

Заменено на генерируемый скрипт плюс oneshot-юнит mtproto-tcpmss.service по образцу уже существующего mtproto-syn-limit.service:

  • переустанавливает правило при каждой загрузке — раньше его ничто не восстанавливало, поскольку iptables-persistent мы намеренно не ставим;
  • проверяет результат через iptables -C и показывает настоящую ошибку iptables;
  • итоговая сводка отражает реальное состояние хоста, как это уже сделано для masking и nfqws;
  • выключение клампа теперь корректно демонтирует юнит, а не оставляет включённый юнит, который каждую загрузку возвращает правило, объявленное в сводке отключённым.

set -e был критичен и отсутствовал — здесь и в synlimit.zig

С одним лишь set -u завершающий exit 0 затирал статус всех предыдущих команд, поэтому oneshot всегда завершался успешно, и systemd показывал active (exited) поверх набора правил, который правило так и не получил. То есть существующий комментарий в synlimit.zig«a failure here fails the unit so the operator notices» — тоже никогда не соответствовал действительности. Оба скрипта переведены на set -eu, и оба теперь передают -w, чтобы конкуренция за xtables-lock с ufw/docker при загрузке не роняла юнит без причины, раз ненулевой статус стал фатальным.

Проверено с подставным iptables под sh, dash и bash:

случай было стало
правило применилось 0 0
правило отвергнуто 0 1
xtables lock (код 4) 0 4
режим flush 0 0

И на живом хосте: намеренно сломанный apply теперь оставляет юнит в состоянии failed, а не active.

Проверено

  • zig build test — 274/274; новые тесты покрывают вердикт для группы <NULL>, наличие set -eu / -w / обратного чтения -C в генерируемом скрипте и семантику юнита при старте и остановке.
  • Кросс-сборки x86_64-linux и aarch64-linux чистые.
  • Все три исправления проверены на живом развёртывании.

Changelog

  • fix(ctl): read procfs correctly, make the TCPMSS clamp real and persistent (#386)

🇬🇧 Release notes (EN)

TL;DR

v1.10.3 carries three fixes found while migrating a live proxy to a new host. Each was verified against a real system rather than inferred from reading the code.

The headline: because of an incorrect /proc read, the hardware-AES (x86_64_v3) build was never selected on any machine — every x86_64 install silently ran the baseline build with software AES. Upgrading is recommended for everyone; with use_middle_proxy = true the effect on video CPU load is especially noticeable.

Every /proc read came back empty

sys.readFileAllocAbsolute/Cwd used Reader.allocRemaining, which sizes its result from stat().size. Every procfs file reports size 0, so the read returned an empty slice. Measured on Linux:

(a) allocRemaining, empty buffer: 0 bytes
(b) allocRemaining, real buffer:  0 bytes
(c) stat size = 0
(d) readSliceShort loop:          1087 bytes   <- the real content

So supportsV3() never saw a flags line and always answered false. release.zig therefore never even tried its x86_64_v3 candidate, and every x86_64 install silently fell back to the baseline build — the one the proxy then warns about:

AES backend is software-only for this build/target. MiddleProxy video traffic will be CPU-heavy.

After the fix the same host resolves mtproto-proxy-linux-x86_64_v3 and the warning is gone.

The identical pattern is fixed in monitoring.zig (VmRSS via /proc/self/status, CPU seconds via /proc/self/stat, and the cgroup memory limits — all silently reporting nothing, i.e. memory and CPU metrics were dead) and in main.zig's /proc/meminfo fallback, which was masked only because sysinfo(2) normally answers first.

The read now fails with StreamTooLong rather than truncating, so a partially-read config.toml can never be parsed as a valid but shorter document. That makes the /proc/cpuinfo limit load-bearing for the first time, so it moves from 256 KiB to 4 MiB: cpuinfo repeats ~1.1 KiB per logical CPU, and the old cap would have answered "no v3" past ~235 threads — exactly the hosts where hardware AES matters most.

Fronting-domain probe misread OpenSSL 3.5.x

classifyOpenSslOutput only matched Server Temp Key. OpenSSL 3.5.5 prints no such line for an ML-KEM hybrid, emitting Negotiated TLS1.3 group: <group> instead — so a genuinely PQ-capable domain was reported as an unusable HRR one.

It now parses the line's value, which matters: a server sharing no group with our offer still prints the line, completed with <NULL> (verified against wb.ru and mail.ru, which prefer secp521r1, and whose handshake ends in Cipher is (NONE)). Reacting to the line's mere presence would have mislabelled those as single-round x25519 — the opposite error.

Confirmed live on OpenSSL 3.5.5:

ozon.ru -> negotiates X25519MLKEM768 (post-quantum) - a good fronting target
wb.ru   -> HelloRetryRequest / non-x25519 group - mismatch

TCPMSS clamp reported success it never achieved, and vanished on reboot (#383)

Reported in #383 on Debian 12/13 and reproduced here on a fresh host: after the installer printed TCPMSS clamping applied, no rule existed in mangle/OUTPUT, and /etc/iptables/rules.v4 was left 0 bytes.

Causes: the clamp was applied fire-and-forget (_ = sys.exec(...) catch {}) and then unconditionally reported success; it never verified the rule landed (unlike nfqws); and it passed a bare "iptables" while nfqws.zig deliberately resolves absolute paths, because a minimal root environment can spawn without /usr/sbin on PATH.

Replaced with a rendered script plus an mtproto-tcpmss.service oneshot, mirroring the existing mtproto-syn-limit.service pattern:

  • re-applies at every boot — nothing else restored it, since we deliberately don't install iptables-persistent
  • verifies via iptables -C and surfaces the real iptables error
  • the final summary reports what the host actually has, matching the existing masking/nfqws discipline
  • turning the clamp off now tears the unit down, instead of leaving an enabled unit re-applying a rule the summary calls disabled

set -e was load-bearing and missing — here and in synlimit.zig

With set -u alone, the trailing exit 0 overwrote every earlier status, so the oneshot always succeeded and systemd reported active (exited) over a ruleset that never received the rule. That means synlimit.zig's existing comment — "a failure here fails the unit so the operator notices" — was never true either. Both scripts now use set -eu, and both pass -w so a boot-time xtables-lock collision with ufw/docker doesn't fail a unit spuriously now that a non-zero status is fatal.

Verified with a stub iptables under sh, dash and bash:

case before after
append succeeds 0 0
append rejected 0 1
xtables lock (exit 4) 0 4
flush mode 0 0

And on the live host, a deliberately broken apply now leaves the unit failed rather than active.

Verified

  • zig build test — 274/274; new tests cover the <NULL> group verdict, the rendered script's set -eu / -w / -C read-back, and the unit's boot and stop semantics.
  • x86_64-linux and aarch64-linux cross-builds clean.
  • All three fixes verified on a live deployment.

Changelog

  • fix(ctl): read procfs correctly, make the TCPMSS clamp real and persistent (#386)

Full Changelog: v1.10.2...v1.10.3