Skip to content

v1.0.0

Choose a tag to compare

@sleep3r sleep3r released this 06 Jun 23:07
997fe8d
🇷🇺 Что нового (RU)

Что решает этот релиз

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)

  • Прокси разбирает недоверенный сетевой ввод (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)

  • 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)

  • Добавлены 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)

  • В 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)

  • Раньше 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)

  • На старте прокси логирует на уровне info первые ~16 входящих отпечатков ClientHello, после чего замолкает (formatClientHelloFingerprint: не-GREASE ciphers, supported_groups, группы key_share).
  • Это позволяет оператору один раз подключиться настоящим клиентом из-под цензуры и увидеть, что именно он предлагает — например, есть ли в наборе X25519MLKEM768.
  • Логирование read-only, с ограничением по объёму, без паник, проверено фаззингом.

Fronting-домен: неизменяемый, с дефолтом, который выигрывает (#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)

  • Установка теперь тянет последний тег релиза zapret/nfqws вместо замороженного коммита: инструмент обхода обязан поспевать за эволюцией DPI.
  • Есть проверенный офлайн-fallback (v72.12) и проверка согласованности clone-commit (отказ собирать root-side при расхождении).
  • Это сознательно НЕ подписанный/контрольно-суммируемый pin.

Многоядерная модель воркеров через SO_REUSEPORT (#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)

  • Реализован нативный 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)

  • Добавлены /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)

  • Тулчейн 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)

  • AesCtr.apply() теперь шифрует выровненную по блокам часть данных пачками по 8 блоков через AES-NI (xorWide) вместо поблочной обработки.
  • Для невыровненных начала и хвоста сохраняется перенос остатка ключевого потока (partial-block keystream), поэтому непрерывность счётчика CTR соблюдается между вызовами произвольной длины.
  • Каждый ретранслируемый байт проходит через AES дважды, поэтому расширение основной части кратно увеличивает пропускную способность ретрансляции на ядро.
  • Добавлен тест на эквивалентность со скалярной реализацией на границах пачек, включая переполнение счётчика (wraparound).

Исправлен quick-ack confirm для abridged-транспорта (#301)

  • RPC_SIMPLE_ACK передаёт клиенту 4-байтовый quick-ack confirm; эталонная реализация для abridged-транспорта отправляет его в обратном порядке байтов, а для intermediate/secure — как есть.
  • Раньше мы для всех транспортов отправляли confirm как есть, из-за чего ack для abridged-клиентов был повреждён.
  • Логика вынесена в relaySimpleAck() и покрыта golden-vector тестом.

Продукт, а не серверный лог (#301)

  • PRODUCT_VISION.md: продуктовый аудит всего пути пользователя — диагноз (технически безупречный инструмент, который «разговаривал» как серверный лог: сделан для инженеров, но нужен людям, защищающим своих близких) и поэтапная дорожная карта.
  • README.md и README.ru.md переписаны от человеческого результата («Держите близких на связи»): добавлены разделы «Для кого это» и «Чем это лучше VPN?», ранний знак доверия, успокаивающая строка и готовое к пересылке сообщение; таблица бенчмарков понижена до доказательства, а не главного аргумента.
  • Финальный экран установки — это победа человека, а не лог деплоя: «Готово. Ваш прокси работает — и его не видно.»

От установки до ссылки за секунды (#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)

  • 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 · »).
  • Разовый праздничный тост при переходе активных подключений 0→1 («Someone just connected through your proxy»). Слова о сбоях очеловечены: «Degraded» → «Needs attention», «(DOWN)» → «(not responding)».
  • Дашборд честно различает онлайн / зависший («running but not responding — restarting usually fixes this») / офлайн — это помечено как локальная проверка прослушивания, а не заявление «не заблокирован».

Поделиться, назвать и переключить язык в дашборде (#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)

  • Раньше SIGHUP перечитывал runtime-конфигурацию, но не список пользователей доступа: чтобы добавить или удалить пользователя, внешним системам управления приходилось полностью перезапускать сервис — это замедляло автоматическую выдачу доступов и рвало активные соединения.
  • Теперь по SIGHUP добавление, удаление и смена секрета пользователя применяются к работающему прокси без перезапуска.
  • Метрики стали стабильными указателями на каждого пользователя. Метрики удалённого пользователя не освобождаются сразу, а «выводятся из обращения» (retired) и живут, пока не завершатся его текущие соединения, после чего память переиспользуется — без use-after-free.
  • Чтение метрик по каждому пользователю синхронизировано read/write-блокировкой с потоком сбора метрик.
  • Покрыто тестами: retire-then-collect и проверка того, что reload дожидается читателей метрик. Реализовано внешним контрибьютором.

Состязательный самоаудит релиза 1.0 (#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 отработала и затихла.

🇬🇧 Release notes (EN)

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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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)

  • 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 · ").
  • 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)

  • 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)

  • 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)

  • 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.

Changelog

  • mtproto.zig 1.0.0 — secure-by-default & audit-hardening milestone (#301) (a187e70)
  • feat: hot-reload of access users via SIGHUP (#302) (3800a9b)