🇷🇺 Что нового (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 -Cand 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'sset -eu/-w/-Cread-back, and the unit's boot and stop semantics.x86_64-linuxandaarch64-linuxcross-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