v1.0.0
🇷🇺 Что нового (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 > 1reload по 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 e2edebian: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_totalmetric 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].workersmodel (SO_REUSEPORT),/healthz+/readyzendpoints, watchdog-readysd_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_domainis baked into theeesecret, so it is part of thetg://link. Never change it on a live deployment: doing so invalidates every link you have already distributed. Pick the domain once (defaultrutube.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. Existingconfig.tomlfiles 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_safetybuild option (defaulttrue) forces the proxy executable to ReleaseSafe: an off-by-one becomes a safe panic instead of exploitable undefined behavior. mtbuddyand 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=DYNon 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)
uvis pinned to0.11.19and 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 underzig 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
--configfile (mode0600) instead of replacing the child environment: the password no longer shows up on the world-readable cmdline, and curl keepsSSL_CERT_FILEand locale. writeCurlProxyConfigwrites that0600config to an unpredictable random path withO_EXCL(refuses a pre-created file or symlink), closing a CWE-377/59 local insecure-temp-file window.- The dashboard websocket
ws_logsnow 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_sharegroup 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_sharegroups). - This lets an operator connect once from a real censored client and see exactly what it offers — for example, whether
X25519MLKEM768is in the set. - The logging is read-only, bounded, panic-free, and fuzzed.
Fronting domain: immutable, with a default that wins (#301)
tls_domainis part of theeesecret, so thetg://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, andARCHITECTURE.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 likewb.ru/mail.ruprefer secp521r1 + HRR and cannot match. warnIfPoorFrontingDomainsoftly 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.rutorutube.ru.wb.ruis exactly the domainwarnIfPoorFrontingDomainwarns against (HRR, rejects x25519); sincetls_domainis immutable, a default accepted blindly should win rather than lose permanently.rutube.ruis 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
ConnectionPoolis sizedmax_connections / workers, so total slot memory stays roughly constant. - Each worker has its own entry in the heartbeat array, so
/healthzand 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=1after bind andWATCHDOG=1on each loop iteration. - But the unit ships with
Type=simple, notType=notify: under containerized systemd (Docker/LXC) the notify datagram often isn't delivered, which together withRestart=alwaysrestart-loops a healthy proxy.Type=simpleis robust everywhere and matches production, so thesd_notifycode 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 toUser=mtproto.
Health checks and a connection-close-reason metric (#301)
- Adds
/healthzand/readyzon the metrics server./healthzreturns503if the event loop hasn't ticked within 5s (normally ~37ms);/readyzreturns503once graceful shutdown begins (LB/k8s drain)./readyzis deliberately not gated on middleproxy. - The 68 free-text close reasons are classified into a bounded
CloseReasonset and exported asmtproto_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.0inbuild.zig.zonplus a.zig-versionfile), so the compiler that builds 1.0 is guaranteed. - Adds
mtproto-proxy --check-config [path]: it validates the config and exits0/1(nginx -tstyle) for safe deploys and CI. - DNS resolution gains a fallback to
getent ahostswhen the standard resolver throwsResolvConfParseFailedon aresolv.confwithout 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_ACKrelays 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.mdandREADME.ru.mdnow 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://andt.melinks 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.melink ("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,
mtbuddyshows the forwardablet.melink first and thetg://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 statusleads 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/qrendpoint (qrencode, argv-only, encodes only our ownt.me/tglinks) 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 generateduser_<id>when all characters are non-ASCII); the display name lives in a dashboard-manageduser-labels.jsonsidecar 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
SIGHUPreloaded 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
SIGHUPreload 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 toType=simpleeverywhere. - The new
_proxy_listeninghealth check opened a real loopback connection on every poll, which the proxy counted asaccept()(inflated Total Connections by roughly 28.8k/day), and it mis-reported "stalled" wheneverbind_addresswas 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_ipand encoded the raw auto-detected IP. The public address is now resolved once and reused. clienthello_fp_budgethad a multi-worker TOCTOU underflow — replaced with a saturating cmpxchg.CloseReason.classifymis-bucketed dd-relay upstream closes — fixed.- Improved dashboard i18n completeness and README masking accuracy; the immutable-
tls_domainwarning was added to both READMEs;build.zig.zonwas added to release-please version tracking.
Verified
zig build testand cross-compilation forx86_64-linux+aarch64-linux— green.- GitHub Actions:
Test & Build,Bench (No Soak),E2E Integration, installer e2edebian:12andubuntu: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.