v1.10.3
sleep3r/mtproto.zigv1.10.3Aug 4, 2026by sleep3r
Upgrading?
AI Summary
Fixes critical bugs where /proc reads returned empty data, OpenSSL 3.5.x incorrectly classified domains, and the TCPMSS clamp was not applied or persisted.
Key Highlights
- Fixed /proc reading causing hardware AES selection failure.
- Fixed OpenSSL 3.5.x classification for post-quantum domains.
- Replaced fire-and-forget TCPMSS clamp with a persistent systemd unit.
New Features
- Fix for hardware AES selection
- Fix for OpenSSL PQ domain detection
- Persistent TCPMSS clamp
Full Release Notes
<details open>
<summary>🇷🇺 Что нового (RU)</summary>
## Коротко
`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](https://github.com/sleep3r/mtproto.zig/discussions/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](https://github.com/sleep3r/mtproto.zig/pull/386))
</details>
---
<details>
<summary>🇬🇧 Release notes (EN)</summary>
## 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](https://github.com/sleep3r/mtproto.zig/discussions/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](https://github.com/sleep3r/mtproto.zig/pull/386))
</details>
---
**Full Changelog**: https://github.com/sleep3r/mtproto.zig/compare/v1.10.2...v1.10.3
Related Repositories
lightpanda-io/browser★ 36,845
Lightpanda: the headless browser designed for AI and automation
tigerbeetle/tigerbeetle★ 17,228
The financial transactions database designed for mission critical safety and performance.
nullclaw/nullclaw★ 8,130
Fastest, smallest, and fully autonomous AI assistant infrastructure written in Zig
zml/zml★ 4,194
Any model. Any hardware. Zero compromise. Built with @ziglang / @openxla / MLIR / @bazelbuild
dmtrKovalenko/odiff★ 3,280
A very fast SIMD-first image comparison library (with nodejs API)