PunchSeven die as two trains collide in EgyptDaily MaverickSUMMER SECURITY: Safer, but not safe enough — Nelson Mandela Bay’s beachfront vandalism endangers festive tourismInquirerVP’s lawyers didn’t dispute authenticity of financial records – prosecutionThe Jerusalem PostIraq says it lost $60b from Iran war, reaches understandings with US on Hormuz oil routeCBC NewsCross Country Checkup: What are you giving up to make ends meet?ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe Sydney Morning HeraldNvidia boss Jensen Huang admits infidelity, board probes affair with alleged Chinese spyVanguardDebate: ‘Lagos has waste-pick-and-dump system, not waste management’ – GRVNOS SportNEC beleeft prima week: ook winst in Utrecht mede door twee goals TadicBillboardTaylor Swift and Karlie Kloss Attend Travis Kelce’s Chiefs Game in Kansas CityANSA SportSerie A, in campo Fiorentina-Juventus DIRETTADigital SpyGillian Anderson lands next lead movie role in "suspenseful" psychological thriller
The Daily Newsstand · Free, Always
Sunday, November 8, 2026

Как мы учили Rust притворяться Chrome с постквантовым TLS, патчили и допиливали Unbihex после 104K просмотров на Хабре

Translate

Привет, Хабр! Когда первая статья про мой легковесный TUN-клиент Unbihex внезапно собрала 104 тысячи просмотров, проект был на версии 0.1.0. Тогда это был дерзкий прототип, который ел 18 МБ оперативки, умел базовый джентльменский набор (VLESS, Vision, REALITY, XHTTP) и решал мою главную боль - избавлял от жирных обёрток со сборщиками мусора.

После публикации начался настоящий продакшен: в репозиторий пришли живые пользователи со своими провайдерами, панелями, роутерами, нестандартными конфигами и пулл-реквестами. Быстро выяснилось, что написать быстрый клиент «для себя» и сделать универсальное прокси-ядро, которое не падает на битых пакетах, переживает белые списки мобильных операторов и байт-в-байт дружит с капризами Xray 26 и sing-box - это две совершенно разные задачи.

К версиям ядра unbihex-engine 0.1.9 (Rust edition 2024) и клиента 0.2.10 проект вырос в полноценную экосистему. Мы внедрили жёсткую гигиену кода (unsafe_code = "deny" и clippy::unwrap_used = "deny", чтобы ни один кривой байт из сети не вызвал панику), добавили десяток протоколов, собрали TUI в духе btop и даже написали свой серверный REALITY-инбаунд. Но самое интересное началось, когда готовые крейты с crates.io перестали справляться и их пришлось вендорить и патчить руками.

Под катом - честный разбор сетевых граблей, неочевидных особенностей TCP в Linux и изнанки современных протоколов обхода блокировок.

1. Почему готовые крейты не вывозят: три патча в vendor/

В экосистеме Rust отличные сетевые библиотеки, но они написаны по RFC для идеального интернета, а не для партизанских туннелей поверх урезанных MTU и DPI. Чтобы всё заработало как надо, нам пришлось затащить в vendor/ и пропатчить три ключевых крейта.

Патч № 1: vendor/ipstack и парадоксы скорости на Linux

Для разбора пакетов из TUN-устройства в userspace мы используем ipstack 1.0.1. В ванильном виде под серьёзной нагрузкой или при раздаче трафика с роутера/контейнера стек начинал задыхаться. В итоге поверх него легли пять наших патчей:

  • Масштабирование окна (RFC 7323): мы подняли окно чтения до 1 МиБ и окно отправки до 256 КиБ, если SYN от приложения это поддерживает.

  • Уважение чужого MSS: если через наш TUN идёт трафик от контейнера или ВМ с сетью на 1500 байт, мы режем сегменты строго по MSS из их SYN, иначе пакеты на 16 КиБ с флагом DF просто умирают по дороге.

  • Ретрансмиты по таймеру и backpressure: ванильный стек переотправлял потери только при следующем пакете от приложения и дропал пакеты при полной очереди; мы прикрутили честный таймер, лимит в 1 МиБ непрочитанных данных и ожидание очереди.

  • Склейка очередей без нарезки: непрерывные данные больше 8 КиБ теперь отдаются читателю сразу, а закрытая повтором «дыра» моментально освобождает всю очередь за собой.

Параллельно при профилировании TUN на Linux всплыли две крутые детали про ядро ОС. Во-первых, userspace-стек тратит ресурсы на каждый отдельный пакет: стоило поднять дефолтный MTU туннеля на Linux с 1500 до 16 384 байт, как пропускная способность выросла примерно в 4,5 раза. Во-вторых, мы наступили на классические грабли с ручным тюнингом SO_RCVBUF и SO_SNDBUF. Оказалось, что на Linux явное задание размера буфера сокета отключает ядерную автонастройку TCP. На маршруте с пингом 50 мс и MTU 1500 фиксация буфера в 4 МиБ давала 55 МБ/с с периодическими провалами до 1 МБ/с, а дефолтный socket_buffer = 0 (отдать на откуп ядру ОС) стабильно выдал 64 МБ/с без единой просадки.

На том же принципе мы ускорили и внутренний TCP relay в диспетчере: каждое направление стартует со скромным буфером в 16 КБ, чтобы тысячи молчащих соединений не жрали память. Но как только чтение заполняет буфер целиком (пошёл активный поток данных), он автоматически расширяется до 64 КБ, почти вдвое поднимая скорость прокачки.

Патч № 2: vendor/rustls, постквантовый ML-KEM и отсутствие close_notify

Стандартный ClientHello от rustls вычисляется ТСПУ по JA3/JA4 на раз-два. Чтобы мимикрировать под браузеры без привязки к сишному BoringSSL или Go-шному uTLS, мы пропатчили rustls 0.23.46, добавив хук перехвата закодированного ClientHello прямо перед его хешированием и отправкой в сокет.

Ядро на лету пересобирает приветствие под выбранный профиль (chrome, firefox, safari, ios, android, edge, 360, qq, random, randomized), сохраняя привязанные к состоянию rustls поля (random, session_id с токеном REALITY, SNI, ALPN, key_share). Причем вся случайность перемешивания расширений детерминированно выводится из random самого ClientHello - благодаря этому при повторном приветствии (HelloRetryRequest) или двойном проходе REALITY получаются те же самые байты.

Тут же вылезли три тонкости, о которых не пишут в мануалах:

  • Постквантовый обмен ключами (X25519MLKEM768): современные браузеры и свежий Xray 26 в режиме REALITY требуют гибридный постквантовый key_share (0x11ec), поэтому мы собрали его на крейтах ml-kem (ключ 1184 байта) и x25519-dalek (32 байта).

  • Отказ от GREASE ECH: настоящий Chrome шлёт расширение GREASE ECH, но если отправить его из rustls, то серверы Cloudflare отвечают настоящим ECH-конфигом, который rustls не понимает и рвёт соединение; как и в uTLS, мы его убрали.

  • Закрытие без close_notify: серверы на Go (crypto/tls в Xray и sing-box) часто закрывают TCP-сокет туннеля без отправки TLS close_notify, что строгий rustls считает ошибкой и рвёт оба направления - мы научили обёртку считать это обычным EOF.

Патч № 3: vendor/h2 и «бомба» из 100 пустых кадров в TrustTunnel

Когда мы добавляли поддержку протокола TrustTunnel (работает поверх HTTP/2), выяснилась забавная особенность его официального эндпоинта. Каждый раз, когда окно передачи сервера к клиенту исчерпывается, он присылает пустой кадр DATA. А в популярном Rust-крейте h2 (0.4.20) зашита жёсткая защита от DoS: после 100 пустых DATA-кадров за всю жизнь соединения оно принудительно убивается. При активном скачивании лимит выбирался за пару минут. Пришлось пропатчить vendor/h2: теперь каждые 16 КиБ реально полученных данных возвращают одну единицу этого лимита (защита от 100 пустых кадров подряд при этом сохраняется).

2. Десяток протоколов, finalmask из Xray 26 и война с User-Agent панелей

В первой версии Unbihex понимал только стек вокруг VLESS. В 0.1.9 мы закрыли практически весь зоопарк протоколов, причём каждый проверяется в автотестах напрямую против живых бинарников xray, sing-box и официального сервера hysteria:

  • Hysteria 2 (QUIC / HTTP/3): работает поверх одного общего QUIC-соединения (quinn), поддерживает обфускацию Salamander (XOR каждого пакета с BLAKE2b-256(password || salt)) и фрагментацию UDP-датаграмм.

  • VMess (AEAD): реализован байт-в-байт по исходникам Xray - с вложенным HMAC KDF, маскированием длин чанков и паддингом через SHAKE128, а также цикличным переносом 16-битного счётчика чанков, чтобы закачки тяжелее 500 МБ не обрывались.

  • Trojan, Shadowsocks AEAD и SOCKS5: полный стек классики; для Trojan доступны TLS и REALITY поверх TCP, плюс WebSocket, HTTPUpgrade и gRPC.

  • NaiveProxy и TrustTunnel: оба работают поверх HTTP/2 с подменой SETTINGS и WINDOW_UPDATE под браузер. В NaiveProxy реализован фирменный паддинг из символов !#$()+<>?@[]^{}`, которые специально выбраны так, чтобы их не сжимал алгоритм Хаффмана в HPACK.

  • Транспорты WebSocket, HTTPUpgrade и gRPC (gun / multi): свой легковесный клиент RFC 6455 и двунаправленный потоковый gRPC с мультиплексированием поверх одного HTTP/2-соединения.

  • UDP поверх одного соединения Mux.Cool: за этот апгрейд отдельное спасибо контрибьютору pshakhmin (PR #53) - теперь UDP-потоки не платят три RTT на каждый новый адрес, а идут через одну сессию на локальный сокет с сохранением Full-Cone NAT.

Отдельный пласт работы - совместимость с новыми фичами Xray 26 и CDN-режимами XHTTP. В режиме packet-up ядро теперь умеет прятать отдаваемый трафик не только в тело POST-запроса, но и нарезать его в base64url прямо в HTTP-заголовки (X-Data-0, X-Data-1) или внутрь Cookie (x_data_0), а паддинг генерировать методом tokenish, где длина случайной base62-строки рассчитывается в байтах после сжатия Хаффмана. Заодно мы поддержали свежий формат fm (finalmask из Xray 26) для нарезки ClientHello на случайные фрагменты с задержками.

Но самая жизненная боль вскрылась при работе с подписками от панелей вроде Remnawave. Многие админы закрывают свои WebSocket/XHTTP-ноды за nginx, который отдаёт 101 Switching Protocols только если видит конкретный User-Agent, прописанный в панели, а всем остальным показывает сайт-заглушку с кодом 200. Проблема в том, что когда клиент запрашивает подписку и получает обычный base64-список ссылок vless://, внутри самих URL-ссылок кастомные заголовки headers просто не передаются.

Мы решили это элегантно: если панель прислала обычные ссылки и среди них есть ws, httpupgrade или xhttp, клиент один раз прозрачно перезапрашивает подписку с заголовком User-Agent: v2rayN/7.10.0 и теми же самыми заголовками x-hwid и данными устройства. Панель в ответ отдаёт полный JSON-конфиг Xray со всеми спрятанными headers, а благодаря совпадению x-hwid не списывает лишний слот устройства в лимите подписки.

Вторая головная боль - мобильные операторы с «белыми списками», когда интернет заблокирован настолько, что сам сервер подписки напрямую не открывается, но IP-адрес одной из рабочих VPN-нод ещё пробивается. Теперь, если прямой запрос подписки упал по ошибке, а VPN в этот момент подключён, Unbihex автоматически повторяет скачивание подписки внутри самого VPN-туннеля через текущий рабочий сервер.

3. «Домашний» и «Удалённый» DNS, перехват SNI и защита от падений

В классической схеме с одним DNS-сервером всегда приходится выбирать из двух зол: либо резолвить всё через прокси (и тогда российские CDN отправляют тебя на условный Франкфурт вместо московского узла), либо светить все домены провайдерскому резолверу.

Мы разделили DNS на два независимых контура (по аналогии с тем, как это сделано в Happ):

  • Удалённый DNS (dns.upstream): поддерживает UDP, TCP, DoT (tls://) и DoH (https://), используется для сайтов, идущих через VPN, причём запросы к нему уходят строго внутри прокси-туннеля.

  • Домашний DNS (dns.direct): например, 77.88.8.8 - опрашивается напрямую в обход диспетчера только для тех доменов, которые по правилам маршрутизации ведут в direct; имена проксируемых сайтов туда никогда не попадают.

Внутри TUN работает наш собственный DNS-сервер с двунаправленным LRU-пулом Fake-IP (198.18.0.0/15 на 65 536 записей). Чтобы после протухания записи приложение со старым кешем не улетело на чужой сайт, пара domain ↔ ip не переиспользуется, пока адрес молчал меньше max(300 с, 4 × ttl). Если вы включили встроенный блокировщик рекламы (поддерживаются списки AdAway, StevenBlack, Peter Lowe и свои ссылки), DNS-сервер туннеля моментально отбивает рекламные домены ответом NXDOMAIN, не тратя время на установку соединений.

А что если браузер включил свой собственный DoH или на компьютере стоит десктопный AdGuard/антивирус, и трафик пришёл в TUN уже не на Fake-IP, а сразу на реальный IP-адрес? Для этого в ядре по умолчанию работает sniff = true: на первых 16 КБ TCP-соединения (с таймаутом 300 мс, пропуская порты вроде SMTP/SSH, где сервер говорит первым) мы вытаскиваем домен из TLS SNI (даже если ClientHello разбит на несколько записей) или заголовка Host. Доменные правила маршрутизации отрабатывают как положено, а для direct-соединений дополнительно включена жесткая защита от DNS rebinding (отсекаются приватные IP в ответах публичных доменов).

Отдельно мы позаботились о том, чтобы Windows не сливала DNS мимо туннеля и не оставляла пользователя «без интернета» после внезапного выключения света:

  • Правила NRPT в Windows: вместо ненадёжной смены DNS на адаптере клиент прописывает в реестре политику NRPT, которая отключает Smart Multi-Homed Name Resolution и заворачивает все запросы строго в туннель.

  • Исключения для NCSI: домены msftconnecttest.com и msftncsi.com по умолчанию исключены из Fake-IP, чтобы значок сети в трее Windows не врал «Без доступа к интернету».

  • Журнал транзакций (routes-journal.json и proxy-journal.json): перед тем как тронуть таблицу маршрутизации, реестр или системный прокси, клиент записывает шаг на диск и при следующем старте гарантированно откатит изменения, если процесс был убит через диспетчер задач.

4. Балансировщик без флаппинга, TUI в стиле btop и свой сервер REALITY

Когда в подписке 20 серверов, переключать их руками каждый раз, когда у хостера лагает канал - сомнительное удовольствие. Мы добавили в ядро полноценный Balancer (а в интерфейс клиента - вкладку «Автовыбор» с поддержкой пользовательских шаблонов серверов, которые не слетают при обновлении подписки).

Главный бич наивных балансировщиков с режимом least_ping - постоянные прыжки между нодами, когда проседает ваш собственный домашний Wi-Fi или мобильная сеть. В Unbihex эта проблема решена через порог задержки (latency_threshold_ms, по умолчанию 500 мс) и гистерезис:

  • Никаких дёрганий без повода: текущий сервер меняется только в том случае, если две проверки подряд показали пинг выше порога (или таймаут), а альтернативный живой сервер быстрее хотя бы на 20% от порога (и не меньше чем на 50 мс).

  • Устойчивость при общей деградации: если провайдер тормозит целиком и все серверы группы вышли за 500 мс, балансировщик остаётся на текущем сервере и не гоняет трафик по кругу, а уже открытые соединения при любом переключении продолжают жить на старой ноде.

По платформам и интерфейсу мы тоже сильно продвинулись (отдельное спасибо Finorion за PR #58 с доработкой UI настроек):

  • Консольный unbihex-cli и TUI для Linux: помимо статических сборок под x86_64, ARM64 и ARMv7 (отлично крутятся в Docker на NAS Synology/QNAP с флагом --lan), появилась сборка unbihex-cli-tui с полноэкранным терминальным интерфейсом в духе btop — с графиками скорости, пингом и переключением серверов по Enter.

  • Режим «чистого туннеля» для роутеров: команда unbihex-cli connect --mode tun --no-routes --mark 255 поднимает только интерфейс unbihex0 и вешает SO_MARK на собственные сокеты ядра, позволяя разруливать трафик домашней сети через ip rule на свой вкус.

  • Android и iOS: на Android появился выбор приложений для VPN («Все», «Только выбранные», «Все, кроме выбранных») и плитка в шторку быстрых настроек (она сама аккуратно срезает флаги и эмодзи в начале имени сервера, чтобы текст влезал на экран), а для iOS готов C-ABI крейт unbihex-ffi под NEPacketTunnelProvider.

Наконец, мы закрыли гештальт из роадмапа первой статьи - внутри крейта ядра за фичей --features server появился собственный серверный стек (test_server) с поддержкой входящих соединений VLESS, XTLS Vision и REALITY. Сейчас он используется для нагрузочного тестирования и сквозных замеров без внешних зависимостей, но его реализация reality_shape.rs заслуживает отдельного упоминания.

Чтобы цензор не отличил наш сервер от настоящего сайта из поля dest ни по размеру пакетов, ни по таймингам, при каждом входящем ClientHello сервер параллельно стучится на dest, перенимает у него выбранный шифр и группу key_share, а собственные серверные записи TLS 1.3 (EncryptedExtensions … Finished) укладывает с паддингом (RFC 8446 §5.4) точно под размеры записей dest. Более того, в конце ответа добавляется микропауза в четверть RTT до dest (10–15 мс) - ровно столько, сколько настоящий сервер тратит на подпись CertificateVerify. В итоге даже тайминг-анализ видит абсолютно идентичную картину.

Исходники ядра и клиента, подробная спецификация на русском и английском (SPEC.md), а также готовые сборки под Windows (инсталлятор и portable с вшитым WebView2 и статическим CRT), Linux (CLI и TUI) и Android (APK) лежат в репозитории на GitHub: https://github.com/crystalmasta/unbihex.

Заходите тестировать, открывайте ишью, присылайте пулл-реквесты - и пишите в комментариях, каких ещё протоколов или сетевых сценариев вам не хватает в повседневной работе! Хочешь, чтобы я дополнительно адаптировал какие-то блоки под формат релиза на GitHub или вынес бенчмарки с ipstack и socket_buffer в отдельный врез с командами для воспроизведения?

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.