🔍 Как DPI анализирует соединение: воронка проверок (от SYN до ML)

Первоисточник

Эта заметка основана на статье Александра Мурзина (@cyberscoper) на Хабре: 👉 «Как работает DPI: разбор по слоям» — habr.com/ru/articles/1009560

Это заметка-«карта»: общий конвейер анализа ТСПУ от первого SYN-пакета до ML-классификации. Глубокий разбор одной его стадии (поведенческой схемы июня 2026) — в парной заметке Как DPI «замораживает» VLESS+REALITY. Вместе они дают и общую картину, и детальный срез.

Дисклеймер

Материал исследовательский и образовательный. Цель — собрать в одну картину то, по каким признакам и в каком порядке DPI разбирает соединение, чтобы было понятно, какой обходной приём какой слой закрывает.

Важно про статус этих данных

Описанная «воронка» — это аналитическая модель, собранная по наблюдениям сообщества (ntc.party) и реверс-инжинирингу, а не опубликованная спецификация ТСПУ. Конкретные числа (5 пакетов, 83 байта, 16 КБ, 95–99 %, 1–5 мс) читайте как порядок величин и авторские оценки, а не как точные константы системы. Реальная реализация различается от оператора к оператору и меняется со временем.


TL;DR — анализ идёт «воронкой»

DPI не выносит вердикт по одному пакету. Он накапливает данные и отсеивает на каждом уровне: дешёвые проверки сначала, дорогой ML — в конце и только для выживших.

                  трафик

   пакеты 1–5   ┌───▼───┐  TTL, TCP-опции (MSS/WS/SACK/TS) → отпечаток ОС
   (TCP/IP)     └───┬───┘

   байты 0–5    ┌───▼───┐  протокольный маркер: 16 03 01… / SSH-2.0- / …
   (сигнатура)  └───┬───┘

   байты 6–300  ┌───▼───┐  ClientHello → JA3 / JA4 (фингерпринт клиента)
   (TLS hello)  └───┬───┘

   байты        ┌───▼───┐  сертификат*: CT-логи, ASN, CN/SAN ↔ SNI
   300–3000     └───┬───┘  (* в TLS 1.3 сертификат шифруется →
                    │        пассивно остаются только SNI и ASN)
   байты        ┌───▼───┐  метаанализ потока: размеры, тайминги,
   3000–16000   └───┬───┘  соотношение in/out, длительность

   после 16 КБ  ┌───▼───┐  поведенческий ML-классификатор
                └───┬───┘

              вердикт → пропустить / RST / drop / throttle

Чем дальше по воронке — тем дороже проверка и тем труднее её обмануть. Обходные средства бьются за то, чтобы отсеяться пораньше (выглядеть скучно уже на ранних слоях).

Как это связано с «сибирской» схемой

Поведенческая схема июня 2026 (подсеть + фингерпринт + частота) — это, по сути, нижние этажи этой воронки: фингерпринт = слой JA3/JA4, частота/параллелизм = слой метаанализа потока. Подробный разбор именно её — в dpi-tls-june-2026. Здесь — как она вписана в общий конвейер.


Этап 0. TCP/IP — ещё до всякого TLS

DPI начинает «нюхать» соединение раньше, чем дойдёт до шифрования — на уровне самих TCP/IP-заголовков.

TTL — отпечаток ОС и детектор fake-пакетов. Разные ОС ставят разный стартовый TTL:

  • Windows → TTL 128, Linux → TTL 64 (macOS/iOS тоже 64).
  • Каждый промежуточный хоп уменьшает TTL на 1: пакет с TTL 63 прошёл один хоп, TTL 62 — два.

Отсюда — два следствия. Во-первых, по TTL грубо угадывается ОС клиента. Во-вторых (важнее для обхода): на TTL ловятся fake-пакеты инструментов вроде GoodbyeDPI — те шлют поддельный пакет с маленьким TTL, чтобы он дошёл до DPI, но «умер» по дороге к серверу. Если DPI умеет сверять TTL, он отличает такой fake от настоящего пакета. Это прямая связь нашей «воронки» с миром Zapret.

TCP-опции — пассивный отпечаток ОС (p0f). Набор и порядок TCP-опций в SYN-пакете — MSS, Window Scale, SACK, Timestamps — различается между операционными системами и их версиями. Это классический пассивный fingerprinting (техника p0f): по одному только SYN можно сказать «это похоже на Windows 11» или «это похоже на Linux».

Авторская оценка (под оговорку)

«Первые пять пакетов соединения проходят без активного вмешательства» — DPI сначала собирает контекст. И: «если первый содержательный пакет после рукопожатия короче 83 байт — подозрительно». Эти числа — иллюстрация логики, а не подтверждённые константы.


Этап 1. Протокольный маркер — первые байты данных

Как только пошли данные, DPI смотрит на первые байты — у многих протоколов начало узнаваемо:

ПротоколМаркер начала
TLS ClientHello16 03 01 xx xx 01 00 xx xx xx 03 03 …
SSHASCII-строка SSH-2.0-…
OpenVPNсвой характерный заголовок
WireGuardхарактерный UDP-заголовок (тип сообщения)
Shadowsocks (без обфускации)статистически случайные байты — отсутствие маркера само по себе примета

Разбор TLS-маркера: 16 = Content Type Handshake; 03 01 = версия record-слоя (легаси, всегда «TLS 1.0» ради совместимости); 01 = Handshake Type ClientHello; 03 03 = legacy_version.

Поправка к первоисточнику

Источник пишет, что 03 03 — это «TLS 1.3 внутри ClientHello». Это неточность: 0x0303 — это legacy_version (формально TLS 1.2), поставленный ради совместимости. Настоящая версия TLS 1.3 объявляется не здесь, а в расширении supported_versions (0x0304). Полный побайтовый разбор реального ClientHello — в байт-дампе RFC 8448 из парной заметки.

Любопытно, что Shadowsocks проходит этот этап «наоборот»: у него нет маркера вообще, поток выглядит как энтропийный шум. На фоне веба, где почти всё — TLS с узнаваемым началом, «совершенно случайные байты с первого пакета» — сами по себе аномалия (по этому признаку, кстати, и ловят многие обфусцированные протоколы).


Этап 2. JA3 / JA4 — отпечаток клиента по ClientHello

Если маркер сказал «это TLS», DPI сворачивает ClientHello в фингерпринт. Точное определение JA3:

JA3 = MD5(
  SSLVersion,
  Ciphers,            # список шифров, без GREASE-значений
  Extensions,         # список расширений, без GREASE-значений
  EllipticCurves,
  EllipticCurvePointFormats
)

JA4 (FoxIO, 2023) — новее и устойчивее: менее чувствителен к перетасовке порядка расширений (сортирует их), разделяет клиентскую и серверную части, даёт более стабильный отпечаток.

По этому слою DPI говорит «это Chrome 13x» или «это голый Go-клиент». Здесь же и кроется ловушка массового fingerprint: chrome. Глубокий разбор — что такое фингерпринт побайтово, чем JA3 отличается от JA4, почему «голый» Go палится и что делает uTLS — в разделе про uTLS парной заметки.

Нюанс про GREASE в JA3

То, что JA3 исключает GREASE-значения, — палка о двух концах. С одной стороны, отпечаток не «плавает» из-за случайных GREASE. С другой — Chrome тасует порядок расширений на каждом соединении (официально с Chrome 110, фактически проявлялось уже в сборках 108–109, начало 2023), а порядок в JA3 учитывается → у Chrome JA3 всё равно «плавает». Именно поэтому и появился JA4 с сортировкой.


Этап 3. Сертификат — проверка TLS-личности сервера

Дальше DPI смотрит на ServerHello и сертификат сервера и сверяет три вещи:

  • Certificate Transparency. Существует ли такой сертификат в публичных CT-логах? Выпущен ли он реально на этот домен?
  • ASN сервера. IP-адрес должен лежать в «правильной» автономной системе для этого бренда. Apple держит серверы в AS714, Microsoft — в AS8075. Если SNI говорит icloud.com, а IP — из чужой AS, это рассогласование.
  • SNI ↔ CN/SAN. Имя в ClientHello (SNI) должно совпадать с именем в сертификате. Если SNI = icloud.com, а приехал сертификат Let’s Encrypt на случайный домен — не совпало.

Главная оговорка этапа: в TLS 1.3 сертификат не виден

Две из трёх проверок выше пассивно работают в основном на TLS 1.2. В TLS 1.3 сообщение Certificate идёт зашифрованным (после ServerHello, под ключами рукопожатия) — пассивный наблюдатель его не читает (RFC 8446). А сегодня почти весь веб — это TLS 1.3. Поэтому:

  • CT-логи и сверка CN/SAN ↔ SNI для пассивного DPI на TLS 1.3 фактически недоступны — для них нужен активный MITM/перехват (это уже не «пассивная воронка»). На TLS 1.2 — работают.
  • Надёжно пассивно в TLS 1.3 виден только SNI в ClientHello (если не включён ECH) и IP/ASN назначения.
  • «Бренд = одна AS» — упрощение: Apple/Microsoft/Google массово раздают трафик через сторонние CDN, и тогда IP легитимно лежит в чужой AS (напр., egress-узлы iCloud Private Relay идут через сторонние CDN — Akamai, Cloudflare, Fastly, — то есть не из AS714). Грубая сверка «чужая AS → палево» даёт ложные срабатывания; реальный DPI держит allow-list всех AS/CDN бренда.

Почему REALITY проходит этот этап

Это ровно тот слой, который закрывает REALITY — и работает он только поверх TLS 1.3, что здесь принципиально. REALITY не выпускает свой сертификат: сервер ретранслирует рукопожатие клиента к реальному dest (популярному сайту). Дальше важно различать двух адресатов:

  • Для постороннего наблюдателя/DPI соединение неотличимо от настоящего TLS к dest: правильные SNI, IP и ASN, а сам Certificateзашифрован (TLS 1.3), так что наблюдатель его и не читает. Именно на шифровании сертификата и держится незаметность — DPI нечего сверять с CT.
  • Для «своего» клиента REALITY-сервер на лету подменяет сертификат на временный доверенный (привязан к ECDH-секрету/AuthKey) — по нему клиент опознаёт «свой» сервер. Если же клиенту приходит настоящий сертификат dest, это сигнал «меня редиректят/MITM-ят», и клиент уходит в режим обычного веб-браузера.

Классический же VLESS/Trojan со своим сертификатом слабее — но пассивно в TLS 1.3 его выдаёт не чтение сертификата, а IP/ASN хостинга и SNI на домен без трафика и репутации; сам Let’s Encrypt-сертификат виден лишь при активном зондировании или на TLS 1.2. Подробнее про заимствование личности — в парной заметке.


Этап 4. Метаанализ потока — поведение, а не содержимое

Если по сертификату не придрались, DPI переходит к форме трафика — её шифрование не скрывает. Здесь смотрят на статистику:

  • Соотношение in/out. У браузера трафик асимметричен: маленькие запросы, большие ответы — типично 1:10 … 1:20. Туннель, по которому льётся всё подряд, это соотношение нарушает.
  • Межпакетные интервалы. Браузер делает паузы (парсинг, отрисовка, реакция человека) — десятки–сотни миллисекунд. Туннель часто гонит данные ровным потоком без «человеческих» пауз.
  • Длительность соединения. Браузер живёт минутами (keep-alive), потом закрывается. VLESS держит соединение открытым часами — это аномалия.

Это и есть тот уровень, на котором работает Сигнал 3 «сибирской» схемы (частота и параллелизм соединений к одному SNI). Разбор поведенческого сигнала и лекарства от него (mux, разные SNI) — в парной заметке.


Этап 5. ML-классификация — после ~16 КБ

Для соединений, доживших до этого этапа, DPI скармливает накопленную статистику (размеры пакетов, тайминги, энтропию, ритм всплесков) ML-классификатору.

Заявленная точность (под оговорку)

«Точность ML-классификаторов в лабораторных условиях достигает 95–99 % для Shadowsocks и VMess. В продакшне — ниже». Лабораторные числа всегда оптимистичны: в реальном трафике с шумом, ложными срабатываниями и ценой ошибки порог срабатывания держат заметно выше.

Не путать «16 КБ → ML» и метод «tcp 16-20»

Совпадение порога обманчиво. В dpi-tls-june-2026 упоминается отдельный метод «tcp 16-20» (l4-25) — он тупо замораживает соединение после ~16 КБ / ~25 пакетов, без всякого ML. Здесь же 16 КБ — это момент, когда у DPI накопилось достаточно данных, чтобы запустить статистику. Это два разных механизма с похожим порогом, а не одно и то же.

Связь с фундаментальным пределом обфускации: даже padding и mux не убирают round-trip-узор вложенного TLS-рукопожатия (детект по USENIX Security 2024). Почему именно — разобрано в парной заметке.


Что DPI делает с вердиктом

Опознав обход, DPI применяет одну из трёх мер:

МетодЧто делаетКак ощущается
TCP RST injectionвбрасывает поддельный RST в потоксоединение резко рвётся
Drop пакетовмолча не пропускает пакеты«зависание», потом таймаут
Throttlingнамеренное замедление + потери«как будто плохой интернет»

«Заморозка на 120/600 секунд» из «сибирской» схемы — это вариант throttling: соединение не рвут демонстративно, а тихо душат, чтобы было похоже на плохую связь, а не на цензуру.

Про задержку самого DPI (под оговорку)

Активная инспекция добавляет «1–5 мс для большинства соединений». Само по себе это иногда косвенный признак присутствия DPI на маршруте.


Контрмеры по слоям: кто какой этаж закрывает

Главный практический вывод обеих заметок совпадает: ни один инструмент в одиночку не закрывает все слои. Каждый приём бьёт по своему этажу воронки:

Слой воронкиЧем закрывается
TCP/IP (TTL, опции ОС)корректный стек ОС; для Zapret — аккуратные fake с верным TTL
Протокольный маркер (байты 0–5)TLS-обёртка (REALITY/Trojan) — выглядит как обычный TLS
JA3 / JA4 (байты 6–300)uTLS-фингерпринт (fingerprint: firefox/edge/…)
SNI / ASN / сертификат (300–3000)¹REALITY (правильный SNI-донор + чужой ASN + защита от активного зондирования)
Метаанализ потока (3000–16 000)mux / разнесение по SNI / xPaddingBytes
ML (после 16 КБ)нормализация формы (XHTTP+XMUX) — и то лишь статистически

¹ В TLS 1.3 сам сертификат пассивно не виден (зашифрован), поэтому пассивно на этом этаже остаются SNI и ASN/IP; REALITY закрывает их (чужой знаменитый SNI-донор + правильный ASN) и защищает от активного зондирования — а не «прячет чтение сертификата», которого пассивный DPI в TLS 1.3 и так не делает (см. callout в Этапе 3).

Видно, что слои ортогональны: можно идеально закрыть JA3 и провалиться по подсети; можно спрятать сертификат через REALITY и спалиться по поведению потока. Устойчивость даёт не «волшебная галочка», а закрытие воронки сверху донизу — ровно к этому и приходит разбор в парной заметке.


⚠️ Честная оговорка про числа

Чтобы не выдавать модель за спецификацию — что здесь проверяемый факт, а что авторская оценка:

  • Твёрдо: TTL 128/64 и декремент по хопам; TCP-опции как отпечаток ОС (p0f); сигнатура 16 03 01…; SSH-2.0-; формула JA3 (5 полей, без GREASE); JA4/FoxIO 2023 (сортировка шифров и расширений); ASN-факты (AS714 Apple, AS8075 Microsoft); методы RST/drop/throttle.
  • ⚠️ Иллюстративно / под оговорку: «5 пакетов без вмешательства», «короче 83 байт», пороги байтовых стадий (0–5 / 6–300 / …), «95–99 % в лаборатории», «1–5 мс задержки», соотношения 1:10–1:20. Это порядок величин и логика, а не подтверждённые константы ТСПУ.
  • 🪧 С важной оговоркой: проверки CT-логов и CN/SAN ↔ SNI (Этап 3) — реальные техники, но пассивно доступны в основном на TLS 1.2; в TLS 1.3 сертификат зашифрован, и для них нужен активный MITM. Связка «бренд = одна AS» ломается на CDN.
  • 🩹 Поправлено: 0x0303 — это legacy_version (TLS 1.2), а не «TLS 1.3 внутри ClientHello» (настоящая версия — в supported_versions); из формулы JA3 убрано ошибочное «без padding (21)» — каноничный JA3 исключает только GREASE.

📚 См. также