Эту статью я готовил с прошлой недели, и пока готовил, ТСПУ выкатил новые правила фильтрации, целящиеся именно в Reality-handshake, о котором тут речь. То есть статья стала актуальнее, чем когда я её начинал.
В прошлой статье я рассказывал, как мы встроили VLESS + Reality прямо в наше iOS-приложение через sing-box, чтобы обход блокировок был не задачей пользователя, а деталью реализации. Если коротко: TLS-рукопожатие проксируется на посторонний крупный сайт, активный пробинг упирается в этот сайт, IP относимся как к расходнику, конфиг доставляется отдельно от сборки. Подход работает, и для подавляющего большинства соединений из России работает прямо сейчас.
Кроме одного класса сетей, в которых не работал.
Внутри этого класса оказались, в том числе, корпоративные подсети, гостевой Wi-Fi в некоторых аэропортах и часть регионального покрытия одного из операторов. Картина в логах одна и та же. Туннель поднимается, TCP-соединение на relay открывается, TLS-рукопожатие начинается, и через секунду sing-box на сервере пишет в журнал: REALITY: processed invalid connection. Сразу обрыв, нет ретраев которые что-то меняют.
Эта статья про то, что мы увидели в этих сетях, почему Reality в одиночку их не пробивает, и что мы поставили рядом, чтобы пробивал. Если читали предыдущую часть, продолжайте отсюда. Если не читали, важен один тезис: туннель у нас живёт внутри приложения, через sing-box, скомпилированный в нативный фреймворк, без системного VPN.
Что такое «белый список» в DPI
Привычная модель цензуры это чёрный список: оператор знает плохие хосты, режет их, остальное идёт. Обычные методы обхода (Reality, прокси с маскировкой) хорошо работают против чёрного списка, потому что выглядят как «остальное».
Белый список это инверсия. Оператор разрешает трафик к небольшому набору заранее одобренных доменов и IP, а всё, что не подходит под этот список, обрубается с разной степенью аккуратности. На уровне SNI это тривиально: ClientHello открытый, в нём видно куда вы идёте, и если этого домена нет в разрешённом наборе, соединение режется. На уровне IP то же самое: пакеты на адреса вне разрешённого подмножества просто не доходят. Это та же DPI-инфраструктура, что обычно, только настроенная по противоположному принципу.
Тут возникает интересный вопрос. Если белый список идёт по SNI, то Reality, который во время TLS-рукопожатия проксирует трафик на постороний крупный сайт (например, microsoft.com), формально должен пройти: цензор видит в SNI разрешённый домен. Но не проходит. И вот тут начинается интересное.
Что именно ловит белый список DPI
Reality прячет содержимое прокси-туннеля внутри валидного TLS-рукопожатия к разрешённому домену. ClientHello идёт настоящий, отпечатки TLS совпадают с настоящим Chrome (через uTLS), сертификат и цепочка валидные. Пассивный наблюдатель и активный пробер не отличат это от обычного браузера, ходящего на microsoft.com.
Но «не отличит» это про обычный DPI, который смотрит набор сигнатур и принимает решение «разрешать или нет». Белый список DPI ведёт себя иначе. Он не пытается опознать плохой трафик, он пытается убедиться в хорошем. И это другой алгоритм.
Тут оговорюсь сразу: я не ковырял исходники конкретного DPI-вендора, и формулировка ниже это моя реконструкция по поведению, а не подтверждённая истина. Параллельно я задавал вопросы знакомым из тель-авивского инженерного слоя (на стороне коммерческих DPI-вендоров там исторически плотно), и формулировка, которую от них слышал чаще всего, звучит примерно как «современные продукты не опознают плохое по одной сигнатуре, они собирают уверенность что трафик хороший из нескольких признаков сразу». Это согласуется с тем, что мы наблюдали, но это всё равно эвристический вывод снаружи.
Дальше я просто перечислю, что в этой картине, по моим наблюдениям, влияет. Если ваше «обращение к microsoft.com» отличается от настоящего по любому набору микро-параметров, эвристика поднимает руку. И таких параметров много: порядок TLS-расширений в ClientHello, паттерн GREASE-значений, тайминги ответов после рукопожатия, размеры записей. uTLS аккуратно копирует ClientHello Chrome версии N, но реальный Chrome ещё и ходит по сети как Chrome: подключение из обычного браузера тянет за собой TCP fast open, HTTP/2 PING-фреймы в характерные моменты, OCSP-staple проверки, ALPN-переговоры, и десятки других мелочей.
Reality всё это не воспроизводит. Reality безупречно вышел из TLS-рукопожатия и переключился на туннель, и дальше через канал идёт ваш прокси-трафик. С точки зрения DPI это TLS на microsoft.com, в котором сразу после рукопожатия начинается странный поток данных, не похожий на HTTPS. Для классического чёрного списка это пройдёт, потому что сигнатуры «странности» в нём не прописаны явно. Для белого списка, где эвристика по умолчанию говорит «не разрешать, если не убедился», этого хватает чтобы зарезать.
Мы попробовали несколько настроек: разные SNI для маскировки, разные uTLS-отпечатки (chrome 120, chrome 131, firefox), разное поведение xtls-rprx-vision. Картина не менялась. Это не вопрос подобрать правильный отпечаток, это вопрос самой парадигмы.
Значит, в этих сетях нужен другой подход.
Почему UDP, и почему именно Hysteria2
В TCP-мире DPI на белый список это решённая задача с понятным ответом: смотрим на TLS, сравниваем со списком, режем неподходящее. В UDP-мире у DPI задача сложнее, потому что UDP сам по себе разный. QUIC к youtube.com и QUIC к Cloudflare и игровой трафик и видеозвонки и просто чей-то самописный протокол выглядят достаточно по-разному, чтобы единая эвристика «это разрешённый UDP» работала плохо. И слишком агрессивно резать UDP опасно для самой сети: половина мобильного трафика сейчас идёт по QUIC, ломать QUIC к крупным сайтам это себе же в ногу.
Поэтому UDP-транспорты в практическом обходе блокировок ведут себя лучше. Но просто QUIC «как есть» это тоже не решение: QUIC-handshake тоже маркируется, и DPI, который натренирован на конкретных публичных версиях QUIC (Chrome, Cloudflare, Google QUIC v1), может опознать и его.
Hysteria2 это собственный протокол поверх QUIC, ориентированный именно на обход блокировок. И главное, что нас интересовало, это его obfs-плагин по имени Salamander. Salamander накладывает на каждый UDP-пакет внешний XOR-слой с ключом, который выводится из пароля. То есть DPI, который попытается посмотреть в первый пакет hy2 и опознать QUIC-handshake по байтам, видит просто бессмысленный поток. Никаких узнаваемых сигнатур внутри пакета не выживает.
Стороны (клиент и сервер) знают пароль, выводят из него ключ, и пишут/читают одинаково. Снаружи не видно ничего полезного.
Этого вполне достаточно для того DPI, что нам нужно было обойти. Подчеркну «вполне достаточно для того» сознательно: я не знаю, как этот же подход поведёт себя на других реализациях DPI, и предположение «UDP+obfs всегда лучше» в общем виде наверняка неверно. Конкретно в наших сценариях оно сработало.
Как это устроено
Архитектура простая. Тот же relay-сервер, на котором уже жил VLESS + Reality на TCP/443, теперь дополнительно слушает hy2 на UDP/443. Один порт, разные транспорты, никакого конфликта (TCP и UDP это разные стеки в ядре).
Серверный конфиг sing-box, ядро:{
"inbounds": [{
"type": "hysteria2",
"listen": "::",
"listen_port": 443,
"users": [{ "password": "..." }],
"obfs": {
"type": "salamander",
"password": "..."
},
"tls": {
"enabled": true,
"server_name": "www.apple.com",
"certificate_path": "/etc/sing-box/hy2_cert.pem",
"key_path": "/etc/sing-box/hy2_key.pem"
}
}]
}
Два пароля, не один. Первый это аутентификация пользователя на hy2. Второй это пароль для Salamander, отдельный, потому что obfs работает на слое до того, как мы вообще разбираем hy2-пакет. Если obfs-пароль не совпал, сервер вообще не поймёт что к нему пришло.
Сертификат self-signed, на тот же CN, что и SNI разрешённого домена (мы используем тот же домен, что и в Reality, по соображениям связности логов и по тому, что cert CN внутри TLS виден только клиенту, для DPI он внутри obfs-слоя и не имеет значения). Клиент ходит с insecure: true, потому что аутентификация выносится на пароли, не на PKI.
Клиентский outbound (тот же sing-box, тот же фреймворк, что и в первой статье):
{
"outbounds": [{
"type": "hysteria2",
"server": "RELAY_ADDR",
"server_port": 443,
"password": "...",
"obfs": {
"type": "salamander",
"password": "..."
},
"tls": {
"enabled": true,
"server_name": "www.apple.com",
"insecure": true
}
}]
}
Один нюанс по сборке gomobile. Если вы собирали sing-box для прошлой статьи, у вас уже есть тег with_utls. Под hy2 + obfs понадобится дополнительно with_quic, иначе нужные модули просто не попадут во фреймворк. На размер бинарника это влияет ощутимо (десятки мегабайт), но это плата за UDP-транспорт, ничего не поделать.
Как клиент выбирает между Reality и hy2
В sing-box есть outbound типа urltest, который параллельно поднимает несколько подоблочных outbound и выбирает самый быстрый по результатам HTTP-проб. Мы сложили в urltest оба варианта, Reality и hy2, для каждого relay. На сетях, где Reality проходит, побеждает он (на TCP мы получаем чуть меньшую латентность). На сетях с белым списком Reality проба тихо не проходит, hy2 проходит, urltest это видит и переключает основной поток на hy2.
Пользователь ничего не настраивает. Приложение в первый раз делает прямое подключение, если не получается, поднимает sing-box, sing-box внутри себя выбирает рабочий транспорт.
Грабли по дороге
С самим протоколом и с конфигами всё было аккуратно, документация sing-box достаточная. Грабли вылезли в инфраструктуре.
Облачные файрволы на UDP. На этой грабле я честно потерял часть вечера, и пишу про неё подробно, потому что это та категория ошибок, в которой ты долго винишь свой конфиг. На двух хостингах из четырёх iptables на самой машине настроили правильно (UDP/443 ACCEPT), сертификаты разложили, sing-box стартанул и слушает, всё хорошо. Снаружи трафика на машину не приходит вообще. Сидишь с tcpdump на хосте, видишь ноль пакетов, и думаешь «что я сломал в iptables», хотя в iptables всё в порядке. Это не баг конфига, это облачный firewall на уровне провайдера, который по умолчанию открывает только привычные порты, а UDP/443 закрыт. У одного из них, к слову, у API-роли инстанса не оказалось права открывать порты программно, пришлось руками заходить в UI. У второго подобная история была с named security group. Это пятиминутная задача, когда вы знаете, что её надо сделать, и часовое расследование, когда не знаете.
Урок: после того, как вы подняли listener на новом порту, обязательно проверяйте c посторонней машины, что пакеты доходят. Никогда не верить «iptables говорит open», верить tcpdump на машине, в котором вы видите входящие пакеты. И если tcpdump молчит, искать причину не в правилах самой машины, а слоем выше.
Один общий SNI для пары транспортов на одном relay. Внутри Reality TLS-рукопожатие настоящее, проксируется на microsoft.com (например), и cert CN должен соответствовать. Внутри hy2 TLS зашифрован obfs-слоем и снаружи не виден, но cert CN тоже microsoft.com, чтобы внутренние логи и панель администрирования не выглядели разнокалиберно. Это не функциональная необходимость, это операционная гигиена, чтобы через полгода смотреть в логи и не теряться.
Self-signed cert и insecure: true. Это нормально для hy2 с паролями, но при первом запуске рука сама тянется проверять «а валидный ли cert». Не валидный, и это нормально. Вся аутентификация на паролях. Если вы для какого-то будущего этапа захотите выпустить настоящий cert (через DNS-01 challenge на поддомен relay), это не сделает hy2 лучше с точки зрения обхода, это просто уберёт строку с insecure из конфига клиента.
Hosting diversity сильнее, чем кажется. Reality горит по IP, и hy2 горит по IP примерно так же. Имеет смысл, чтобы пара (Reality, hy2) у вас была на разных провайдерах, в разных регионах. Если cidr одного провайдера попадает под массовый блок, у вас остаются другие. Мы держим набор из нескольких сочетаний (один провайдер, второй провайдер, третий) и в конфиге, который доставляется отдельно от сборки, прописаны они все. Подробнее об этом подходе писал в первой статье.
Честно про границы
Уже писал в первой статье, повторю кратко: туннель меняет то, как соединение выглядит для цензора по дороге, и не меняет того, кто стоит на концах. Содержимое переписки защищается отдельно, на уровне приложения (мы используем libsignal). Hy2 + Salamander это про другой слой: про то, чтобы пакеты вообще доходили до сервера.
Salamander обфускация на пароле, не на полноценной криптографии. Если пароль вытащить (например, из вашей же скомпрометированной сборки), obfs снимается тривиально и DPI снова видит QUIC-handshake внутри. Поэтому пароли мы (как и всё, что должно быстро меняться) храним не в бинарнике, а в подписанном конфиге, который доставляется в рантайме. Сменили relay, сменили пароли, перевыпустили подпись, клиент подтянул.
Ещё одно важное наблюдение, которое мы видим по логам и обратной связи. Даже с hy2 в дополнение к Reality у части пользователей всё равно не поднимается транспорт. Это меньшинство, но устойчивое. По косвенным признакам (геолокация, оператор, время суток когда проблема обостряется) картина довольно последовательная: чем ближе сеть пользователя к зонам с особо чувствительной для государства активностью, тем плотнее настроены белые списки и тем агрессивнее режется всё, что не вписывается. То есть DPI это не однородный слой по стране, это градиент с локальными ужесточениями. Я не могу это доказать строго, у меня нет доступа к настройкам ни одного оператора, но картина совпадает у достаточного числа пользователей, чтобы упоминать это как наблюдение, а не совпадение.
И отдельно: всё это гонка. Сегодня белый список DPI не умеет хорошо резать обфускированный UDP, через год может научиться. Это нормальный процесс, к нему просто надо относиться как к процессу: иметь запас инструментов, мониторить, что отвалилось, и не считать ни один из них «навсегда».
Что забрать
Если вы делаете похожую задачу:
В сетях с белым списком DPI один Reality поверх TCP может не сработать, потому что DPI работает не на «опознать плохое», а на «убедиться в хорошем». Это другая парадигма, и Reality в неё не попадает по построению.
UDP + obfs (в нашем случае Hysteria2 + Salamander) в этих сетях работает лучше, потому что UDP-DPI в принципе слабее, а Salamander дополнительно убирает любые сигнатуры внутри пакета.
Не выбирайте один транспорт, держите оба. Reality экономит ресурс на сетях без белого списка, hy2 пробивает то, что Reality не пробивает. urltest в sing-box решает это автоматически.
Облачные firewall на UDP это отдельная тема и почти всегда отдельная боль. Закладывайте время на «открыть нужный порт в UI провайдера», особенно если у вас несколько разных провайдеров.
И сразу планируйте, что и hy2, и Reality будут гореть по IP. Один из них может прожить дольше, но оба расходники. Конфиг доставляется отдельно от сборки, иначе каждый горящий IP это новый релиз в App Store.
Всё, что описано, живёт в нашем мессенджере RCQ, сейчас он в открытой бете на iOS. Клиент с открытым исходным кодом, можно посмотреть, как именно устроен транспорт и переключение между ним: github.com/rcq-messenger/rcq-ios.
Если занимаетесь похожим и видели у себя поведение белого списка DPI или работали с hy2 в проде, расскажите в комментариях. Особенно интересно про сети, в которых не сработал ни Reality, ни hy2: с такими мы пока не встречались, но это не значит, что их нет.
Комментарии (15)

Alexinthecold
27.05.2026 07:12У кого массово упали определенные конфиги ,вы в курсе что Роскомнадзор ударил по фингерпринту хрома видимо с целью поломать MT proto и заодно упали куча vpn-сервисов.? Лечится сменой обычно фингер-принта на другой как временное решенте

max9
27.05.2026 07:12пробовал. бесполезно

Alexinthecold
27.05.2026 07:12Какой конфиг используете?
Reality tcp vision,
Xhttp reality
Свой домен или что у вас,?
Они местами снова врубили блок на количество соединений с хэндшейками..лечится переходом на grpc или xhhtp extra max connections

max9
27.05.2026 07:12Xhttp reality, причем там бывает соедиинилось, 3 "пинга" прошло и src ip влетает в блок на 15 минут

Alexinthecold
27.05.2026 07:12Так может вы просто попали под динамические блокировки ряда хостеров? И протокол и транспорт тут вообще не причем...вам просто триггером блочат сервер или даже целую подсеть
ЗЫ Вы пробовали поставить для xhttp extra max connections = 1 (только если вы измените хоть одну из стандартных настроек вам надо вручную укзаать все остальные

max9
27.05.2026 07:12пробовал 2 разных хостера и 2 разных подсети. блокировки действительно неоднородны - на мобилах разлили все давно, местный проводной пока работал. причем на мобиле с одним клиентом блок сразу, с другим работает. попробую эксперимент с extra позже.

Alexinthecold
27.05.2026 07:12Вопрос возник по поводу саламандры.. я правильно понимаю, что при включении саламандры трафик просто будет неопознанным нечто как шэдоусокс работал? что мешает тогда тспу включить белые списки протоколов через который такое не пройдет а стандартный vless tcp\xhttp reality или со своим доменом на 443 порту пролезет
такое уже было когда просто глушили весь неопознанный трафик..
Или у хистери это про другое и я не так понял
rcq Автор
27.05.2026 07:12Да, всё так. Salamander делает ровно это: XOR-маска поверх QUIC-пакетов, наружу выходит просто шум без структуры. И эту картину можно резать ровно так же как когда-то резали shadowsocks в Китае: DPI смотрит на энтропию байт, видит «случайный шум, неизвестный протокол», режет. Мега защиты от этой атаки нет.
Почему пока на UDP работает, а на TCP уже нет : TCP-443 это в основном HTTPS и SSH, всё остальное можно отрезать без особого ущерба, поэтому фильтры там быстро эволюционируют. На UDP сложнее. Там QUIC от Google и Cloudflare (HTTP/3), WebRTC (телега звонит через него, WhatsApp тоже), игровой трафик, DNS-over-QUIC. Whitelist UDP-протоколов технически делается, но collateral по легитимному трафику огромный, и это пока сдерживает :)
Окно временное. Если решат закрыть, Salamander поедет туда же куда поехал shadowsocks. У нас в плане держать relay-пул как расходник и добавлять транспорты по другим осям, чтобы когда один транспорт умрёт, был запас. Hysteria2 не главная ставка, просто часть стопки.

K0Jlya9
27.05.2026 07:12Белые списки это не белый список ип адресов? Сыриусли?

rcq Автор
27.05.2026 07:12Не, белый список здесь не про IP, а про протоколы. На уровне DPI устройство классифицирует первые байты соединения как HTTPS, QUIC, WebRTC, SSH, DNS-over-QUIC и т.д., и если узнаваемого протокола нет, соединение режется. Именно так в Китае давили obf shadowsocks и v2ray: пакеты выглядят как «случайный шум», DPI их не классифицирует, идёт дроп.
Когда я писал «TCP-443 это HTTPS и SSH, остальное режется», это и было про protocol whitelist на этом порту. На UDP технически тот же подход (пропускать только узнаваемый QUIC от Google/Cloudflare, WebRTC по STUN-сигнатурам, DNS-over-QUIC и прочее) возможен, но collateral по легитимному трафику огромный, поэтому пока не делают массово.

K0Jlya9
27.05.2026 07:12Когда это было актуально, здесь на хабре было много статей и обсуждений, и ни разу никто не упомянул что можно обойти белые списки каким то другим способом кроме размещения своего впн сервера в облаке какого-нибудь белого и пушистого яндекса и вк.

rcq Автор
27.05.2026 07:12Хм. Способы есть, и на Хабре они тоже есть.. ищи по «Reality», «xray», «sing-box».
Логика «сесть в большом облаке» работает за счёт IP-collateral: чтобы заблокировать твой IP, надо ронять заодно часть трафика к крупному хостеру. Telegram в 2018-м именно так и выжил.
Reality (наш транспорт) делает ровно ту же логику, только на TLS-слое: handshake идёт от лица реального публичного сайта (microsoft.com, yandex.com и т.п.), сертификат настоящий, SNI валидный. Зарезать наш трафик значит зарезать заодно соединение к настоящему microsoft.com у всех остальных на той же подсети. IP-collateral переехал в TLS-collateral, идея та же.
Плюс есть Hysteria2 на UDP (про неё писал выше), domain fronting через CDN (большая часть прикрыта, но местами дышит), разные обфускации поверх Shadowsocks. Тема живая, на Хабре про неё регулярно пишут.

K0Jlya9
27.05.2026 07:12Все статьи на хабре о «Reality», «xray», «sing-box» сводятся к их настройке для работы в черных списках.
Все статьи которые я тут видел о впн в белых списках были о том как получить место в белых списках. Не могу вспомнить ни одного другого варианта.
NeoCode2
У меня vless+reality+xhttp работает всё хуже. Иногда работает, иногда нет, похоже, научились как-то детектировать. Смена сайта SNI не помогает. При этом если поначалу блокировали IP-адрес целиком (даже по ssh нельзя было к vps подключиться), то теперь ssh работает, а xray часто не работает. Уже подумываю изучить что тут писали про телемост и прочую экзотику.
rcq Автор
У нас похожая картина в логах. Reality в моменте перестал работать на части маршрутов, при том что обычный TLS на тех же сетях проходит без вопросов.
Из того что видно снаружи: TSPU похоже перешёл с фильтрации по IP (то что вы наблюдали когда даже SSH не работал) на DPI который смотрит внутрь TLS. Reality-хэндшейк ловится по сочетанию мелких сигналов: порядок TLS-расширений, GREASE-паттерн, тайминги, поведение после рукопожатия. Полного списка не разобрать снаружи, тут только догадки по поведению.
«Иногда работает, иногда нет» обычно означает sampling: DPI не каждое соединение разбирает, выборочно. Со временем правила обычно затягивают, не ослабляют.
Про конкретно телемост ничего не подскажу. Но смотреть в UDP-сторону смысл есть, UDP-DPI у TSPU сейчас работает заметно слабее. Если поверх UDP положить obfs (Salamander у Hysteria2, например, XOR-маска на каждый пакет), DPI вообще не видит handshake внутри пакета. Внутри шум. У нас на тех сетях где Reality умер, hy2+salamander пока ходит. Гонка продолжается, но пара месяцев форы это уже что-то.