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

TLS-фрагментация против DPI: почему IPv6-прокси обходят блокировки там, где VPN сдаётся

TLS-фрагментация против DPI: почему IPv6-прокси обходят блокировки там, где VPN сдаётся

Почему TLS-фрагментация вообще работает

DPI-системы анализируют трафик пакет за пакетом. Они ищут характерные сигнатуры: SNI в ClientHello, определённые расширения TLS, размеры пакетов, тайминги. Логика простая — если разбить ClientHello на куски так, что ни один отдельный пакет не содержит полного SNI, сигнатура не собирается. DPI видит мусор из фрагментов и пропускает соединение.

Ключевой момент: большинство DPI не собирают TCP-поток заново. Они работают на уровне отдельных пакетов или небольших окон. Это архитектурное ограничение — реконструкция потока требует буферизации, памяти, CPU. Провайдеры экономят. Именно на этой экономии строится вся техника фрагментации.

TCP-фрагментация отличается от IP-фрагментации. При TCP-фрагментации мы режем данные на уровне сегментов, но каждый пакет остаётся валидным TCP-сегментом с корректными sequence numbers. DPI должен собрать их заново, чтобы увидеть полный TLS record. При IP-фрагментации режется сам IP-пакет, и промежуточные узлы обязаны собрать его до передачи дальше. Многие DPI IP-фрагменты просто дропают или обрабатывают некорректно.

Как выглядит ClientHello и что там ищут

TLS 1.3 ClientHello — это структура фиксированного формата с расширениями переменной длины. SNI находится в расширении server_name, тип 0x0000. DPI ищет паттерн: байты 0x00 0x00 (тип расширения), затем длина, затем доменное имя в ASCII.

Вот как выглядит сырой ClientHello в Python:

```python

import socket

import ssl

context = ssl.create_default_context()

sock = socket.create_connection(("example.com", 443))

ssock = context.wrap_socket(sock, server_hostname="example.com")

Перехватываем ClientHello через кастомный BIO

import io

class CaptureBIO(io.RawIOBase):

def __init__(self):

self.buffer = b""

def write(self, data):

self.buffer += data

return len(data)

bio = CaptureBIO()

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

ctx.wrap_bio(bio, bio, server_hostname="example.com")

print(f"ClientHello size: {len(bio.buffer)} bytes")

print(f"First 16 bytes: {bio.buffer[:16].hex()}")

```

Типичный ClientHello с SNI занимает 250-520 байт. SNI обычно попадает в первые 100-200 байт. Если разрезать первый сегмент на куски по 1-3 байта, SNI размазывается по нескольким пакетам. DPI, который смотрит только на первый пакет в потоке, видит 1-3 байта и ничего не находит.

Проблема в том, что некоторые DPI научились ждать. Они буферизуют первые N байт потока, скажем, 1500, и только потом анализируют. Против таких нужна более агрессивная стратегия — рандомизация размеров фрагментов, добавление задержек между ними, или комбинация с другими техниками.

IPv6 меняет правила игры

IPv6-трафик в большинстве сетей обрабатывается иначе, чем IPv4. Провайдеры часто ставят DPI только на IPv4-маршруты, а IPv6 пускают через более простые фильтры или вообще без инспекции. Причины разные: оборудование старое, IPv6-трафик составляет малую долю, инженеры не настроили зеркалирование.

Вторая причина — архитектура IPv6. Заголовок фиксированный, 40 байт, расширения идут цепочкой. DPI, заточенный под IPv4, часто не умеет корректно парсить цепочку extension headers. Если вставить Hop-by-Hop или Destination Options между основным заголовком и TCP, многие DPI просто теряют пакет из виду.

Третья — MTU. В IPv6 минимальный MTU 1280 байт, но реальный часто 1500. Фрагментация на уровне IPv6 делается через extension header Fragment. DPI, который не обрабатывает этот заголовок, видит только первый фрагмент, а остальные пропускает как неопознанные.

На практике это означает: одна и та же техника фрагментации на IPv4 может не работать, а на IPv6 — работать. Провайдер режет IPv4-DPI, а IPv6 идёт мимо. Прокси на lexic.ml использует это как один из слоёв — IPv6-транспорт плюс фрагментация на уровне приложения.

Комбинация с доменным фронтенгом

Чистая фрагментация работает не везде. Если DPI настроен агрессивно — буферизует поток, собирает сегменты, проверяет тайминги — фрагментация сама по себе не спасёт. Нужен второй слой: маскировка под легитимный домен.

Идея: ClientHello содержит SNI реального домена, который DPI пропускает. Например, cloudflare.com или любого крупного CDN. DPI видит легитимный SNI, пропускает соединение. Но трафик идёт не на cloudflare, а на прокси-сервер, который слушает на том же IP или использует SNI-роутинг.

Схема работает так: клиент отправляет ClientHello с SNI=allowed-domain.com. DPI пропускает. Прокси-сервер принимает соединение, читает SNI, понимает, что это его клиент (по дополнительному маркеру в расширениях или по IP), и начинает проксировать трафик. Если DPI проверяет сертификат — прокси отдаёт валидный сертификат для allowed-domain.com, полученный через Let's Encrypt с DNS-валидацией.

Комбинация фрагментации и доменного фронтенда даёт устойчивость: даже если DPI соберёт ClientHello, он увидит легитимный SNI. Даже если DPI проверит сертификат, он будет валидным.

Почему VPN проигрывают

VPN использует фиксированные протоколы: OpenVPN, WireGuard, IPsec. У каждого есть сигнатура. WireGuard — фиксированный размер handshake, 148 байт, характерные байты. OpenVPN — свой handshake с определёнными паттернами. DPI ловит их за миллисекунды.

Обфускация VPN существует: obfs4, Shadowsocks, VMess. Но это отдельные протоколы, которые тоже детектируются. Shadowsocks долгое время был невидим, пока DPI не научились энтропийному анализу. VMess с TLS-маскировкой работает, но требует правильной настройки, и при ошибке палится.

Главная проблема VPN — централизация. Один IP-адрес сервера, тысячи клиентов. DPI видит: много соединений на один IP, все с похожими паттернами. Блокировка по IP убивает весь сервис. Прокси с IPv6-пулом решает это иначе: каждый клиент получает свой IPv6-адрес из /64 или /48. Блокировка одного адреса не влияет на остальных.

Практическая настройка фрагментации

Вот рабочий пример на Python с использованием scapy для TCP-фрагментации ClientHello:

```python

from scapy.all import *

import random

def fragment_clienthello(target_ip, target_port, clienthello_data):

fragments = []

offset = 0

seq_base = random.randint(1000000, 4000000)

while offset < len(clienthello_data):

chunk_size = random.choice([1, 2, 3, 5, 8])

chunk = clienthello_data[offset:offset + chunk_size]

pkt = IP(dst=target_ip) / TCP(

sport=random.randint(30000, 60000),

dport=target_port,

flags="PA",

seq=seq_base + offset

) / Raw(load=chunk)

fragments.append(pkt)

offset += chunk_size

return fragments

packets = fragment_clienthello("2001:db8::1", 443, clienthello_bytes)

for pkt in packets:

send(pkt)

time.sleep(0.001)

```

Размеры фрагментов рандомизированы: 1, 2, 3, 5, 8 байт. Задержка 1 мс между пакетами. Это создаёт поток из 50-200 мелких пакетов. DPI, который смотрит на первые 5 пакетов, видит 5-20 байт мусора. Некоторые DPI начинают троттлить такие потоки — слишком много мелких пакетов, похоже на атаку. Поэтому важно не переусердствовать: 1-3 байта на фрагмент, но не больше 100 фрагментов.

Альтернатива — фрагментация только первых 100 байт. SNI обычно в этой зоне. Остальной ClientHello идёт нормальными пакетами.

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

Фрагментация добавляет задержку. Каждый фрагмент — отдельный пакет, а значит, отдельный round-trip на уровне TCP ACK. Если фрагментов 50 и RTT 20 мс, теоретическая задержка 1 секунда. На практике ACK приходят асинхронно, и задержка составляет 50-150 мс для 50 фрагментов.

MTU тоже играет роль. При MTU 1500 и 50 фрагментах по 3 байта общий объём данных 150 байт, но накладные расходы: 40 байт IPv6 + 20 байт TCP = 60 байт на пакет. Итого 50 * 60 = 3000 байт overhead. Это в 20 раз больше полезной нагрузки. Для ClientHello это нормально — он отправляется один раз. Для постоянного трафика такая схема не годится.

Вот таблица сравнения техник по устойчивости к разным типам DPI:

| Техника | Против packet-based DPI | Против stream-based DPI | Задержка (мс) | Overhead |

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

| TCP-фрагментация | Работает | Не работает | 50-150 | 20x |

| IP-фрагментация | Работает | Работает частично | 30-80 | 10x |

| SNI-маскировка | Работает | Работает | 0-20 | 1.1x |

| IPv6 + фрагментация | Работает | Работает | 60-180 | 25x |

| VPN (WireGuard) | Не работает | Не работает | 5-15 | 1.05x |

Цифры приблизительные, зависят от сети. Но порядок величин понятен.

Ошибки и грабли

Первая грабля — фрагментация после установки соединения. Некоторые пытаются фрагментировать весь трафик. Это убивает пропускную способность. Фрагментировать нужно только handshake, первые 1-3 пакета.

Вторая — игнорирование MSS. Если фрагменты больше MSS, они всё равно фрагментируются на уровне IP. Двойная фрагментация ломает некоторые DPI, но и некоторые клиенты. Лучше держать фрагменты меньше MSS.

Третья — отсутствие fallback. Если фрагментация не сработала, соединение зависает. Нужен таймаут и повторная попытка без фрагментации или с другой стратегией. Реализация: пробуем фрагментацию, ждём 3 секунды, если нет ответа — пробуем SNI-маскировку, потом прямое соединение.

Четвёртая — IPv6-адресация. Если провайдер выдаёт /128, а не /64, пул адресов не сделать. Нужен туннель до брокера, который даёт /48 или /56. Это отдельная головная боль.

Когда фрагментация не поможет

Если DPI использует ML-модели для классификации трафика, фрагментация может не сработать. Модель обучена на паттернах фрагментированных ClientHello и распознаёт их. Такие системы пока редки, но появляются.

Если провайдер блокирует по IP-адресу назначения, фрагментация бесполезна. Нужен пул адресов и ротация.

Если используется активное зондирование — DPI сам подключается к серверу и проверяет, что он отвечает. Прокси должен корректно отвечать на зонды, маскируясь под легитимный сервер.

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

Итог

TLS-фрагментация эксплуатирует архитектурное ограничение DPI: анализ на уровне пакетов вместо потока. IPv6 усиливает эффект, потому что DPI часто не настроен на IPv6 или не умеет парсить extension headers. Комбинация с SNI-маскировкой и пулом адресов даёт устойчивость к большинству известных DPI.

VPN проигрывает из-за фиксированных сигнатур и централизации. Прокси с фрагментацией и IPv6-пулом — гибче. Но требует правильной настройки: рандомизация фрагментов, таймауты, fallback-стратегии. Без этого — просто ещё один костыль, который палится на второй день.

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