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

Как TLS-фрагментация пакетов обходит DPI: настройка разбиения ClientHello

Как TLS-фрагментация пакетов обходит DPI: настройка разбиения ClientHello

Первый пакет TLS-рукопожатия — лакомый кусок для DPI. В нём открытым текстом лежит SNI: имя сайта, к которому вы идёте. Провайдер видит `example.com` ещё до того, как установится шифрованный канал. Фрагментация ломает эту идиллию: разрезаем ClientHello на куски так, что SNI не помещается целиком ни в один TCP-сегмент. DPI, который смотрит на один пакет за раз, видит мусор.

Почему SNI вообще виден

TLS 1.2 и 1.3 передают расширение `server_name` в ClientHello открытым текстом. Это не баг, это фича: сервер по SNI выбирает сертификат, если на одном IP висит сотня сайтов. Шифрование начинается только после ServerHello. До этого момента весь handshake — plaintext.

DPI-системы этим пользуются. Простой парсер ищет в TCP-потоке байтовую сигнатуру `0x00 0x00` (тип расширения server_name) и вытаскивает домен. Дальше — сравнение со списком запрещённых. Совсем простые системы смотрят только на первый пакет соединения. Умные собирают поток и режут по сигнатурам. Против первых и работает фрагментация.

Механика: что именно режем

ClientHello — это структура с заголовком 5 байт (тип 0x16, версия, длина) и телом. Внутри тела — версия TLS, random, session_id, cipher_suites, compression, extensions. SNI сидит в extensions, ближе к концу. Если разрезать пакет так, чтобы граница пришлась ровно на середину имени хоста, DPI получит первый сегмент с обрывком строки.

TCP сам по себе потоковый, сегментация — деталь реализации. Но DPI часто работает на уровне «один пакет = одна единица анализа». Разрыв в 1 байт между сегментами ломает парсер, который не умеет склеивать поток. Есть варианты: разбить на два куска, на N кусков по 1 байту, вставить пустой сегмент между ними.

tcpdump как первый шаг диагностики

Прежде чем что-то ломать, посмотрите, как выглядит ваш ClientHello. Захват на loopback при обращении к любому HTTPS-хосту:

```bash

sudo tcpdump -i lo -s 0 -w /tmp/hs.pcap 'tcp port 443'

curl -s https://example.com -o /dev/null

sudo tcpdump -r /tmp/hs.pcap -A | grep -A2 example

```

Вы увидите ASCII-строку `example.com` прямо в дампе. Это и есть SNI. Если провайдер режет по ней — фрагментация поможет. Если режет по IP или по TLS-фингерпринту (JA3) — не поможет, там другая история.

Linux: iptables и nftables

Классический костыль — модуль `tpws` из zapret или самописные правила через `NFQUEUE`. Но есть и грубый способ на уровне ядра: `iptables` с `TCPMSS`. Он не режет ClientHello, он уменьшает MSS, из-за чего сегменты становятся мельче. Работает криво, ломает MTU на некоторых путях.

Гибче — `nftables` с payload matching. Пример: пометить первые пакеты нового соединения и отправить в userspace-демон, который разрежет их вручную. Но проще взять готовое. `zapret` умеет `--dpi-desync=split2` и `--dpi-desync=disorder`. Первый режет на два сегмента, второй отправляет их в обратном порядке — DPI видит второй кусок раньше первого и сходит с ума.

```bash

zapret, split на 2 сегмента с задержкой 1 мс

nfqws --dpi-desync=split2 --dpi-desync-split-pos=2 \

--dpi-desync-repeats=2 --dpi-desync-fooling=md5sig \

--qnum=200

```

`split-pos=2` означает разрез после второго байта TLS-записи. SNI гарантированно попадает во второй сегмент и не виден в первом. `fooling=md5sig` добавляет фейковый TCP-опции, чтобы некоторые DPI отбросили пакет как битый.

Стратегии под разных провайдеров

Универсальной настройки нет. Ростелеком, МТС, Билайн, мелкие региональные — у всех свои DPI-коробки. Т-Банк (Тинькофф) мобильный режет агрессивнее проводного. Иногда `split2` работает, иногда нужен `disorder`, иногда — `fake` с подменой TTL.

Практика: начинайте с `split2` на позиции 2. Если не работает — `disorder`. Если и это мимо — `fake` с TTL 8 (пакет-обманка умирает, не доходя до сервера, но DPI его видит и запоминает «плохой» поток). Тестировать на конкретном домене:

```bash

curl -v --max-time 5 https://rutracker.org 2>&1 | head -20

```

Пустой ответ или `Connection reset` — DPI режет. Успешный handshake — обход работает. Гоняйте по списку доменов, смотрите, где рвётся.

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

Если хочется понять, как это устроено под капотом, — соберите ClientHello сами и разрежьте. Ниже — грубый, но рабочий перехват: слушаем исходящий SYN, ждём первый data-пакет, режем его на два и шлём через raw socket.

```python

from scapy.all import *

import random

def split_client_hello(pkt, pos=2):

if not pkt.haslayer(TCP) or not pkt.haslayer(Raw):

return

payload = bytes(pkt[Raw].load)

if len(payload) < pos + 1 or payload[0] != 0x16:

return

p1, p2 = payload[:pos], payload[pos:]

ip = pkt[IP]

tcp = pkt[TCP]

seq = tcp.seq

s1 = IP(dst=ip.dst)/TCP(sport=tcp.sport, dport=tcp.dport,

flags="PA", seq=seq, ack=tcp.ack)/Raw(load=p1)

s2 = IP(dst=ip.dst)/TCP(sport=tcp.sport, dport=tcp.dport,

flags="PA", seq=seq+pos, ack=tcp.ack)/Raw(load=p2)

send(s1, verbose=0)

time.sleep(0.001)

send(s2, verbose=0)

```

Код учебный: он не глушит оригинальный пакет, поэтому настоящий ClientHello тоже уйдёт. Для реального перехвата нужен `NFQUEUE` или `netfilter`-хук. Но механику видно: `pos=2` режет после второго байта, SNI уезжает во второй сегмент. DPI, который смотрит только первый пакет, видит `16 03` и ничего больше.

nginx как точка тестирования

Чтобы проверить обход без внешнего сервиса, поднимите свой TLS-сервер и смотрите, доходят ли до него полные ClientHello. Конфиг минимальный:

```nginx

server {

listen 443 ssl;

server_name test.local;

ssl_certificate /etc/ssl/test.crt;

ssl_certificate_key /etc/ssl/test.key;

ssl_protocols TLSv1.2 TLSv1.3;

access_log /var/log/nginx/tls.log;

}

```

Если фрагментация ломает handshake на стороне сервера (не DPI, а сам nginx), вы увидите `SSL_do_handshake() failed` в error.log. Правильная фрагментация для сервера прозрачна: TCP склеит сегменты, nginx соберёт ClientHello целиком. Ломается только то, что смотрит на пакеты по отдельности.

Где фрагментация не спасает

Против ESNI/ECH (Encrypted Client Hello) фрагментация не нужна — там SNI уже зашифрован. Против DPI, который собирает поток и ищет сигнатуры в склейке, — тоже мимо. Против блокировки по IP — вообще бесполезно.

Отдельная история — QUIC. Там ClientHello уезжает в UDP, и TCP-фрагментация не при делах. Нужен `--dpi-desync` с поддержкой UDP или отключение QUIC в браузере (`chrome://flags/#enable-quic` → Disabled), чтобы трафик ушёл по TCP и попал под ваши правила.

Замеры: что реально меняется

Фрагментация стоит задержки. Разрез на 2 сегмента добавляет ~1 мс на handshake в локальной сети. `disorder` с реордерингом — до 5–15 мс, потому что сервер ждёт второй сегмент. `fake` с TTL-обманкой — почти без задержки, но требует аккуратной настройки TTL, иначе обманка долетит до сервера и он сбросит соединение.

| Метод | Добавка к handshake | Совместимость | Ломает что |

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

| split2 | 1–3 мс | высокая | ничего |

| disorder | 5–15 мс | средняя | некоторые фаерволы |

| fake (TTL 8) | <1 мс | низкая | пути с малым TTL |

| фрагментация на 1 байт | 10–40 мс | высокая | медленные каналы |

Цифры — с моих замеров на канале 100 Мбит/с, RTT до сервера 12 мс. На мобильном интернете всё хуже: реордеринг там штатное явление, `disorder` может слиться с естественными перестановками и не дать эффекта.

Кейс: провайдер режет по первому пакету

Сервер на nginx 1.24 за NAT, клиент за МТС домашним. `curl https://blocked.example` падает с `Connection reset by peer` через 200 мс после начала handshake. tcpdump показывает: клиент шлёт ClientHello, сервер отвечает ServerHello, но клиентский ACK не доходит — RST от провайдера.

Причина: DPI видит SNI в первом data-пакете и инжектит RST. Решение: `zapret` с `--dpi-desync=split2 --dpi-desync-split-pos=2`. После включения — handshake проходит, скорость падает на 2 мс. Проверено на 15 доменах, все открываются.

Кейс: мобильный оператор и TLS 1.3

Билайн мобильный, Android 13, Chrome. Сайт грузится через раз. Захват показывает: при TLS 1.3 ClientHello уходит целиком, DPI режет. При TLS 1.2 — тоже режет, но иначе: рвёт после ServerHello.

Причина: DPI различает версии по байту в заголовке и применяет разные правила. Решение: `--dpi-desync=disorder --dpi-desync-split-pos=1 --dpi-desync-repeats=3`. Три повтора нужны, потому что мобильная сеть теряет сегменты. После настройки — стабильно, но handshake вырос с 80 до 140 мс.

Кейс: корпоративный прокси и JA3

Офис за корпоративным прокси, который фильтрует по JA3-фингерпринту (хэш параметров ClientHello). Фрагментация не помогает: прокси собирает поток и считает хэш по склеенному ClientHello. Хоть на 100 кусков режь.

Причина: JA3 считается по содержимому, а не по разбиению. Решение — менять сам ClientHello: другие cipher_suites, другой порядок extensions, другой TLS-стек. В браузере это `curl_cffi` или `curl-impersonate`. Фрагментация тут — мёртвый номер, и это важно понимать до того, как полдня убить на подбор `split-pos`.

Чек-лист перед настройкой

Сначала выясните, что именно режет. tcpdump + Wireshark, фильтр `tcp.port==443 && tcp.len>0`. Смотрите, на каком пакете приходит RST или FIN. Если RST после первого data-пакета — фрагментация поможет. Если после ServerHello — режут по ответу сервера, нужны другие методы.

Проверьте MTU пути: `ping -M do -s 1472 example.com`. Если пакеты больше 1400 не проходят, фрагментация усугубит проблему — сегменты будут дробиться на IP-уровне, и DPI увидит ещё больше мусора. В таких случаях `split-pos` ставят ближе к концу, чтобы куски влезали в MTU.

И последнее: тестируйте на живом трафике, а не на `curl`. Браузер шлёт ClientHello иначе — другие extensions, другой размер. То, что работает в curl, может не работать в Chrome. Если нужен стабильный обход на разных провайдерах без ручной возни с `split-pos`, посмотрите в сторону готовых решений вроде lexic.ml — там IPv6-прокси с 2015 года, и трафик идёт мимо DPI на уровне маршрутизации, а не парсинга пакетов.

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