TLS-фрагментация как способ обхода DPI: настройка на клиенте и сервере
Содержание
- Что происходит на уровне TLS
- TCP-фрагментация: как это работает
- Build ClientHello manually via memory BIO
- Send in fragments
- small delay between fragments
- Фрагментация на уровне TLS-записей
- hello_bytes is a single TLS record containing ClientHello
- record header is 5 bytes
- First record: header + first part of payload
- Second record: new header + rest
- Настройка на клиенте
- Настройка на сервере
- Limit TLS record size to 512 bytes
- Почему DPI иногда всё равно ловит
- iptables rule to set low TTL on first fragment
- Замеры и реальные цифры
- Ограничения и подводные камни
- Что выбрать
Что происходит на уровне 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, и потери скорости минимальны.