17 KiB
SNI-маршрутизация в xray-core / sing-box + TLS-fronting
Термины (в контексте этого кейса)
- TLS-fronting домен — домен, который фигурирует в TLS ClientHello как SNI (например,
petrovich.ru): он используется как "маска" на L7 и как ключ маршрутизации в прокси-роутере. - xray-core / sing-box — локальный или удалённый L7/TLS-роутер (прокси), который:
- принимает входящее TCP/TLS-соединение,
- читает TLS ClientHello,
- извлекает SNI,
- по SNI выбирает outbound/апстрим,
- устанавливает новое TCP-соединение к целевому хосту уже от себя.
- SNI (Server Name Indication) — поле в TLS ClientHello, где клиент Telegram сообщает доменное имя для "маскировки"
- DNS-resolve на стороне L7-роутера — если выходной адрес задан доменом (или роутер решил "всё равно идти по SNI"), то DNS резолвится на стороне xray/sing-box, а не на стороне Telegram-клиента
Ключевая идея: куда на самом деле идёт соединение решает не то, что вы указали клиенту, а то как L7-роутер трактует SNI
Механика:
- Telegram-клиенту вы можете указать IP/домен telemt,как "сервер".
- Между клиентом и telemt стоит xray-core/sing-box, который принимает TCP, читает TLS ClientHello и видит SNI=petrovich.ru
- Дальше роутер говорит: "Вижу SNI - направить на апстрим/маршрут N"
- И устанавливает исходящее соединение не "по тому IP, который пользователь подразумевал", а по домену из SNI (или по сопоставлению SNI→outbound), используя для определния его IP собственный DNS-кеш или резолвер
petrovich.ruпо A-записи указывает не на IP telemt, а значит при L7-маршрутизации трафик уйдёт на "оригинальный" сайт за этим доменом, а не в telemt: Telegram-клиент, естественно, не сможет получить ожидаемое поведение, потому что ответить с handshake на той стороне некому
Схема №1 "Как это НЕ работает"
Telegram Client
|
| (указан IP/домен telemt)
v
telemt instance
Ожидание: "я указал telemt -> значит трафик попадёт в telemt" - нет!
Схема №2. "Как это реально работает с TLS/L7-роутером и SNI"
Telegram Client
|
| 1) TCP/TLS connection:
| - ClientHello:
| - SNI=petrovich.ru
v
xray-core / sing-box / любой L7 router
|
| 2) читает ClientHello -> вытаскивает SNI
| 3) выбирает маршрут по SNI
| 4) делает DNS для petrovich.ru
| 5) подключается к полученному IP по TLS с этим SNI
v
"Оригинальный" сайт, A-запись которого не на telemt
|
X не telemt -> Telegram-клиент не коннектится как ожидалось
Почему указанный в клиенте IP/домен telemt "не спасает"
Потому что в таком режиме xray/sing-box выступает как точка терминации TCP/TLS, можно сказать - TLS-инспектор на уровне ClientHello, это означает:
- TCP-сессия от Telegram-клиента заканчивается на xray/sing-box
- Дальше создаётся новая TCP-сессия "от имени" xray/sing-box к апстриму
- Выбор апстрима делается правилами роутинга, а в TLS-сценариях самый удобный и распространённый ключ — SNI
То есть, "куда идти дальше" определяется логикой L7-роутера:
- либо правилами вида
if SNI == petrovich.ru -> outbound X, - либо более "автоматическим" поведением:
подключаться к тому хосту, который указан в SNI, - плюс кэш DNS и собственные резолверы роутера
Что именно извлекается из TLS ClientHello и почему этого достаточно
TLS ClientHello отправляется в начале TLS-сессии и, в классическом TLS без ECH, содержит SNI в открытом виде.
Упрощённо:
ClientHello:
- supported_versions
- cipher_suites
- extensions:
- server_name: petrovich.ru <-- SNI
- alpn: h2/http1.1/...
- ...
Роутеру не нужно расшифровывать трафик и завершать TLS "как сервер" — часто достаточно просто прочитать первые пакеты и распарсить ClientHello, чтобы получить SNI и принять решение
Типовой алгоритм SNI-роутинга
-
Принять входящий TCP.
-
Подождать первые байты.
-
Определить протокол:
- если видим TLS ClientHello → парсим SNI/ALPN
-
Применить route rules:
- match по
server_name/domain/tls.sni
- match по
-
Выбрать outbound:
- direct / proxy / specific upstream / detour
-
Установить исходящее соединение:
- либо на фиксированный IP:порт,
- либо на домен через DNS-resolve на стороне роутера
-
Начать проксирование данных между входом и выходом
Почему "A-запись фронтинг-домена не на telemt" ломает кейс
Ситуация
- В ClientHello:
SNI = petrovich.ru - DNS:
petrovich.ru -> 203.0.113.77- "оригинальный" сайт - telemt живёт на:
198.51.100.10
Что делает роутер
-
Видит SNI
petrovich.ru -
Либо:
- (а) напрямую коннектится к
petrovich.ru:443, резолвя A-запись в203.0.113.77, - либо:
- (б) выбирает outbound, который указывает на
petrovich.ruкак destination, - либо:
- (в) делает sniffing/override destination по SNI
- (а) напрямую коннектится к
В итоге исходящий коннект идёт на 203.0.113.77:443, а не на telemt!
Другой сервер, другой протокол, другая логика, где telemt не участвует
"Где именно происходит подмена destination на SNI"
Это зависит от конфигурации, но типовые варианты:
Вариант A: outbound задан доменом (и он совпадает с SNI)
Правило по SNI выбирает outbound, у которого destination задан доменом фронтинга, тогда DNS резолвится на стороне роутера и вы уходите на "оригинальный" хост
Вариант B: destination override / sniffing
Роутер "снифает" SNI и перезаписывает destination на домен из SNI (даже если вход изначально был на IP telemt), это особенно коварно: пользователь видит "я подключаюсь к IP telemt", но роутер после sniffing решает иначе
Вариант C: split DNS / кеш / независимый резолвер
Даже если клиент "где-то" резолвит иначе, это не важно: конечный DNS для исходящего коннекта — на стороне xray/sing-box, который может иметь:
- свой DoH/DoT,
- свой кеш,
- свои правила fake-ip / system resolver,
- и, как следствие, своя "карта" домен/SNI -> IP
Признаки того, что трафик "утёк на оригинал", а не попал в telemt
- На стороне telemt отсутствуют входящие соединения/логи
- На стороне роутера видно, что destination — домен фронтинга, а IP соответствует публичному сайту
- TLS-метрики/сертификат на выходе соответствует "оригинальному" сайту в записах трафика
- Telegram-клиент получает неожиданный тип ответов/ошибку handshaking/timeout в debug-режиме
Best-practice решение для этого кейса: свой домен фронтинга + заглушка на telemt + Let's Encrypt
Цель
Сделать так, чтобы:
- SNI (фронтинг-домен) резолвился в IP telemt,
- на IP telemt реально был TLS-сервис с валидным сертификатом под этот домен,
- даже если кто-то "попробует открыть домен как сайт", он увидит нормальную заглушку, а не "пустоту"
Что это даёт
- xray/sing-box, маршрутизируя по SNI, будет неизбежно приходить на telemt, потому что DNS(SNI-домен) → IP telemt
- Внешний вид будет правдоподобным: обычный домен с обычным сертификатом
- Устойчивость: меньше сюрпризов от DNS-кеша/перерезолва/"умных" правил роутера
Рекомендуемая схема (целевое состояние)
Telegram Client
|
| TLS ClientHello: SNI = hello.example.com
v
xray-core / sing-box
|
| Route by SNI -> outbound -> connect to hello.example.com:443
| DNS(hello.example.com) = IP telemt
v
telemt instance (IP telemt)
|
| TLS cert for hello.example.com (Let's Encrypt)
| + сайт-заглушка / health endpoint
v
OK
Практический чеклист (минимальный)
-
Купить/иметь домен:
hello.example.com -
В DNS:
A hello.example.com -> <IP telemt>- (опционально) AAAA, если используете IPv6 и он стабилен
-
На telemt-хосте:
- поднять TLS endpoint на 443 с валидным сертификатом LE под
hello.example.com - отдать "заглушку" (например, статический сайт), чтобы домен выглядел как обычный веб-сервис
- поднять TLS endpoint на 443 с валидным сертификатом LE под
-
В xray/sing-box правилах:
- маршрутизировать нужный трафик по SNI =
hello.example.comв "правильный" outbound (к telemt) - избегать конфигураций, где destination override уводит на чужой домен
- маршрутизировать нужный трафик по SNI =
-
Важно:
- если вы используете кеш DNS на роутере — сбросить/обновить его после смены A-записи
Пояснение про сайт-заглушку
Для эмуляции TLS, telemt имеет подсистему TLS-F в src/tls_front:
- её модуль - fetcher, собирает TLS-профили, чтоб максимально поведенчески корректно повторять TLS конкретно указанного сайта
Когда вы указываете сайт, который не отвечает по TLS:
- fetcher не может собрать TLS-профиль и происходит fallback на
fake_cert_len- примитивный алгоритм, - он забивает служебную информацию TLS рандомными байтами,
- простые системы DPI не распознают это
- однако, продвинутые системы, такие как nEdge или Fraud Control в сетях мобильной связи легко заблокируют или замедлят такой трафик
Создав сайт-заглушку с Let's Encrypt сертификатом, вы даёте TLS-F возможность получить данные сертификата и корректно его "повторять" в дальнейшем
Вариант конфиг-подхода: "SNI строго привязываем к telemt - фиксированный IP"
Чтобы полностью исключить зависимость от DNS если вам это нужно, можно сделать outbound, который ходит на фиксированный IP telemt, но при этом выставляет SNI/Host как hello.example.com.
Идея:
- destination:
IP:443 - SNI:
hello.example.com - сертификат на telemt именно под
hello.example.com
Так вы получаете:
- TLS выглядит корректно, ведь SNI совпадает с сертификатом,
- а routing никогда не уйдёт на "оригинал", потому что A-запись указывает на telemt и контроллируется вами!
Но в вашем описании проблема как раз в том, что роутер "сам решает по SNI и резолвит домен", поэтому самый универсальный вариант — сделать так, чтобы DNS всегда приводил в telemt
Пример логики правил на псевдоконфиге L7-роутера
if inbound is TLS and sni == "hello.example.com":
route -> outbound "telemt"
else:
route -> outbound "default"
Outbound telemt:
- destination:
hello.example.com:443 - TLS enabled
- SNI:
hello.example.com
Отдельно: что может неожиданно сломать даже "правильный" DNS
- Кеширование DNS на xray/sing-box или на системном резолвере, особенно при смене A-записи
- Split-horizon DNS: разные ответы внутри/снаружи, попытки подмены/терминирования в других точках
- IPv6: если есть AAAA и он указывает не туда, роутер может предпочесть IPv6: помните, что поддержка v6 нестабильна и не рекомендуется в prod
- DoH/DoT на роутере: он может резолвить не тем резолвером, которым вы проверяли
Минимальная гигиена:
- контролировать A/AAAA,
- держать TTL разумным,
- проверять, каким резолвером пользуется именно роутер,
- при необходимости отключить/ограничить destination override
Итог
В режиме TLS-fronting с xray-core/sing-box как L7/TLS-роутером SNI становится приоритетным "source-of-truth" для маршрутизации
Если фронтинг-домен по DNS указывает не на IP telemt, роутер честно уводит трафик на "оригинальный" сайт, потому что он строит исходящее соединение "по SNI"
Надёжное решение для этого кейса:
- использовать свой домен для фронтинга,
- направить его A/AAAA на IP telemt,
- поднять на telemt TLS-сервис с Let’s Encrypt сертификатом под этот домен,
- (желательно) держать сайт-заглушку, чтобы 443 выглядел как обычный HTTPS