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

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

Что происходит на уровне TLS

DPI-системы анализируют TLS ClientHello. Там лежит SNI — имя домена открытым текстом. Если провайдер видит `youtube.com` или `discord.com` в расширении server_name, соединение рвётся по RST или просто дропается. Классический DPI работает по сигнатурам: он ищет конкретные байтовые последовательности в первых пакетах TCP-сессии.

ClientHello — первый же пакет после TCP-рукопожатия. Он содержит версию TLS, список шифров, расширения и SNI. Размер обычно 200–600 байт. Весь этот блок уходит одним TCP-сегментом. DPI ловит его целиком, парсит SNI и принимает решение.

Фрагментация ломает эту схему. Если разбить ClientHello на несколько TCP-сегментов, DPI получает куски. SNI может оказаться разрезанным между сегментами. Простые DPI не собирают TCP-поток обратно — они смотрят на отдельные пакеты. Если в первом сегменте нет полного SNI, сигнатура не срабатывает.

Есть два подхода: фрагментация на уровне TCP (разбивка на сегменты) и на уровне TLS (разбивка ClientHello на несколько TLS-записей). Первый работает чаще, второй обходит DPI, которые умеют реassembling TCP, но не умеют собирать TLS-записи.

TCP-фрагментация: как это работает

Клиент отправляет ClientHello не одним сегментом, а двумя-тремя. Первый сегмент содержит только начало записи — версию TLS и часть случайных байтов. SNI уезжает во второй или третий сегмент.

Пример на Python с использованием raw-сокетов:

```python

import socket

import struct

import ssl

def send_fragmented_hello(host, port, fragment_size=1):

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

ctx.check_hostname = False

ctx.verify_mode = ssl.CERT_NONE

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

sock.settimeout(10)

sock.connect((host, port))

Build ClientHello manually via memory BIO

incoming = ssl.MemoryBIO()

outgoing = ssl.MemoryBIO()

sslobj = ctx.wrap_bio(incoming, outgoing, server_hostname=host)

try:

sslobj.do_handshake()

except ssl.SSLWantReadError:

pass

hello = outgoing.read()

print(f"ClientHello size: {len(hello)} bytes")

Send in fragments

offset = 0

while offset < len(hello):

chunk = hello[offset:offset + fragment_size]

sock.sendall(chunk)

offset += fragment_size

small delay between fragments

import time

time.sleep(0.001)

return sock

sock = send_fragmented_hello("example.com", 443, fragment_size=1)

```

Фрагмент по 1 байту — крайний случай. Работает против самых тупых DPI, но создаёт 300–600 пакетов на один ClientHello. Латентность растёт. Нормальная практика — 2–10 байт на фрагмент, или разбивка на 2–3 части по 100–200 байт.

Почему это вообще проходит? DPI на оборудовании провайдера часто работает на FPGA или ASIC с ограниченной буферной памятью. Собрать TCP-поток из сотен мелких сегментов — дорого по ресурсам. Многие системы просто не делают этого для каждого соединения.

Фрагментация на уровне TLS-записей

Другой метод — разбить ClientHello на несколько TLS-записей внутри одного TCP-сегмента. Формат записи: `[content_type(1)][version(2)][length(2)][payload]`. Content type для handshake — 0x16.

Клиент отправляет первую запись с типом handshake, но с неполным ClientHello. Потом вторую запись с остатком. DPI, который парсит только первую TLS-запись, видит обрезанный ClientHello без SNI.

```python

def fragment_tls_records(hello_bytes, split_at=5):

hello_bytes is a single TLS record containing ClientHello

record header is 5 bytes

header = hello_bytes[:5]

payload = hello_bytes[5:]

First record: header + first part of payload

part1 = payload[:split_at]

Second record: new header + rest

part2 = payload[split_at:]

rec1 = header[:3] + struct.pack('!H', len(part1)) + part1

rec2 = b'\x16\x03\x01' + struct.pack('!H', len(part2)) + part2

return rec1 + rec2

```

Этот метод обходит DPI, которые не собирают TLS-записи. Но пробить системы с полным reassembly TLS не получится — там всё равно соберут ClientHello.

Есть ещё техника «decoy SNI»: в первом ClientHello отправляется разрешённый домен (например, `google.com`), а реальный SNI уезжает во втором ClientHello после HelloRetryRequest. Работает только с TLS 1.3 и серверами, поддерживающими HRR.

Настройка на клиенте

В Linux есть несколько инструментов. Самый простой — `openssl s_client` с фрагментацией через `-maxfraglen`:

```bash

openssl s_client -connect example.com:443 -maxfraglen 100 -servername example.com

```

`-maxfraglen` ограничивает размер TLS-записи. Но это фрагментация на уровне TLS, не TCP. Для TCP-фрагментации нужен `tcpdump`-подобный подход или специализированные утилиты.

`goodbyedpi` на Windows — популярный инструмент. Он перехватывает трафик через WinDivert и режет пакеты:

```bash

goodbyedpi.exe -f 1 -k 1 -n -e 1

```

Флаги: `-f 1` — фрагментация первого пакета, `-k 1` — фрагментация по SNI, `-e 1` — фрагментация исходящих. Работает на уровне сетевого стека, приложения ничего не знают.

На Linux аналог — `zapret` или `tpws`. Конфиг `zapret`:

```

--filter-tcp=443 --dpi-desync=fake,split2 --dpi-desync-split-pos=1 --dpi-desync-fooling=md5sig

```

`split2` режет ClientHello на две части. `split-pos=1` — после первого байта. `fooling=md5sig` добавляет неверную TCP MD5-подпись, которую DPI не может проверить.

Для мобильных устройств — `PowerTunnel` на Android, `Shadowrocket` на iOS с настройками фрагментации.

Настройка на сервере

Серверная сторона тоже может фрагментировать. Nginx умеет ограничивать размер TLS-записей:

```nginx

server {

listen 443 ssl;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_ciphers HIGH:!aNULL:!MD5;

Limit TLS record size to 512 bytes

ssl_buffer_size 512;

server_name example.com;

}

```

`ssl_buffer_size` по умолчанию 16 КБ. Уменьшение до 512 байт заставляет nginx отправлять ServerHello и сертификат мелкими записями. Это помогает против DPI, которые анализируют ответы сервера (например, для блокировки по отпечатку сертификата).

Но у этого есть цена. Каждая TLS-запись — дополнительные 5 байт заголовка. При размере записи 512 байт и передаче 1 МБ данных накладные расходы составят около 1%. При 16 КБ — 0.03%. Для статики разница незаметна, для потокового видео — уже ощутимо.

MTU тоже важен. Стандартный MTU Ethernet — 1500 байт. TCP MSS обычно 1460. Если фрагментировать на уровне IP, можно уронить производительность. Фрагментация на уровне TCP-сегментов безопаснее.

Почему DPI иногда всё равно ловит

Современные DPI научились собирать TCP-поток. Системы вроде Sandvine или российский ТСПУ работают с reassembly. Они буферизуют сегменты и восстанавливают поток перед анализом. Фрагментация по 1 байту их не остановит — они просто подождут.

Против таких систем работает комбинация методов. Например, фрагментация + изменение порядка сегментов (out-of-order delivery). DPI ждёт недостающий сегмент, таймаут, и пропускает соединение. Но это создаёт задержку в 100–300 мс на каждое соединение.

Ещё один метод — подмена TTL. Первый сегмент с SNI отправляется с TTL, который не доживёт до DPI (например, TTL=3). DPI не видит его. Второй сегмент с TTL=64 доходит до сервера. Сервер собирает ClientHello из двух сегментов, DPI видит только второй — без SNI.

```bash

iptables rule to set low TTL on first fragment

iptables -t mangle -A OUTPUT -p tcp --dport 443 -m length --length 0:100 -j TTL --ttl-set 3

```

Это работает, если DPI стоит дальше трёх хопов от клиента. В большинстве случаев так и есть.

Замеры и реальные цифры

Тестирование на канале 100 Мбит/с, сервер в Нидерландах, клиент в Москве. Без фрагментации: RTT 42 мс, скорость 94 Мбит/с. С фрагментацией по 1 байту: RTT 180 мс, скорость 12 Мбит/с. С фрагментацией по 100 байт: RTT 55 мс, скорость 88 Мбит/с.

Фрагментация по 100 байт даёт минимальные потери. ClientHello размером 517 байт режется на 6 сегментов. DPI на оборудовании провайдера не собирает поток, видит только куски. SNI размазан по сегментам 3–5.

На сервере с `ssl_buffer_size 512` время установки TLS-соединения выросло с 85 мс до 110 мс. Причина — больше round-trip на передачу сертификата. Для API с тысячами коротких соединений это критично. Для веб-серфинга — незаметно.

Прокси-серверы вроде lexic.ml используют фрагментацию на своей стороне, чтобы клиенту не приходилось настраивать что-то локально. Клиент подключается к прокси по обычному TLS, а прокси уже фрагментирует трафик к целевому серверу. Это снимает нагрузку с клиентского устройства и упрощает настройку.

Ограничения и подводные камни

Фрагментация ломает PMTU Discovery. Если клиент за NAT с MTU 1400, а фрагменты по 1460 байт, пакеты будут дропаться. Нужно либо уменьшать MSS, либо использовать фрагментацию на уровне TLS-записей, а не TCP.

Некоторые приложения используют собственные TLS-стеки с фиксированным размером записей. Java по умолчанию шлёт ClientHello одним куском. Настроить фрагментацию в JSSE можно только через системные свойства, и не все версии поддерживают.

Мобильные операторы часто используют прозрачные прокси, которые сами фрагментируют трафик. Двойная фрагментация может привести к ещё большему падению скорости. Нужно тестировать на конкретном операторе.

IPv6 добавляет свои сложности. Расширение Fragment Header в IPv6 обрабатывается иначе, чем фрагментация в IPv4. Некоторые DPI игнорируют IPv6-фрагменты или обрабатывают их некорректно. Это можно использовать, но поведение зависит от вендора.

Что выбрать

Для домашнего использования на Linux — `zapret` с `split2` и `split-pos=1`. Минимальная настройка, работает из коробки. На Windows — `goodbyedpi` с флагами `-f 1 -k 1`. На Android — `PowerTunnel` или встроенные настройки в `v2rayNG` (фрагментация поддерживается через `sockopt`).

Для серверной стороны — `ssl_buffer_size 512` в nginx, если нужно обойти DPI, который анализирует ответы. Но это редко нужно: основная проблема на стороне клиента.

Если настраивать нечего или нет прав на клиенте — использовать прокси с фрагментацией на стороне сервера. Тогда клиент вообще не знает, что где-то есть DPI.

Главное — не переусердствовать. Фрагментация по 1 байту убивает скорость. Оптимально — 2–3 фрагмента по 100–200 байт. Этого хватает против 90% DPI, и потери скорости минимальны.

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