v1.0.0
sleep3r/mtproto.zigv1.0.0Jun 6, 2026by sleep3r
Upgrading?
AI Summary
A major milestone release that transforms the proxy into a secure-by-default, user-friendly tool. It hardens the data plane with ReleaseSafe compilation, adds multi-core support, and overhauls the UX with a bilingual dashboard, instant links, and QR codes.
Key Highlights
- Security hardening: ReleaseSafe, PIE/ASLR, and fuzzed parsers
- ServerHello fingerprint evasion by echoing client cipher
- Multi-core worker model via SO_REUSEPORT
- New health endpoints (`/healthz`, `/readyz`) and close reason metrics
- Product overhaul: bilingual dashboard, instant links, and QR codes
New Features
- ReleaseSafe data plane compilation
- Multi-core worker support
- Health check and metrics endpoints
- Bilingual dashboard with user management
- Instant link generation and QR codes
- Hot-reload of access users via SIGHUP
Full Release Notes
<details open>
<summary>🇷🇺 Что нового (RU)</summary>
## Что решает этот релиз
`v1.0.0` — рубежный релиз. Он превращает быстрый, но «инженерный» прокси в инструмент обхода блокировок, который **безопасен по умолчанию**, виден оператору как на ладони, и которым обычный человек может пользоваться ради своих близких.
Работа шла по четырём направлениям:
- **Безопасность data plane.** Прокси — самый выставленный в интернет процесс, разбирающий недоверенный ввод. Теперь он собирается в ReleaseSafe + PIE/ASLR, парсеры покрыты coverage-guided фаззингом, а цепочка поставки (`uv`, Python-зависимости) зафиксирована и проверяется.
- **Незаметность и наблюдаемость.** ServerHello больше не выдаёт себя константным cipher; structural-тест ловит дрейф отпечатка; новая метрика `mtproto_connection_close_reason_total` даёт измерение «errors» в модели RED — оператор **видит**, как цензор начинает зондировать или блокировать узел.
- **Масштаб и надёжность.** Опциональная многоядерная модель `[server].workers` (SO_REUSEPORT), эндпоинты `/healthz`+`/readyz`, watchdog-совместимый `sd_notify`, устойчивый DNS-fallback, закалённый sandbox systemd-юнита.
- **Продукт.** Полная переработка пути пользователя: мгновенная ссылка, QR, понятный статус-вердикт, двуязычный дашборд, имена пользователей на родном языке — чтобы поставить прокси для мамы можно было без стены акронимов.
`1.0.0` — это ещё и контракт стабильности: `COMPATIBILITY.md` фиксирует под SemVer формат ссылок/секретов, ключи конфига, имена метрик, health-эндпоинты, CLI, systemd-юниты и верификацию релиза.
> [!IMPORTANT]
> `tls_domain` вшит в `ee`-secret, поэтому он — часть `tg://`-ссылки. **Никогда не меняйте его на живом деплое: это инвалидирует все уже розданные ссылки.** Выбирайте домен один раз (дефолт `rutube.ru` — single-round x25519, который наш FakeTLS умеет имитировать).
> [!NOTE]
> Многоядерность (`[server].workers > 1`) — opt-in; поведение по умолчанию (один воркер) не изменилось байт в байт. Существующие `config.toml` менять не нужно: отсутствующие ключи берут безопасные значения по умолчанию.
## Что изменено
### Усиленный бинарь прокси: ReleaseSafe для data plane + PIE/ASLR ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Прокси разбирает недоверенный сетевой ввод (FakeTLS, обфускация, MiddleProxy, SOCKS5, HTTP, TOML) на самом «выставленном в интернет» процессе, но раньше собирался в ReleaseFast — с отключёнными проверками безопасности.
- Новая опция сборки `-Ddataplane_safety` (по умолчанию `true`) принудительно собирает исполняемый файл прокси в ReleaseSafe: off-by-one превращается в безопасный panic, а не в эксплуатируемое undefined behavior.
- `mtbuddy` и bench сохраняют запрошенный режим сборки.
- Замерено: ReleaseSafe-прокси не больше ReleaseFast по размеру; накладные расходы приходятся на циклы парсинга, а не на путь ретрансляции с аппаратным AES.
- Прокси теперь собирается как PIE: загрузчик рандомизирует базовый адрес (ASLR). В ELF `e_type=DYN` на x86_64 и aarch64.
- Полный RELRO уже включён по умолчанию. Stack canaries не используются (им нужен libc, а бинарь собран без libc; проверки границ даёт ReleaseSafe).
### Цепочка поставки: пины и проверка зависимостей тулчейна ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `uv` зафиксирован на версии `0.11.19` и проверяется по опубликованному `.sha256`.
- Python-зависимости зафиксированы точными версиями: `fastapi 0.115.6`, `starlette 0.41.3`, `uvicorn 0.34.0`, `psutil 6.1.1`, `websockets 14.1`.
### Фаззинг парсеров, смотрящих в сеть ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Добавлены coverage-guided fuzz-таргеты для парсеров, работающих с сетевыми данными (через `std.testing.fuzz` / Smith из Zig 0.16).
- Покрыты: TLS ClientHello (`extractSni`/`extractFirstTls13Cipher`/`validateTlsHandshake`), парсеры ответов SOCKS5 и парсер ответа HTTP CONNECT.
- Каждый таргет проверяет отсутствие panic и выхода за границы: panic в парсере = удалённый DoS узла обхода блокировок.
- Детерминированно под `zig build test`, coverage-guided под `zig build test --fuzz`.
### Закрыты дыры: утечка учётных данных, небезопасный temp-файл и DNS-rebinding ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- В http-fetch учётные данные upstream-прокси теперь передаются через curl `--config` (файл с правами `0600`), а не подменой окружения дочернего процесса: пароль не светится в общедоступной командной строке, а curl сохраняет `SSL_CERT_FILE` и локаль.
- `writeCurlProxyConfig` пишет этот `0600`-конфиг по непредсказуемому случайному пути с `O_EXCL` (отказывает, если файл или симлинк уже создан), закрывая локальное окно CWE-377/59 с небезопасным temp-файлом.
- Websocket дашборда `ws_logs` теперь тоже требует loopback Host-pin (защита от DNS-rebinding), как и HTTP-middleware, которое он раньше обходил.
### ServerHello повторяет cipher клиента и закреплён структурным тестом ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Раньше FakeTLS ServerHello всегда возвращал константный cipher `0x1301`. Один и тот же шифр в каждом соединении — это пассивный признак для JA3S/ServerHello, по которому прокси отличается от настоящего сервера.
- Теперь прокси парсит список cipher suites из ClientHello (с учётом GREASE) и подставляет в ServerHello первый не-GREASE набор TLS 1.3, предложенный клиентом (fallback — `0x1301`).
- FakeTLS-клиенты Telegram сам cipher игнорируют, поэтому это чистый выигрыш по незаметности (клиенты сам шифр не проверяют).
- Добавлен изолированный локальный тест: он прогоняет реалистичный ClientHello и проверяет отпечаток ServerHello — cipher следует за ClientHello, в структурных полях нет GREASE, проверяются группа `key_share` и геометрия записей (отличается там, где отличается настоящий сервер; константно там, где это константа). Дрейф отпечатка теперь роняет тест, а не попадает в релиз незамеченным.
- Значение клампа TCPMSS стало настраиваемым: `--tcpmss <n>` (по умолчанию по-прежнему 88).
### Логирование отпечатков ClientHello при деплое ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- На старте прокси логирует на уровне info первые ~16 входящих отпечатков ClientHello, после чего замолкает (`formatClientHelloFingerprint`: не-GREASE ciphers, `supported_groups`, группы `key_share`).
- Это позволяет оператору один раз подключиться настоящим клиентом из-под цензуры и увидеть, что именно он предлагает — например, есть ли в наборе `X25519MLKEM768`.
- Логирование read-only, с ограничением по объёму, без паник, проверено фаззингом.
### Fronting-домен: неизменяемый, с дефолтом, который выигрывает ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `tls_domain` входит в `ee`-secret, поэтому `tg://`-ссылка — это функция от (secret, `tls_domain`). Менять домен на живом деплое НЕЛЬЗЯ: это инвалидирует все уже розданные ссылки. Зафиксировано в описании поля конфига, `COMPATIBILITY.md` и `ARCHITECTURE.md`.
- Наш FakeTLS из трёх записей (один x25519 `key_share`, без HelloRetryRequest) может сымитировать только домен, чей настоящий TLS 1.3 завершается в один раунд на x25519: `rutube.ru`, `ozon.ru`, `vk.com`, `yandex.ru`, `dzen.ru`. Домены вроде `wb.ru`/`mail.ru` предпочитают secp521r1 + HRR и совпасть не могут.
- `warnIfPoorFrontingDomain` мягко проверяет выбранный домен (через openssl) и предупреждает ДО генерации ссылки — пока выбор ещё можно поменять.
- Дефолтный fronting-домен изменён с `wb.ru` на `rutube.ru`. `wb.ru` — ровно тот домен, против которого предупреждает `warnIfPoorFrontingDomain` (HRR, отвергает x25519); раз `tls_domain` неизменяем, бездумно принятый дефолт должен выигрывать, а не проигрывать навсегда. `rutube.ru` — проверенный крупный российский сайт с single-round x25519, доступный и неблокируемый в РФ. Обновлены дефолты установщика/интерактива/маскировки, CLI-help, README, bootstrap.
- Дефолт маскировки — устойчивый локальный self-signed Nginx (после отката хрупкого дефолта с фронтингом реального домена): он всегда доводит handshake до конца, не требует DNS/сети и работает для любого домена. Фронтинг реального домена (`mask_port=443`) остаётся opt-in для достижимого домена с single-round x25519. Self-signed сертификат — задокументированный остаточный признак при активном зондировании.
### zapret отслеживает свежий релиз обхода DPI ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Установка теперь тянет последний тег релиза zapret/nfqws вместо замороженного коммита: инструмент обхода обязан поспевать за эволюцией DPI.
- Есть проверенный офлайн-fallback (`v72.12`) и проверка согласованности clone-commit (отказ собирать root-side при расхождении).
- Это сознательно НЕ подписанный/контрольно-суммируемый pin.
### Многоядерная модель воркеров через SO_REUSEPORT ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Добавлен опциональный `[server].workers` (по умолчанию 1; `0` = auto = по одному воркеру на CPU; значение зажато до 256).
- При `workers > 1` каждый воркер запускает собственный epoll-цикл на своём SO_REUSEPORT-листенере, а ядро балансирует соединения между CPU.
- Поведение по умолчанию (один воркер) не изменилось байт в байт.
- Общее состояние (replay cache, snapshot MiddleProxy, flood guard и subnet rate limiter) защищено настоящим межпоточным mutex, поэтому лимиты действуют ГЛОБАЛЬНО, а не на каждый воркер отдельно.
- `ConnectionPool` каждого воркера имеет размер `max_connections / workers`, так что суммарная память под слоты остаётся примерно постоянной.
- У каждого воркера свой элемент в heartbeat-массиве, поэтому `/healthz` и watchdog видят зависший shard.
- При `workers > 1` reload по SIGHUP запрещён: live-reload освободил бы общие строки конфига, которые другие воркеры читают на горячем пути, — чтобы применить изменения, нужен restart.
- Функция opt-in и пока требует проверки под реальной нагрузкой на Linux.
### Усиленный systemd-юнит и неактивный sd_notify ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Реализован нативный `sd_notify(3)` (`src/proxy/sd_notify.zig`, без внешних зависимостей): `READY=1` после bind и `WATCHDOG=1` на каждой итерации цикла.
- Но юнит поставляется с `Type=simple`, а не `Type=notify`: в контейнеризованном systemd (Docker/LXC) notify-датаграмма часто не доходит, и в паре с `Restart=always` это зацикливает рестарты здорового прокси. `Type=simple` надёжен везде и совпадает с production, поэтому код `sd_notify` остаётся неактивным до появления детекта контейнера.
- Юнит прокси (deploy, сгенерированный инсталлером, tunnel и записанный из dashboard) получает sandbox: `SystemCallFilter=@system-service`, `RestrictAddressFamilies`, `MemoryDenyWriteExecute`, `ProtectKernel*`/`ProtectClock`/`ProtectHostname`, `PrivateDevices`, `UMask=0077` и понижение прав до `User=mtproto`.
### Health-проверки и метрика причин закрытия соединений ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Добавлены `/healthz` и `/readyz` на metrics-сервере. `/healthz` отдаёт `503`, если event loop не тикал дольше 5 с (норма ~37 мс); `/readyz` отдаёт `503`, как только начинается graceful shutdown (drain для LB/k8s). `/readyz` намеренно не завязан на middleproxy.
- Свободные текстовые причины закрытия (68 штук) сведены в ограниченный набор `CloseReason` и экспортируются как `mtproto_connection_close_reason_total{reason}` на `/metrics`. Сигналы обхода блокировок размечены точно: `tls_validation_failed`, `sni_mismatch`, `replay_detected`, `handshake_budget`, `handshake_timeout`.
- Это измерение «errors» в модели RED: всплеск относительно baseline позволяет оператору вовремя **увидеть**, что цензор начал зондировать или блокировать прокси. Стоит один атомарный инкремент — нулевая цена на горячем пути.
### Безопасные деплои и устойчивое разрешение имён ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Тулчейн Zig зафиксирован (`minimum_zig_version = 0.16.0` в `build.zig.zon` плюс файл `.zig-version`), так что компилятор, которым собирается 1.0, гарантирован.
- Добавлен `mtproto-proxy --check-config [path]`: валидирует конфиг и выходит с кодом `0`/`1` (в стиле `nginx -t`) для безопасных деплоев и CI.
- DNS-резолвинг получает fallback на `getent ahosts`, когда стандартный resolver выдаёт `ResolvConfParseFailed` на `resolv.conf` без завершающего перевода строки (SolusVM и ряд VPS-образов). Раньше прокси резолвил IP-литералы, но не хостнеймы, из-за чего тихо ломались real-domain fronting и upstream-proxy host. Найдено при live-отладке деплоя в tunnel-режиме.
### AES-CTR: обработка по 8 блоков на горячем пути ретрансляции ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `AesCtr.apply()` теперь шифрует выровненную по блокам часть данных пачками по 8 блоков через AES-NI (`xorWide`) вместо поблочной обработки.
- Для невыровненных начала и хвоста сохраняется перенос остатка ключевого потока (partial-block keystream), поэтому непрерывность счётчика CTR соблюдается между вызовами произвольной длины.
- Каждый ретранслируемый байт проходит через AES дважды, поэтому расширение основной части кратно увеличивает пропускную способность ретрансляции на ядро.
- Добавлен тест на эквивалентность со скалярной реализацией на границах пачек, включая переполнение счётчика (wraparound).
### Исправлен quick-ack confirm для abridged-транспорта ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `RPC_SIMPLE_ACK` передаёт клиенту 4-байтовый quick-ack confirm; эталонная реализация для abridged-транспорта отправляет его в обратном порядке байтов, а для intermediate/secure — как есть.
- Раньше мы для всех транспортов отправляли confirm как есть, из-за чего ack для abridged-клиентов был повреждён.
- Логика вынесена в `relaySimpleAck()` и покрыта golden-vector тестом.
### Продукт, а не серверный лог ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `PRODUCT_VISION.md`: продуктовый аудит всего пути пользователя — диагноз (технически безупречный инструмент, который «разговаривал» как серверный лог: сделан для инженеров, но нужен людям, защищающим своих близких) и поэтапная дорожная карта.
- `README.md` и `README.ru.md` переписаны от человеческого результата («Держите близких на связи»): добавлены разделы «Для кого это» и «Чем это лучше VPN?», ранний знак доверия, успокаивающая строка и готовое к пересылке сообщение; таблица бенчмарков понижена до доказательства, а не главного аргумента.
- Финальный экран установки — это победа человека, а не лог деплоя: «Готово. Ваш прокси работает — и его не видно.»
### От установки до ссылки за секунды ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Рабочая ссылка печатается в тот момент, когда прокси поднялся: раньше она появлялась только после необязательной маскировки и сборки zapret из исходников (минуты) — теперь `tg://` и `t.me` ссылки выводятся сразу под «Your proxy is LIVE», сокращая время до первой ссылки с минут до секунд.
- После запуска установка печатает QR прямо в терминале (полублоки UTF8) для `t.me`-ссылки первого пользователя («Scan to connect a device:») — наведите камеру телефона на окно SSH, без копирования; по возможности (нужен qrencode, ставится best-effort).
- При установке `mtbuddy` сначала показывает пересылаемую `t.me`-ссылку, ниже — `tg://` (без жаргона «fakeTLS:»), и добавляет сообщение «поделитесь с тем, кого любите».
- Страшный экран из шести галочек DPI свёрнут в один дружелюбный вопрос («Turn on recommended protection against blocking?») с безопасными настройками по умолчанию; шесть экспертных галочек остаются только под «Advanced».
### Понятный статус — от CLI до дашборда ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `mtbuddy status` начинается с простого вердикта («You're online…» / «Your proxy is offline…») вместо деталей сервиса.
- Баннер CLI несёт слоган («a door they can't close.»), приветствие в интерактивном режиме и строку миссии на баннере запуска прокси; ошибки заканчиваются следующим шагом; русские строки де-англизированы.
- Дашборд читается как успокоение, а не как кокпит: во всю ширину — статус-герой с одним понятным ответом: офлайн («friends can't connect until it's back»), онлайн и занят («Everything's working. N people connected right now»), онлайн и простаивает («share a link to get started»). Бейдж статуса словами («Online · <uptime>»).
- Разовый праздничный тост при переходе активных подключений 0→1 («Someone just connected through your proxy»). Слова о сбоях очеловечены: «Degraded» → «Needs attention», «(DOWN)» → «(not responding)».
- Дашборд честно различает онлайн / зависший («running but not responding — restarting usually fixes this») / офлайн — это помечено как локальная проверка прослушивания, а не заявление «не заблокирован».
### Поделиться, назвать и переключить язык в дашборде ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Дашборд получает localhost-only эндпоинт `/api/qr` (qrencode, только через argv, кодирует только наши собственные `t.me`/`tg` ссылки) и кнопку 📲 Share у каждого пользователя, открывающую модалку (большой QR, «Copy link», нативный лист «Send»); создание пользователя завершается именно в этом моменте — модалка открывается сама.
- Форма добавления пользователя очищена от жаргона: «Who is this for?» (мама, папа, work).
- Имена пользователей — на вашем языке («Мама», «work 💼»): поле принимает любой язык, но прокси всегда видит только ASCII-ключ `[access.users]` (слаг или сгенерированный `user_<id>`, если все символы не-ASCII); отображаемое имя живёт в управляемом дашбордом файле-компаньоне `user-labels.json`, который прокси никогда не читает.
- Двуязычный дашборд (EN/RU): JS-слой i18n (полная таблица EN/RU, поиск через `t()`, переключатель EN|RU в шапке с сохранением выбора, язык браузера по умолчанию → русский из коробки); переводится весь текст, подписи, кнопки, статус-герой, модалка и тосты, имена собственные остаются, покрытие ключ-в-ключ.
### Горячее применение списка пользователей по SIGHUP ([#302](https://github.com/sleep3r/mtproto.zig/pull/302))
- Раньше `SIGHUP` перечитывал runtime-конфигурацию, но не список пользователей доступа: чтобы добавить или удалить пользователя, внешним системам управления приходилось полностью перезапускать сервис — это замедляло автоматическую выдачу доступов и рвало активные соединения.
- Теперь по `SIGHUP` добавление, удаление и смена секрета пользователя применяются к работающему прокси без перезапуска.
- Метрики стали стабильными указателями на каждого пользователя. Метрики удалённого пользователя не освобождаются сразу, а «выводятся из обращения» (retired) и живут, пока не завершатся его текущие соединения, после чего память переиспользуется — без use-after-free.
- Чтение метрик по каждому пользователю синхронизировано read/write-блокировкой с потоком сбора метрик.
- Покрыто тестами: retire-then-collect и проверка того, что reload дожидается читателей метрик. Реализовано внешним контрибьютором.
### Состязательный самоаудит релиза 1.0 ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Перед релизом PR 1.0 прошёл многоагентный состязательный самоаудит: 25 находок (0 ложных срабатываний), все исправлены.
- Дашборд при удалении туннеля переписывал живой unit на `Type=notify`, из-за чего контейнер уходил в цикл перезапуска. Везде возвращён `Type=simple`.
- Новая проверка здоровья `_proxy_listening` на каждом опросе открывала реальное loopback-соединение, которое прокси засчитывал как `accept()` (раздувало Total Connections примерно на 28.8k в сутки), и ложно сообщала «stalled», когда `bind_address` указывал не на loopback. Теперь проверка читает `/proc/net/tcp{,6}` и ищет сокет в состоянии LISTEN — без учитываемого подключения и с учётом адреса привязки.
- Ранний QR-код «proxy is LIVE» игнорировал заданный `[server].public_ip` и кодировал «сырой» автоопределённый IP. Теперь публичный адрес вычисляется один раз и переиспользуется.
- `clienthello_fp_budget` имел TOCTOU-underflow при нескольких воркерах — заменён на насыщающий cmpxchg. `CloseReason.classify` неверно классифицировал закрытия со стороны upstream для dd-relay — исправлено.
- Доведена полнота i18n дашборда и точность описания маскировки в README; предупреждение о неизменности `tls_domain` добавлено в оба README; `build.zig.zon` добавлен в отслеживание версий release-please.
## Проверено
- `zig build test` и кросс-компиляция под `x86_64-linux` + `aarch64-linux` — зелёные.
- GitHub Actions: `Test & Build`, `Bench (No Soak)`, `E2E Integration`, installer e2e `debian:12` и `ubuntu:24.04`.
- Развёрнут в production (`proxy.sleep3r.ru`) и проверен под реальной нагрузкой: 0 рестартов, ~24 МБ RSS, без паник/SIGSYS, MiddleProxy-метаданные подтянулись, телеметрия ClientHello отработала и затихла.
</details>
---
<details>
<summary>🇬🇧 Release notes (EN)</summary>
## What this release addresses
`v1.0.0` is a milestone release. It turns a fast but "engineer-shaped" proxy into a circumvention tool that is **secure by default**, fully visible to its operator, and usable by an ordinary person for the people they love.
The work spanned four directions:
- **Data-plane security.** The proxy is the most internet-exposed process, parsing untrusted input. It now builds ReleaseSafe + PIE/ASLR, its parsers are covered by coverage-guided fuzzing, and the supply chain (`uv`, Python deps) is pinned and verified.
- **Evasion and observability.** The ServerHello no longer gives itself away with a constant cipher; a structural test catches fingerprint drift; the new `mtproto_connection_close_reason_total` metric provides the 'errors' dimension of the RED model — the operator can **see** a censor begin probing or blocking the node.
- **Scale and reliability.** An opt-in multi-core `[server].workers` model (SO_REUSEPORT), `/healthz`+`/readyz` endpoints, watchdog-ready `sd_notify`, a resilient DNS fallback, and a hardened systemd unit sandbox.
- **Product.** A full rework of the user journey: an instant link, QR, a plain-language status verdict, a bilingual dashboard, and user names in your own language — so setting the proxy up for your mom no longer means a wall of acronyms.
`1.0.0` is also a stability contract: `COMPATIBILITY.md` freezes, under SemVer, the link/secret format, config keys, metric names, health endpoints, the CLI, systemd units, and release verification.
> [!IMPORTANT]
> `tls_domain` is baked into the `ee` secret, so it is part of the `tg://` link. **Never change it on a live deployment: doing so invalidates every link you have already distributed.** Pick the domain once (default `rutube.ru` — single-round x25519, which our FakeTLS can mimic).
> [!NOTE]
> Multi-core (`[server].workers > 1`) is opt-in; the default single-worker behavior is unchanged byte for byte. Existing `config.toml` files need no edits: missing keys use safe defaults.
## What changed
### Hardened proxy binary: ReleaseSafe data plane + PIE/ASLR ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- The proxy parses untrusted network input (FakeTLS, obfuscation, MiddleProxy, SOCKS5, HTTP, TOML) on the most internet-exposed process, yet it previously shipped as ReleaseFast — with safety checks off.
- A new `-Ddataplane_safety` build option (default `true`) forces the proxy executable to ReleaseSafe: an off-by-one becomes a safe panic instead of exploitable undefined behavior.
- `mtbuddy` and bench keep their requested build mode.
- Measured: the ReleaseSafe proxy is not larger than ReleaseFast; the overhead is in the parse loops, not in the HW-AES relay path.
- The proxy is now built as PIE: the loader randomizes the base address (ASLR). ELF `e_type=DYN` on x86_64 and aarch64.
- Full RELRO is already default. Stack canaries are skipped (they need libc, and the binary is libc-free; ReleaseSafe provides the bounds checks).
### Supply chain: pinned and verified toolchain deps ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `uv` is pinned to `0.11.19` and verified against its published `.sha256`.
- Python deps are pinned to exact versions: `fastapi 0.115.6`, `starlette 0.41.3`, `uvicorn 0.34.0`, `psutil 6.1.1`, `websockets 14.1`.
### Fuzzing the wire-facing parsers ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Added coverage-guided fuzz targets for parsers that handle network data (via `std.testing.fuzz` / Smith in Zig 0.16).
- Covered: TLS ClientHello (`extractSni`/`extractFirstTls13Cipher`/`validateTlsHandshake`), the SOCKS5 response parsers, and the HTTP CONNECT response parser.
- Each target asserts no panic and no out-of-bounds: a parser panic = remote DoS of a circumvention node.
- Deterministic under `zig build test`, coverage-guided under `zig build test --fuzz`.
### Closed holes: credential exposure, insecure temp file, and DNS rebinding ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- In http-fetch, upstream proxy credentials are now passed via a curl `--config` file (mode `0600`) instead of replacing the child environment: the password no longer shows up on the world-readable cmdline, and curl keeps `SSL_CERT_FILE` and locale.
- `writeCurlProxyConfig` writes that `0600` config to an unpredictable random path with `O_EXCL` (refuses a pre-created file or symlink), closing a CWE-377/59 local insecure-temp-file window.
- The dashboard websocket `ws_logs` now also enforces the loopback Host-pin (DNS-rebinding defense), matching the HTTP middleware it previously bypassed.
### ServerHello echoes the client's cipher, locked by a structural test ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Previously the FakeTLS ServerHello always returned a constant cipher `0x1301`. The same cipher on every connection is a passive JA3S/ServerHello distinguisher that sets the proxy apart from a real server.
- The proxy now parses the cipher suite list from the ClientHello (GREASE-aware) and patches the ServerHello to the first non-GREASE TLS 1.3 suite the client offered (fallback `0x1301`).
- Telegram FakeTLS clients ignore the cipher itself, so this is pure evasion upside (clients don't validate the cipher).
- A hermetic local test was added: it drives a realistic ClientHello and asserts the ServerHello fingerprint — the cipher tracks the ClientHello, there is no GREASE in structural fields, the `key_share` group and record geometry are checked (differ where a real server differs; constant where it is constant). Fingerprint drift now fails the test instead of shipping unnoticed.
- The TCPMSS clamp value is now configurable: `--tcpmss <n>` (default still 88).
### ClientHello fingerprint logging on deploy ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- On startup the proxy logs the first ~16 incoming ClientHello fingerprints at info, then goes quiet (`formatClientHelloFingerprint`: non-GREASE ciphers, `supported_groups`, `key_share` groups).
- This lets an operator connect once from a real censored client and see exactly what it offers — for example, whether `X25519MLKEM768` is in the set.
- The logging is read-only, bounded, panic-free, and fuzzed.
### Fronting domain: immutable, with a default that wins ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `tls_domain` is part of the `ee` secret, so the `tg://` link is a function of (secret, `tls_domain`). It must NEVER be changed on a live deployment: doing so invalidates every link already distributed. Documented at the config field, `COMPATIBILITY.md`, and `ARCHITECTURE.md`.
- Our three-record FakeTLS (single x25519 `key_share`, no HelloRetryRequest) can only mimic a domain whose genuine TLS 1.3 completes in a single round on x25519: `rutube.ru`, `ozon.ru`, `vk.com`, `yandex.ru`, `dzen.ru`. Domains like `wb.ru`/`mail.ru` prefer secp521r1 + HRR and cannot match.
- `warnIfPoorFrontingDomain` softly probes the chosen domain (via openssl) and warns BEFORE the link is generated — while the choice is still free to change.
- The default fronting domain changed from `wb.ru` to `rutube.ru`. `wb.ru` is exactly the domain `warnIfPoorFrontingDomain` warns against (HRR, rejects x25519); since `tls_domain` is immutable, a default accepted blindly should win rather than lose permanently. `rutube.ru` is a verified major RU site with single-round x25519, reachable and unblocked in RU. Updated the installer/interactive/masking defaults, CLI help, README, and bootstrap.
- The masking default is a robust local self-signed Nginx (after reverting a fragile real-domain-fronting default): it always completes a handshake, needs no DNS/network, and works for any domain. Real-domain fronting (`mask_port=443`) stays an opt-in for a reachable single-round-x25519 domain. The self-signed cert is a documented active-probe residual.
### zapret tracks the latest DPI-bypass release ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Installation now pulls the latest zapret/nfqws release tag instead of a frozen commit: a circumvention tool must keep up with DPI evolution.
- There is a known-good offline fallback (`v72.12`) and a clone-commit consistency check (refuses to build root-side on mismatch).
- This is deliberately NOT a signed/checksummed pin.
### Multi-core worker model via SO_REUSEPORT ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Adds an opt-in `[server].workers` (default 1; `0` = auto = one worker per CPU; clamped to 256).
- With `workers > 1`, each worker runs its own epoll loop on its own SO_REUSEPORT listener, and the kernel load-balances connections across CPUs.
- Default single-worker behavior is unchanged byte for byte.
- Shared state (replay cache, MiddleProxy snapshot, flood guard, and subnet rate limiter) is guarded by a real cross-thread mutex, so limits hold GLOBALLY rather than per worker.
- Each worker's `ConnectionPool` is sized `max_connections / workers`, so total slot memory stays roughly constant.
- Each worker has its own entry in the heartbeat array, so `/healthz` and the watchdog can spot a wedged shard.
- With `workers > 1`, SIGHUP reload is refused: a live reload would free shared config strings that other workers read on the hot path — restart to apply changes.
- Opt-in, and still needs validation under real load on Linux.
### Hardened systemd unit and dormant sd_notify ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Native `sd_notify(3)` implemented (`src/proxy/sd_notify.zig`, zero dependencies): `READY=1` after bind and `WATCHDOG=1` on each loop iteration.
- But the unit ships with `Type=simple`, not `Type=notify`: under containerized systemd (Docker/LXC) the notify datagram often isn't delivered, which together with `Restart=always` restart-loops a healthy proxy. `Type=simple` is robust everywhere and matches production, so the `sd_notify` code stays dormant pending container detection.
- The proxy unit (deploy, installer-generated, tunnel, and dashboard-written) gains a sandbox: `SystemCallFilter=@system-service`, `RestrictAddressFamilies`, `MemoryDenyWriteExecute`, `ProtectKernel*`/`ProtectClock`/`ProtectHostname`, `PrivateDevices`, `UMask=0077`, and dropping privileges to `User=mtproto`.
### Health checks and a connection-close-reason metric ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Adds `/healthz` and `/readyz` on the metrics server. `/healthz` returns `503` if the event loop hasn't ticked within 5s (normally ~37ms); `/readyz` returns `503` once graceful shutdown begins (LB/k8s drain). `/readyz` is deliberately not gated on middleproxy.
- The 68 free-text close reasons are classified into a bounded `CloseReason` set and exported as `mtproto_connection_close_reason_total{reason}` on `/metrics`. Evasion signals are matched precisely: `tls_validation_failed`, `sni_mismatch`, `replay_detected`, `handshake_budget`, `handshake_timeout`.
- This is the 'errors' dimension of the RED model: a spike versus baseline lets an operator **see** a censor begin probing or blocking the proxy in time. It costs one atomic increment — zero hot-path cost.
### Safe deploys and resilient name resolution ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- The Zig toolchain is pinned (`minimum_zig_version = 0.16.0` in `build.zig.zon` plus a `.zig-version` file), so the compiler that builds 1.0 is guaranteed.
- Adds `mtproto-proxy --check-config [path]`: it validates the config and exits `0`/`1` (`nginx -t` style) for safe deploys and CI.
- DNS resolution gains a fallback to `getent ahosts` when the standard resolver throws `ResolvConfParseFailed` on a `resolv.conf` without a trailing newline (SolusVM and several VPS images). Previously the proxy resolved IP literals but not hostnames, silently breaking real-domain fronting and an upstream-proxy host. Found while live-debugging a tunnel-mode deployment.
### AES-CTR: 8 blocks at a time on the relay hot path ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `AesCtr.apply()` now encrypts the block-aligned portion in batches of 8 blocks via AES-NI (`xorWide`) instead of one block at a time.
- For the unaligned head and tail it keeps the partial-block keystream carry, so CTR counter continuity holds across calls of arbitrary length.
- Every relayed byte passes through AES twice, so widening the bulk multiplies per-core relay throughput.
- Added an equivalence test against the scalar implementation across batch boundaries, including counter wraparound.
### Fixed the quick-ack confirm for the abridged transport ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `RPC_SIMPLE_ACK` relays a 4-byte quick-ack confirm to the client; the reference sends it byte-reversed for the abridged transport and verbatim for intermediate/secure.
- Previously we sent the confirm verbatim for all transports, corrupting the ack for abridged clients.
- The logic was extracted into `relaySimpleAck()` and covered by a golden-vector test.
### A product, not a server log ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `PRODUCT_VISION.md`: a product audit across the whole user journey — the diagnosis (a technically flawless tool that "spoke" like a server log: built for engineers, but used by people protecting the family they love) and a phased roadmap.
- `README.md` and `README.ru.md` now lead with the human outcome ("Keep the people you love connected"): added "Who it's for" and "Why not just a VPN?" sections, an early trust anchor, a reassurance line, and a forwardable share message; the benchmark table is demoted to a proof point rather than the headline.
- The install success header is a human win, not a deploy log: "Done. Your proxy is live — and invisible."
### From install to a shareable link in seconds ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- The working link prints the instant the proxy is live: it used to appear only after the optional masking and from-source zapret compile (minutes) — now the `tg://` and `t.me` links print immediately under "Your proxy is LIVE", cutting time-to-first-link from minutes to seconds.
- After startup the install prints a terminal QR (UTF8 half-blocks) of the first user's `t.me` link ("Scan to connect a device:") — point a phone camera at the SSH window, no copy-paste; best-effort (needs qrencode, installed best-effort).
- On install, `mtbuddy` shows the forwardable `t.me` link first and the `tg://` link below (no "fakeTLS:" jargon), plus a "share with someone you love" message.
- The scary six-checkbox DPI screen is collapsed into one friendly question ("Turn on recommended protection against blocking?") with safe defaults applied; the six expert checkboxes only appear under "Advanced".
### Plain-language status, from CLI to dashboard ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- `mtbuddy status` leads with a plain verdict ("You're online…" / "Your proxy is offline…") instead of service internals.
- The CLI banner carries a tagline ("a door they can't close."), a buddy greeting in interactive mode, and a mission line on the proxy startup banner; errors end with a next step; RU strings are de-anglicized.
- The dashboard reads like reassurance, not a cockpit: a full-width status hero gives one plain answer — offline ("friends can't connect until it's back"), online and busy ("Everything's working. N people connected right now"), online and idle ("share a link to get started"). Status badge in words ("Online · <uptime>").
- A one-time celebratory toast when active connections go 0→1 ("Someone just connected through your proxy"). Cold-failure words humanized: "Degraded" → "Needs attention", "(DOWN)" → "(not responding)".
- The dashboard honestly distinguishes online / stalled ("running but not responding — restarting usually fixes this") / offline — labeled a local listening check, not a "not blocked" claim.
### Share, name, and switch language in the dashboard ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- The dashboard gains a localhost-only `/api/qr` endpoint (qrencode, argv-only, encodes only our own `t.me`/`tg` links) and a per-user 📲 Share button that opens a modal (big QR, "Copy link", native "Send" sheet); creating a user ends in that share moment — the modal auto-opens.
- The Add-User form is de-jargoned: "Who is this for?" (mom, dad, work).
- Name users in your own language ("Мама", "work 💼"): the field accepts any language, but the proxy only ever sees an ASCII `[access.users]` key (a slug, or a generated `user_<id>` when all characters are non-ASCII); the display name lives in a dashboard-managed `user-labels.json` sidecar the proxy never reads.
- Bilingual dashboard (EN/RU): a JS i18n layer (full EN/RU table, `t()` lookup, a persisting EN|RU header toggle, browser-language default → Russian out of the box); all prose, labels, buttons, status hero, modal, and toasts translate, proper nouns stay, key-for-key complete.
### Hot-reload of access users via SIGHUP ([#302](https://github.com/sleep3r/mtproto.zig/pull/302))
- Previously `SIGHUP` reloaded the runtime config but not the access-user list: to add or remove a user, external management systems had to fully restart the service — this slowed automated provisioning and dropped active connections.
- Now a `SIGHUP` reload applies user add/remove/secret-change to the running proxy with no restart.
- Metrics became stable per-user pointers. A removed user's metrics are not freed immediately but are "retired" and kept alive until its in-flight connections drain, after which the memory is reclaimed — no use-after-free.
- The per-user metrics read path is synchronized with the metrics-collection thread via a read/write lock.
- Covered by tests: retire-then-collect, and a check that the reload waits for metrics readers. Contributed by an external contributor.
### 1.0 adversarial self-review ([#301](https://github.com/sleep3r/mtproto.zig/pull/301))
- Before release, the 1.0 PR went through a multi-agent adversarial self-review: 25 findings (0 false positives), all fixed.
- On tunnel deletion the dashboard rewrote the live unit to `Type=notify`, which sent the container into a restart loop. Reverted to `Type=simple` everywhere.
- The new `_proxy_listening` health check opened a real loopback connection on every poll, which the proxy counted as `accept()` (inflated Total Connections by roughly 28.8k/day), and it mis-reported "stalled" whenever `bind_address` was not on loopback. The check now reads `/proc/net/tcp{,6}` for a LISTEN socket — no counted connection, and bind-address aware.
- The early "proxy is LIVE" QR ignored a configured `[server].public_ip` and encoded the raw auto-detected IP. The public address is now resolved once and reused.
- `clienthello_fp_budget` had a multi-worker TOCTOU underflow — replaced with a saturating cmpxchg. `CloseReason.classify` mis-bucketed dd-relay upstream closes — fixed.
- Improved dashboard i18n completeness and README masking accuracy; the immutable-`tls_domain` warning was added to both READMEs; `build.zig.zon` was added to release-please version tracking.
## Verified
- `zig build test` and cross-compilation for `x86_64-linux` + `aarch64-linux` — green.
- GitHub Actions: `Test & Build`, `Bench (No Soak)`, `E2E Integration`, installer e2e `debian:12` and `ubuntu:24.04`.
- Deployed to production (`proxy.sleep3r.ru`) and verified under real load: 0 restarts, ~24 MB RSS, no panics/SIGSYS, MiddleProxy metadata refreshed, ClientHello telemetry ran and went quiet.
</details>
## Changelog
* mtproto.zig 1.0.0 — secure-by-default & audit-hardening milestone ([#301](https://github.com/sleep3r/mtproto.zig/pull/301)) ([a187e70](https://github.com/sleep3r/mtproto.zig/commit/a187e70))
* feat: hot-reload of access users via SIGHUP ([#302](https://github.com/sleep3r/mtproto.zig/pull/302)) ([3800a9b](https://github.com/sleep3r/mtproto.zig/commit/3800a9bf0d4527a4a974c84dea845f368860aea2))
Related Repositories
lightpanda-io/browser★ 36,854
Lightpanda: the headless browser designed for AI and automation
tigerbeetle/tigerbeetle★ 17,229
The financial transactions database designed for mission critical safety and performance.
nullclaw/nullclaw★ 8,131
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)