← Назад в базу знаний

TLS-фрагментация против DPI: как обойти блокировку прокси на уровне пакетов

Почему DPI цепляется за TLS ClientHello

Клиентский Hello — первый пакет TLS-рукопожатия. В нём открытым текстом (по стандарту) лежит SNI — имя хоста, к которому идёт подключение. DPI-железо на маршрутизаторе провайдера читает этот пакет, сверяет SNI со списком запрещённых доменов и рвёт соединение через TCP RST или просто дропает пакеты.

Проблема в том, что SNI спрятать нельзя без ECH (Encrypted Client Hello), а ECH до сих пор не раскатан массово — Cloudflare поддерживает с 2023 года, но не все браузеры и не все серверы. Пока ECH не стал нормой, единственный способ протащить запрещённый SNI мимо DPI — разрезать ClientHello на куски так, чтобы парсер DPI не смог собрать целую строку.

Ключевой момент: DPI обычно не держит состояние TCP-потока полностью. Он смотрит на отдельные пакеты или на первые несколько. Если разбить ClientHello на TCP-сегменты по 1–5 байт, многие системы фильтрации просто не склеивают их обратно.

Анатомия TLS ClientHello

Прежде чем резать, надо понять, что режем. Структура ClientHello:

```

TLS Record Header (5 байт):

Content Type: 0x16 (Handshake)

Version: 0x0301 (TLS 1.0 в заголовке записи)

Length: 2 байта

Handshake Header (4 байта):

Type: 0x01 (ClientHello)

Length: 3 байта

ClientHello Body:

Version: 2 байта

Random: 32 байта

Session ID Length + Session ID

Cipher Suites Length + Cipher Suites

Compression Methods

Extensions:

- SNI (type 0x0000) — вот тут домен

- ALPN, Supported Groups, и т.д.

```

SNI сидит в расширении с типом 0x0000. Смещение SNI внутри ClientHello — примерно 40–80 байт от начала записи, зависит от размера session ID и списка шифров. Именно эту область и надо разорвать.

Способы фрагментации

Есть несколько техник, и они не равнозначны.

**TCP-сегментация.** Клиент отправляет ClientHello, разбитый на несколько TCP-сегментов. Ядро Linux умеет это через `TCP_NODELAY` + несколько `send()`. Проблема: ядро может склеить вызовы в один сегмент из-за Nagle или из-за того, что MSS позволяет. Нужен контроль.

**TLS-запись по байтам.** Клиент формирует несколько TLS-записей, каждая с одним байтом handshake-данных. Это законно по RFC 5246 — TLS-записи могут быть любой длины, хоть 1 байт. DPI, который парсит только одну запись за раз, увидит мусор.

**Фейковый первый пакет.** Отправляется ClientHello с неправильным SNI или вообще без него, потом настоящий. Работает против простых DPI, которые ставят соединение на паузу после первого подозрительного пакета.

Комбинация TCP-сегментации и разбиения на мелкие TLS-записи даёт лучший результат.

Реализация на Python: ручная фрагментация

Вот рабочий пример, который режет ClientHello на куски по 3 байта и отправляет их отдельными TLS-записями:

```python

import socket

import ssl

import struct

def fragment_client_hello(host, port, sni, chunk_size=3):

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

sock.connect((host, port))

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

ctx.check_hostname = False

ctx.verify_mode = ssl.CERT_NONE

ctx.set_alpn_protocols(['h2', 'http/1.1'])

incoming = ssl.MemoryBIO()

outgoing = ssl.MemoryBIO()

sslobj = ctx.wrap_bio(incoming, outgoing, server_side=False, server_hostname=sni)

sslobj.do_handshake()

hello = outgoing.read()

hello = TLS record header (5) + handshake data

payload = hello[5:]

for i in range(0, len(payload), chunk_size):

chunk = payload[i:i+chunk_size]

record = b'\x16\x03\x01' + struct.pack('>H', len(chunk)) + chunk

sock.sendall(record)

return sock, sslobj, incoming, outgoing

```

Тут важна деталь: `outgoing.read()` возвращает целую TLS-запись, которую Python собрал внутри. Мы её разбираем обратно — берём только handshake-данные, нарезаем и заворачиваем каждую порцию в собственную TLS-запись с корректным заголовком. Сервер соберёт всё обратно, потому что TLS-записи конкатенируются на уровне протокола.

curl и фрагментация: что умеет из коробки

curl с 7.86 умеет `--tls-max` и `--ciphers`, но фрагментации ClientHello там нет. Однако есть трюк с `--resolve` и `--connect-to`, который иногда помогает обойти SNI-фильтры на уровне DNS, но не DPI.

Проверить, режет ли ваш провайдер по SNI, можно так:

```bash

curl -v --resolve blocked.example:443:203.0.113.10 https://blocked.example/ 2>&1 | head -30

```

Если TCP-соединение устанавливается (`Connected to`), но TLS-рукопожатие падает — DPI на уровне SNI. Если даже TCP не идёт — блокировка по IP.

Для реальной фрагментации через curl нужен внешний инструмент. Например, `curl-impersonate` с патчами, или обёртка через `openssl s_client` с кастомным BIO.

OpenSSL s_client и -split_send_frag

OpenSSL 3.x умеет фрагментировать исходящие записи. Опция `-split_send_frag N` разбивает каждую TLS-запись на N частей:

```bash

openssl s_client -connect 203.0.113.10:443 \

-servername blocked.example \

-split_send_frag 1 \

-max_send_frag 64 \

-tls1_3

```

`-split_send_frag 1` означает, что каждая запись будет разбита на максимально мелкие куски. `-max_send_frag 64` ограничивает размер записи 64 байтами. Вместе это даёт ClientHello, размазанный по десяткам TCP-сегментов.

Минус: увеличивается RTT. На канале с 50 мс задержки ClientHello в 512 байт, разрезанный на 64-байтовые куски, добавит 7 дополнительных round-trip'ов только на отправку. Это 350 мс сверху. Терпимо для статики, больно для интерактивных приложений.

Сравнение методов по устойчивости

| Метод | Обход простого DPI | Обход stateful DPI | Доп. задержка | Сложность |

|---|---|---|---|---|

| TCP-сегментация (ядро) | Да | Иногда | 1-2 RTT | Низкая |

| TLS-записи по 1-3 байта | Да | Да | 5-15 RTT | Средняя |

| Фейковый ClientHello | Да | Нет | 1 RTT | Низкая |

| ECH | Да | Да | 0 | Высокая (нужен сервер) |

| Domain fronting | Да | Зависит от CDN | 0 | Средняя |

Stateful DPI — это системы, которые собирают TCP-поток в буфер перед анализом. Против них фрагментация не работает, потому что они всё равно склеят. Но таких систем меньше, чем принято думать: они дорогие по CPU и памяти на пропускной способности 10+ Гбит/с.

Nginx как прокси-фронт с фрагментацией

Если вы контролируете серверную сторону, можно настроить nginx так, чтобы он сам фрагментировал ответы и, что важнее, принимал фрагментированные ClientHello без проблем. Nginx 1.24 с OpenSSL 3.0 принимает любую разбивку.

```nginx

server {

listen 443 ssl;

server_name proxy.example;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_ciphers HIGH:!aNULL:!MD5;

ssl_session_cache shared:SSL:10m;

ssl_session_timeout 10m;

ssl_buffer_size 4k;

location / {

proxy_pass http://127.0.0.1:8080;

proxy_http_version 1.1;

proxy_set_header Host \$host;

}

}

```

`ssl_buffer_size 4k` уменьшает размер буфера записи, что заставляет nginx отправлять данные мелкими порциями. Это не фрагментация ClientHello (её делает клиент), но снижает вероятность того, что DPI поймает паттерн в ответе сервера.

Прокси на lexic.ml и фрагментация на стороне клиента

Когда клиент подключается через IPv6-прокси, фрагментация ClientHello должна происходить до выхода трафика в туннель. Иначе DPI увидит уже собранный пакет. На стороне клиента это делается либо через патченный curl, либо через локальный редирект вроде `redsocks` с кастомным модулем.

Схема: приложение → локальный фрагментирующий сокет → прокси → целевой сервер. Локальный сокет принимает обычный TLS, режет ClientHello и пересылает через прокси. На практике это реализуется через `tun2socks` с патчем или через `sing-box` с опцией `fragment`.

В `sing-box` конфиг выглядит так:

```json

{

"inbounds": [{"type": "tun", "tag": "tun-in"}],

"outbounds": [

{

"type": "vless",

"tag": "proxy",

"server": "2001:db8::1",

"server_port": 443,

"uuid": "00000000-0000-0000-0000-000000000000",

"tls": {

"enabled": true,

"server_name": "proxy.example",

"fragment": true,

"fragment_fallback_delay": "500ms"

}

}

]

}

```

`fragment: true` включает разбиение ClientHello. `fragment_fallback_delay` — задержка перед отправкой второго фрагмента, чтобы DPI успел «увидеть» первый и не ждал продолжения.

Что ломается при фрагментации

Фрагментация — не бесплатная операция. Есть грабли.

**MTU и PMTUD.** Если фрагменты идут через туннель с MTU 1280 (минимум для IPv6), а TCP-сегменты по 1400 байт, ядро начнёт фрагментировать на IP-уровне. Это уже другая история, и DPI может реагировать на IP-фрагменты агрессивнее. Надо явно ставить MSS clamping.

**Retransmission.** Мелкие TLS-записи увеличивают число пакетов. Потеря одного пакета из 50 вызовет ретрансмит всего окна. На плохом канале с потерями 2% это может уронить соединение.

**Серверный rate limit.** Некоторые серверы (особенно за CDN) считают аномалией ClientHello из 100+ мелких записей. Cloudflare, например, может выдать challenge. Akamai режет по TLS fingerprint.

Проверка на реальных данных

Замерял на канале 100 Мбит/с, RTT до сервера 42 мс. ClientHello размером 517 байт.

| Режим | Число TCP-сегментов | Время до первого байта ответа |

|---|---|---|

| Без фрагментации | 1 | 89 мс |

| TCP-сегментация по 64 байта | 9 | 134 мс |

| TLS-записи по 3 байта | 173 | 612 мс |

| TLS-записи по 1 байту | 517 | 1840 мс |

Разница между 64-байтовыми и 3-байтовыми фрагментами — почти пятикратная по задержке. Для большинства DPI хватает 64 байт, чтобы SNI не собрался: строка домена длиной 20+ символов не поместится в один сегмент. Резать по 1 байту — избыточно и вредно.

Оптимально: 3–5 TCP-сегментов по 40–80 байт, покрывающих область SNI. Остальную часть ClientHello можно отправить целиком.

Почему DPI не всегда побеждается

Фрагментация работает против пассивного DPI, который смотрит на отдельные пакеты. Против активного DPI с полной сборкой потока — нет. Против DPI с эвристиками (например, «если ClientHello разбит более чем на 10 сегментов — блокировать») — тоже нет.

Реальный обход — это комбинация: фрагментация + смена TLS fingerprint (uTLS) + ECH, если сервер поддерживает. По отдельности каждый метод имеет дыры. Вместе они дают устойчивость, потому что DPI-системы обычно заточены под конкретный паттерн, а не под класс поведений.

Плюс нельзя забывать про DNS. Если провайдер блокирует на уровне DNS, фрагментация TLS не поможет — клиент просто не узнает IP. DoH/DoT обязателен как базовый слой.

✔️Купить прокси