multisplit — последовательная TCP-сегментация (zapret2 / nfqws2)
Файл: lua/zapret-antidpi.lua:483
nfqws1 эквивалент: --dpi-desync=multisplit
Сигнатура: function multisplit(ctx, desync)
multisplit — базовая функция TCP-сегментации в zapret2. Она берёт текущий payload (или reasm, или blob), разрезает его на несколько TCP-сегментов по заданным позициям и отправляет их последовательно (от первого к последнему). После успешной отправки выносит VERDICT_DROP, чтобы оригинальный пакет не ушёл.
Родственные функции: multidisorder (обратный порядок), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок), hostfakesplit (по hostname), tcpseg (диапазон), oob (urgent byte).
О чём заметка
multisplit— функция дурения DPI в Zapret 2 (движокnfqws2), которая режет исходящий TCP-пакет на несколько кусков, чтобы цензор не увидел в нём имя запрашиваемого сайта. Здесь разобраны все аргументы функции, маркеры позиций разреза, техникаseqovlи готовые примеры команд. Общий контракт--lua-desync-функций (кто и когда их вызывает) описан в desync, общий для всех техник порядок работы — в жизненный цикл desync-функции, устройство входной таблицыdesync— в структура desync и диссекта.
TL;DR
multisplitразрезает прикладные данные (HTTP-запрос, TLS ClientHello) на несколько TCP-сегментов, отправляет их по порядку, а перехваченный оригинал дропает. Сервер собирает поток обратно штатными средствами TCP — для него ничего не меняется.- Смысл приёма: DPI ищет имя сайта (
Host:в HTTP, SNI в TLS) сигнатурой внутри одного пакета. Если имя разорвано границей сегментов, а DPI не пересобирает TCP-поток — сигнатура не находится. - Где резать, задаёт аргумент
pos: либо числом («первые N байт уходят отдельным сегментом»), либо маркером, привязанным к структуре протокола (midsld— середина домена второго уровня). Маркеров можно перечислить несколько через запятую. seqovl=Nдополнительно приклеивает слева к первому сегменту N байт фальшивки и сдвигает sequence number назад: сервер эти байты отбросит как вышедшие за границу окна, а DPI их прочитает и может принять за начало запроса.- Типовой рабочий вариант для TLS:
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld. Это прямой аналог--dpi-desync=multisplit --dpi-desync-split-pos=1,midsldиз первой версии zapret. - Если открыли заметку впервые — читайте подряд до раздела «Практические примеры»; разбор кода вынесен в конец и нужен, только если вы правите Lua-функции сами.
Оглавление
- TL;DR
- Зачем нужен multisplit
- Быстрый старт
- Откуда берутся данные для нарезки
- Маркеры позиций (pos)
- seqovl — скрытый фейк внутри сегмента
- Полный список аргументов
- Порядок отправки сегментов
- Поведение при replay / reasm
- Автосегментация по MSS
- Нюансы и подводные камни
- Отличия от других функций сегментации
- Миграция с nfqws1
- Практические примеры
- Как это устроено внутри
- 📚 См. также
Зачем нужен multisplit
DPI анализирует TCP-поток, пытаясь собрать полный payload и найти в нём сигнатуры (hostname в HTTP, SNI в TLS). Если мы разрежем пакет на несколько TCP-сегментов, DPI может:
- Не собрать данные: некоторые DPI работают попакетно и не реассемблируют TCP
- Не найти сигнатуру: если разрез проходит через
Host:или SNI, ни в одном отдельном сегменте полного hostname нет - Принять фейк за реальные данные: с помощью
seqovlможно замешать ложную информацию, которую DPI проглотит, а сервер — нет
Сервер при этом корректно собирает поток — TCP-стек гарантирует это через sequence numbers.
Проще всего увидеть смысл на байтах. Браузер отправляет HTTP-запрос одним пакетом, и цензору достаточно найти в нём строку с именем сайта:
GET /watch HTTP/1.1\r\nHost: www.youtube.com\r\n...
^^^^^^^^^^^^^^^^^ сигнатура целиком в одном пакете
С --lua-desync=multisplit:pos=midsld тот же запрос уходит двумя TCP-сегментами с разрезом посередине домена второго уровня (youtube):
сегмент 1: GET /watch HTTP/1.1\r\nHost: www.you
сегмент 2: tube.com\r\n...
Строки www.youtube.com теперь нет ни в одном пакете — она появляется только после сборки потока. DPI, который сигнатуру ищет в границах отдельного пакета, её не найдёт. Сервер соберёт запрос как обычно и ответит нормально: для него это просто данные, пришедшие двумя порциями вместо одной.
Оговорка: приём рассчитан на DPI, который не пересобирает TCP-поток. Системы, умеющие реассемблирование, разрез сам по себе не обманет — поэтому multisplit обычно комбинируют с фейковыми пакетами и seqovl, а рабочую комбинацию подбирают перебором (см. verify-strategy).
multisplit — самый простой вариант: режем и шлём по порядку. Для обратного порядка есть multidisorder, для замешивания фейковых сегментов — fakedsplit/fakeddisorder.
Быстрый старт
Минимально (дефолты: pos=2 — первые 2 байта отдельным сегментом, payload=known, dir=out):
--lua-desync=multisplitТиповой TLS-разрез:
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsldTLS с seqovl:
--payload=tls_client_hello --lua-desync=multisplit:pos=1:seqovl=5:seqovl_pattern=0x1603030000HTTP с разрезом по hostname:
--payload=http_req --lua-desync=multisplit:pos=host,midsld,endhostОткуда берутся данные для нарезки
Прежде чем что-то резать, multisplit должна выбрать, какие именно данные она режет. Это делает всего одна строка, но за ней стоит цепочка приоритетов:
local data = blob_or_def(desync, desync.arg.blob) or desync.reasm_data or desync.dis.payloadLua-оператор or возвращает первый истинный (не nil и не false) операнд, поэтому строка читается как «сверху вниз, берём первое, что есть». Разберём три источника по порядку.
Первый приоритет — явный blob. Если в аргументах задан blob=имя, функция blob_or_def достаёт содержимое этого blob и режет именно его, полностью игнорируя реальный payload пакета. Это способ отправить произвольные данные вместо того, что реально шлёт приложение: заранее заготовленный фейковый ClientHello, модифицированный запрос, содержимое файла. Что такое blob и как их объявлять (inline-hex 0x..., @файл, именованные переменные) — см. отдельную заметку blob. Если blob= не задан, blob_or_def возвращает nil, и or идёт дальше.
Второй приоритет — реассемблированные данные. Если blob нет, но движок собрал многопакетный payload в desync.reasm_data (например, TLS ClientHello, растянувшийся на несколько TCP-сегментов), режется этот собранный буфер целиком. Благодаря этому разрез по midsld попадёт в реальную середину доменного имени, даже если само имя физически лежало во втором или третьем пакете исходного потока. Механику сборки и однократной отправки reasm разбирает раздел Поведение при replay / reasm.
Третий приоритет — payload текущего пакета. Если ни blob, ни reasm нет (обычный одиночный пакет, всё поместилось), берётся desync.dis.payload — тело именно этого перехваченного TCP-сегмента. Это дефолтный путь для большинства простых случаев.
Почему порядок именно такой и что из этого следует. Все дальнейшие аргументы — маркеры pos, длина seqovl, проверки — применяются к тем данным, которые реально выбраны на этом шаге, а не к «пакету вообще». Практическое следствие: если вы задали blob=myblob, то относительные маркеры вроде midsld или host сработают только при условии, что содержимое myblob — это валидный TLS- или HTTP-payload, который движок сумеет распознать (иначе desync.l7payload для blob окажется unknown и такие маркеры молча отвалятся, см. Важные нюансы pos).
Проще говоря
Функция сначала спрашивает: «мне явно подсунули blob? — режу его». Если нет: «есть собранный из кусков payload? — режу его». Если и его нет: «режу тело текущего пакета». Маркеры и seqovl всегда работают по тем данным, которые выиграли этот выбор.
Маркеры позиций (pos)
pos — главный аргумент multisplit. Определяет где внутри payload будет произведён разрез. Задаётся как строка со списком маркеров через запятую.
Маркер — это смещение от начала данных, считая с нуля: разрез происходит перед байтом с таким номером. Отсюда простое правило, которым удобно пользоваться на практике: pos=N означает «первые N байт уходят отдельным сегментом». pos=1 → первый сегмент длиной 1 байт, pos=5 → 5 байт, дефолтный pos=2 → 2 байта. Внутри Lua те же позиции хранятся как 1-based индексы строки (resolve_multi_pos прибавляет единицу), но в командной строке считать нужно от нуля.
Типы маркеров
| Тип | Описание | Пример |
|---|---|---|
| Абсолютный положительный | Смещение от начала payload, считая с 0. pos=N → первые N байт становятся отдельным сегментом | 1, 5, 100 |
| Абсолютный отрицательный | Смещение от конца payload. -1 = последний байт (он уйдёт отдельным сегментом) | -1, -10, -50 |
| Относительный | Логическая позиция внутри распознанного payload. Привязана к структуре протокола | midsld, host, sniext |
Относительные маркеры
| Маркер | Описание | Для каких payload |
|---|---|---|
method | Начало HTTP-метода (GET, POST, HEAD, PUT и т.д.). Обычно позиция 0, но может стать 1-2 при использовании http_methodeol | http_req |
host | Первый байт имени хоста (Host: в HTTP, SNI в TLS) | http_req, tls_client_hello |
endhost | Байт, следующий за последним байтом имени хоста. Т.е. host..endhost-1 = полный hostname | http_req, tls_client_hello |
sld | Первый байт домена второго уровня (SLD). Для www.example.com — это e в example | http_req, tls_client_hello |
endsld | Байт, следующий за последним байтом SLD. Для example.com — это . после example | http_req, tls_client_hello |
midsld | Середина SLD (самый популярный маркер). Для example (7 символов) — позиция 3-го или 4-го символа | http_req, tls_client_hello |
sniext | Начало поля данных SNI extension в TLS ClientHello. Extension состоит из type (2 байта) + length (2 байта) + данные — sniext указывает на начало данных | tls_client_hello |
extlen | Поле длины всех TLS extensions | tls_client_hello |
Арифметика маркеров
К любому маркеру можно прибавить (+) или вычесть (-) целое число:
midsld+1 — один байт ПОСЛЕ середины SLD
midsld-1 — один байт ДО середины SLD
endhost-2 — два байта до конца hostname
method+2 — два байта после начала метода (разрежет "GET " после "GE")
sniext+1 — один байт после начала SNI extension data
host+3 — три байта после начала hostname
-1 — последний байт payload (абсолютный, не относительный)
Арифметика работает и с абсолютными маркерами, хотя это избыточно (5+3 = 8).
Пример списка маркеров
pos=100,midsld,sniext+1,endhost-2,-10
Здесь 5 маркеров → payload разрежется максимум на 6 частей (если все маркеры успешно разрешатся и дадут различные позиции).
Как маркеры разрешаются в коде
Внутри multisplit вызывается:
local pos = resolve_multi_pos(data, desync.l7payload, spos)Функция resolve_multi_pos:
- Разбивает строку
sposпо запятым - Для каждого маркера вызывает
resolve_pos(blob, l7payload_type, marker) - Если маркер не может быть разрешён (например,
midsldдляunknownpayload) — он молча пропускается - Результаты дедуплицируются и сортируются
- Возвращается массив уникальных абсолютных позиций, уже переведённых в 1-based индексы Lua (маркер
1превращается в позицию2, маркер0— в позицию1)
Затем вызывается:
delete_pos_1(pos) -- удалить Lua-позицию 1, то есть маркер 0Важные нюансы pos
- Разрез в самом начале данных отбрасывается.
delete_pos_1удаляет Lua-позицию 1, которая соответствует маркеру0. Резать «перед первым байтом» бессмысленно: первый сегмент получился бы пустым. Практическое следствие —pos=0не сработает, аpos=1(первый байт отдельным сегментом) работает и используется очень часто, в том числе в связке сseqovl - Реальный случай, когда маркер выпадает по этой причине —
pos=methodдля HTTP. Метод (GET,POST) обычно лежит на смещении 0, поэтому маркер разрешится в позицию 1 и будет удалён; разреза не произойдёт вовсе. Чтобы резать внутри метода, нужен сдвиг:pos=method+2 - Дублирующиеся позиции объединяются.
pos=5,5,5=pos=5 - Неразрешимые маркеры пропускаются. Если
midsldне разрешается (payload = unknown), он просто исчезает из списка. Если все маркеры не разрешились — multisplit ничего не делает (логирует “no valid split positions”) - Позиции сортируются. Независимо от порядка записи,
pos=100,5,50будет обработано как5,50,100 - По умолчанию
pos=2. Еслиposне задан, payload делится на 2 части: первые 2 байта отдельным сегментом, весь остаток — вторым. Тот же дефолт был у--dpi-desync-split-posв nfqws1
seqovl — скрытый фейк внутри сегмента
seqovl (Sequence Overlap) — техника скрытого замешивания фейковых данных в реальный TCP-сегмент через манипуляцию TCP sequence number. В multisplit seqovl применяется только к первому отправляемому сегменту.
Принцип работы seqovl
Чтобы понять seqovl, нужно вспомнить, как TCP-получатель решает, какие байты принять. У каждого байта в TCP-потоке есть свой sequence number — сквозной номер позиции в потоке. Приёмная сторона держит «окно» (receive window) и принимает только байты, чей номер попадает в него, начиная с номера следующего ожидаемого байта. Всё, что левее этой границы (номера, которые сторона уже получила или ещё не ждёт с этого места), молча отбрасывается как дубликат или мусор — это штатное поведение TCP-стека, а не ошибка.
seqovl эксплуатирует ровно этот механизм. Возьмём для примера первый сегмент, который в норме начинался бы с sequence number 1000 и нёс реальные данные. Без seqovl сегмент уходит как есть: seq = 1000, тело — реальная первая часть, сервер принимает её целиком, потому что 1000 — как раз следующий ожидаемый им байт.
С seqovl=10 функция делает две вещи одновременно. Во-первых, дописывает слева к телу сегмента 10 байт паттерна-фейка (seqovl_pattern, по умолчанию нули). Во-вторых, уменьшает объявленный в заголовке sequence number на те же 10 — с 1000 до 990. В результате в сеть уходит один-единственный, совершенно обычный с виду TCP-сегмент: seq = 990, тело = [10 байт паттерна][реальная первая часть].
Дальше этот сегмент по-разному воспринимают две стороны.
Что видит DPI. Для системы глубокого анализа это цельный TCP-сегмент, начинающийся с sequence 990. Многие DPI разбирают содержимое сегмента «как есть», от его начала, не сверяясь с тем, какие именно байты примет получатель. Поэтому DPI анализирует весь блок — вместе с 10 байтами паттерна. Если в паттерн подложен, например, фрагмент, похожий на начало легитимного TLS-record или ложный SNI, DPI может принять фейк за реальные данные и вынести решение по нему, а не по настоящему домену.
Что видит сервер (TCP-стек получателя). Следующий ожидаемый им байт по-прежнему имеет номер 1000. Байты с номерами 990–999 (те самые 10 байт паттерна) оказываются левее границы окна — сервер отбрасывает их автоматически, как повтор/устаревшие данные. А байты, начиная с 1000 (реальная первая часть), попадают в окно и принимаются. В итоге сервер собирает ровно исходный поток, будто никакого паттерна и не было.
Наглядно расклад по одному sequence-байту такой:
| Диапазон seq | Содержимое | DPI | Сервер (TCP-стек) |
|---|---|---|---|
| 990–999 | PATTERN (10 байт фейка) | анализирует вместе со всем сегментом | отбрасывает — левее границы окна |
| с 1000 | реальная первая часть | анализирует | принимает |
Ключевая идея: одни и те же 10 байт для DPI — «полноценные данные для анализа», а для сервера — «мусор за левым краем окна». Расхождение в трактовке и есть рабочий механизм обхода.
Зачем seqovl лучше обычного fooling
| Критерий | Обычный fooling (TTL, badseq, md5sig) | seqovl |
|---|---|---|
| Заголовки | Модифицируются (TTL, seq, ack, md5) | Не модифицируются — пакет выглядит полностью легитимным |
| Обнаружение | DPI может детектировать подозрительные заголовки | DPI видит “честный” сегмент с правильными заголовками |
| Механизм отбрасывания | Сервер отбрасывает весь пакет из-за невалидных заголовков | Сервер отбрасывает только часть, выходящую за TCP window |
| Надёжность | Зависит от поведения конкретного стека | Основан на фундаментальном свойстве TCP |
Вывод: seqovl — средство создания скрытых фейков, не требующее fooling. Это его ключевое преимущество.
seqovl_pattern
Паттерн, которым заполняется seqovl-область (N байт слева от реальных данных). По умолчанию — 0x00 (нули).
В multisplit seqovl_pattern — это имя blob. Паттерн повторяется до нужной длины seqovl.
# Inline hex blob (маскировка под начало TLS record)
--lua-desync=multisplit:pos=1:seqovl=5:seqovl_pattern=0x1603030000
# Предзагруженный blob
--blob=tlspat:0x1603030100 \
--lua-desync=multisplit:pos=1:seqovl=8:seqovl_pattern=tlspatЕсли optional задан и blob seqovl_pattern отсутствует — используется нулевой паттерн (операция не отменяется).
Важно в multisplit: seqovl — только число, маркеры не поддерживаются (в отличие от multidisorder и fakeddisorder, где seqovl может быть маркером).
Полный список аргументов
Формат вызова:
--lua-desync=multisplit[:arg1[=val1][:arg2[=val2]]...]
Все val приходят в Lua как строки. Если =val не указан, значение = пустая строка "" (в Lua это truthy), поэтому флаги пишутся просто как :optional, :nodrop, :tcp_ts_up.
A) Собственные аргументы multisplit
pos
- Формат:
pos=<marker[,marker2,...]> - Тип: строка со списком маркеров через запятую
- По умолчанию:
"2" - Описание: Точки разреза, смещения считаются от начала данных с нуля. N маркеров → до N+1 сегментов
- Примеры:
pos=2— разрез после 2-го байта (дефолт)pos=1— первый байт уходит отдельным сегментомpos=midsld— разрез посередине SLDpos=1,midsld— два разреза: после 1-го байта и посередине SLD → 3 сегментаpos=host,midsld,endhost-2,-10— четыре разреза → до 5 сегментовpos=method+2— после первых 2 символов HTTP-метода (самpos=methodне сработает, см. Важные нюансы pos)
seqovl
- Формат:
seqovl=N(где N > 0) - Тип: только число (маркеры не поддерживаются — в отличие от multidisorder)
- По умолчанию: не задан (нет seqovl)
- Описание: Применяется только к первому отправляемому сегменту. К данным первого сегмента слева добавляется N байт
seqovl_pattern, а TCPth_seqуменьшается на N. Сервер отбросит левую часть, DPI — может не отбросить - Примеры:
seqovl=5— 5 байт фейка слеваseqovl=13— 13 байт фейка слеваseqovl=10000— 10000 байт (если превысит MSS — автосегментация разобьёт на несколько TCP-сегментов)
seqovl_pattern
- Формат:
seqovl_pattern=<blobName> - Тип: имя blob-переменной
- По умолчанию: один байт
0x00, повторяемый до длиныseqovl - Описание: Данные для заполнения seqovl-области. Blob повторяется функцией
pattern()до нужного размера - Поведение с
optional: еслиoptionalзадан и blob отсутствует — используется нулевой паттерн, seqovl не отменяется - Примеры:
seqovl_pattern=0x1603030000— inline hex (маскировка под TLS)seqovl_pattern=my_pattern_blob— предзагруженный blob
blob
- Формат:
blob=<blobName> - Тип: имя blob-переменной
- По умолчанию: не задан
- Описание: Заменить текущий payload/reasm на указанный blob и резать/слать его. Используется для отправки произвольных данных (фейковых payload, модифицированных ClientHello и т.д.)
- Примеры:
blob=fake_default_tls— стандартный TLS-фейкblob=0xDEADBEEF— inline hexblob=my_custom_ch— предзагруженный blob
optional
- Формат:
optional(флаг, без значения) - Описание: Мягкий режим:
- Если задан
blob=...и blob отсутствует → multisplit ничего не делает (тихий skip, без ошибок) - Если задан
seqovl_pattern=...и blob отсутствует → используется нулевой паттерн (seqovl не отменяется)
- Если задан
- Использование: защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией)
nodrop
- Формат:
nodrop(флаг, без значения) - Описание: После успешной отправки сегментов не выносить
VERDICT_DROP(вместо этого вернутьVERDICT_PASS). Это означает, что оригинальный пакет тоже будет отправлен (наряду с нарезанными сегментами) - Использование: для отладки, для отправки произвольных данных без блокировки оригинала
- Предупреждение: в боевых профилях
nodropобычно нежелателен — оригинал ещё раз уйдёт, что создаст дублирование и может ухудшить обход
B) Standard direction
| Параметр | Значения | По умолчанию |
|---|---|---|
dir | in, out, any | out |
Фильтр по направлению пакета. multisplit по умолчанию работает только с исходящими (out).
dir=out— только исходящие (от клиента к серверу)dir=in— только входящие (от сервера к клиенту)dir=any— оба направления
При первом вызове с указанным dir функция делает direction_cutoff_opposite — отсекает себя от противоположного направления.
C) Standard payload
| Параметр | Значения | По умолчанию |
|---|---|---|
payload | список типов через запятую | known |
Фильтр по типу payload на уровне Lua. Это дополнительный фильтр к --payload=... на уровне профиля.
payload=known— только распознанные протоколы (http_req,tls_client_hello,quic_initialи т.д.)payload=all— любой payload, включаяunknownpayload=tls_client_hello,http_req— конкретные типыpayload=~unknown— инверсия: всё кроме unknown
Важно: лучше ставить --payload=... на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.
D) Standard fooling
Модификации L3/L4 заголовков. В multisplit применяются ко всем отправляемым сегментам (в отличие от fakedsplit, где fooling идёт только на фейки).
| Параметр | Описание | Пример |
|---|---|---|
ip_ttl=N | Установить IPv4 TTL | ip_ttl=6 |
ip6_ttl=N | Установить IPv6 Hop Limit | ip6_ttl=6 |
ip_autottl=delta,min-max | Автоматический TTL (delta от серверного TTL) | ip_autottl=-2,40-64 |
ip6_autottl=delta,min-max | Аналогично для IPv6 | ip6_autottl=-2,40-64 |
ip6_hopbyhop[=HEX] | Вставить extension header hop-by-hop (по умолчанию 6 нулей) | ip6_hopbyhop |
ip6_hopbyhop2[=HEX] | Второй hop-by-hop header | ip6_hopbyhop2 |
ip6_destopt[=HEX] | Destination options header | ip6_destopt |
ip6_destopt2[=HEX] | Второй destination options | ip6_destopt2 |
ip6_routing[=HEX] | Routing header | ip6_routing |
ip6_ah[=HEX] | Authentication header | ip6_ah |
tcp_seq=N | Сместить TCP sequence (+ или -) | tcp_seq=-10000 |
tcp_ack=N | Сместить TCP ack (+ или -) | tcp_ack=-66000 |
tcp_ts=N | Сместить TCP timestamp | tcp_ts=-100 |
tcp_md5[=HEX] | Добавить TCP MD5 option (16 байт; по умолчанию случайные) | tcp_md5 |
tcp_flags_set=LIST | Установить TCP-флаги | tcp_flags_set=FIN,PUSH |
tcp_flags_unset=LIST | Снять TCP-флаги | tcp_flags_unset=ACK |
tcp_ts_up | Поднять TCP timestamp option в начало заголовка | tcp_ts_up |
tcp_nop_del | Удалить все TCP NOP опции | tcp_nop_del |
fool=<func> | Кастомная Lua-функция fooling | fool=my_fooler |
Заметка про tcp_ts_up: На Linux-серверах пакеты с инвалидным ACK стабильно отбрасываются только если TCP timestamp option идёт первой в заголовке. tcp_ts_up перемещает её в начало, обеспечивая корректную работу badseq-fooling.
E) Standard ipid
| Параметр | Описание | По умолчанию |
|---|---|---|
ip_id=seq | Последовательные IP ID | seq |
ip_id=rnd | Случайные IP ID | — |
ip_id=zero | Нулевые IP ID | — |
ip_id=none | Не менять IP ID | — |
ip_id_conn | Сквозная нумерация IP ID в рамках соединения (требует tracking) | — |
ip_id применяется к каждому отправляемому сегменту (включая под-сегменты при MSS-сегментации).
F) Standard ipfrag
IP-фрагментация поверх TCP-сегментации. Каждый TCP-сегмент дополнительно фрагментируется на уровне IP.
| Параметр | Описание | По умолчанию |
|---|---|---|
ipfrag[=func] | Включить IP-фрагментацию. Если без значения → ipfrag2 | — |
ipfrag_disorder | Отправить IP-фрагменты в обратном порядке | — |
ipfrag_pos_tcp=N | Позиция фрагментации TCP (кратно 8) | 32 |
ipfrag_pos_udp=N | Позиция фрагментации UDP (кратно 8). Для multisplit бесполезно — он только TCP | 8 |
ipfrag_next=N | IPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы) | — |
G) Standard reconstruct
| Параметр | Описание |
|---|---|
badsum | Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
H) Standard rawsend
| Параметр | Описание |
|---|---|
repeats=N | Отправить каждый сегмент N раз (идентичные повторы) |
ifout=<iface> | Интерфейс для отправки (по умолчанию определяется автоматически) |
fwmark=N | Firewall mark (только Linux, nftables/iptables) |
Порядок отправки сегментов
multisplit всегда отправляет сегменты последовательно — от первого к последнему (в порядке возрастания TCP sequence).
Пример с 3 позициями разреза
Возьмём payload длиной 600 байт и три позиции разреза: pos=100,300,450. Они делят данные на четыре куска (0–99, 100–299, 300–449, 450–599), и multisplit отправляет их четырьмя TCP-сегментами по возрастанию sequence number. Sequence каждого сегмента равен смещению его первого байта в исходном payload, поэтому сервер собирает поток обратно без пропусков:
| Сегмент | Байты payload | Длина | Смещение (seq) |
|---|---|---|---|
| 1 | 0–99 | 100 | 0 |
| 2 | 100–299 | 200 | 100 |
| 3 | 300–449 | 150 | 300 |
| 4 | 450–599 | 150 | 450 |
Пример с seqovl=10
Тот же payload 600 байт, но pos=100 (один разрез, два сегмента) и seqovl=10. К первому сегменту слева дописываются 10 байт паттерна, а его sequence сдвигается на те же 10 в минус — поэтому паттерн уходит за левую границу окна и сервер его отбрасывает, приняв только реальную часть с seq 0:
| Сегмент | Содержимое | Длина | Смещение (seq) | Что с ним на сервере |
|---|---|---|---|---|
| 1 | 10 байт паттерна + байты 0–99 | 110 | −10 | паттерн отброшен, реальная часть принята |
| 2 | байты 100–599 | 500 | 100 | принят целиком |
Поведение при replay / reasm
Многопакетный запрос (например, большой TLS ClientHello с post-quantum ключами, не влезающий в один TCP-сегмент) nfqws2 придерживает, собирает в буфер reasm_data и затем перепроигрывает через desync-функции. multisplit использует штатную развилку перепроигрывания из общего скелета desync-функций (жизненный цикл desync-функции, стадия 6) без изменений: режет и отправляет весь собранный буфер на первой части, а остальные части дропает, потому что их содержимое уже ушло в составе нарезки.
Специфика именно multisplit тут одна: маркеры разрешаются по всему reasm_data, а не по отдельному пакету. Поэтому разрез по midsld попадёт в реальную середину домена, даже если имя хоста физически лежало во втором или третьем пакете исходного потока.
Если отправка сорвалась (rawsend вернул false), флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть — данные не теряются.
Автосегментация по MSS
О размерах TCP-сегментов думать не нужно. Отправкой занимается rawsend_payload_segmented из zapret-lib.lua, и она сама доводит куски до допустимого размера:
- MSS берётся из
desync.tcp_mss— движок отслеживает его для каждого TCP-соединения - Если кусок вместе с заголовками превышает MSS — он дополнительно режется по MSS
- Каждый под-сегмент отправляется с корректным TCP sequence (
th_seqувеличивается на длину предыдущего)
Есть тонкость с тем, что применяется к под-сегментам, а что нет: fooling применяется один раз к исходному диссекту, то есть достаётся всем под-сегментам одинаково, а политика ip_id применяется отдельно к каждому под-сегменту.
Пример: seqovl=10000 не вызовет ошибку. Функция отправит несколько TCP-сегментов подряд, начиная со сдвига −10000 относительно текущего sequence: сначала пойдут полные сегменты паттерна, а в последнем к остатку паттерна будет пристыковано начало реальных данных.
Нюансы и подводные камни
1. Работает только с TCP — и почему
multisplit обрабатывает исключительно TCP. Первая же стадия общего скелета отсеивает всё остальное: на не-TCP пакете функция делает instance_cutoff и навсегда отключается от этого потока, чтобы не тратить CPU (исключение — связанный ICMP; механика описана в жизненный цикл desync-функции).
Ограничение не техническая недоработка, а следствие сути приёма — он опирается на два свойства, которые есть у TCP и отсутствуют у UDP.
TCP — это поток байтов со сквозной нумерацией, UDP — набор независимых датаграмм. В TCP у каждого байта есть свой sequence number, и получатель обязан по этим номерам собрать поток в исходном порядке — это гарантия протокола. Поэтому один payload можно разрезать на несколько сегментов, разослать их (по порядку — multisplit, вперемешку — multidisorder), и сервер прозрачно склеит всё обратно: ему безразлично, пришли данные одним пакетом или пятью. В UDP такого нет — каждая датаграмма самодостаточна, никакой пересборки «потока» по номерам байт на приёмной стороне не происходит. Разрезать UDP-payload на две датаграммы бессмысленно: принимающее приложение получит два отдельных куска и не обязано (и не будет) склеивать их так, как задумано. Резать в TCP-стиле там просто нечего.
seqovl тем более завязан на TCP. Трюк seqovl держится на конкретной детали TCP-стека: получатель отбрасывает байты, чей sequence number ушёл левее границы окна приёма. Дописываем фейк слева, сдвигаем seq в минус — сервер выкидывает фейк, а DPI его анализирует. У UDP нет ни sequence number, ни окна приёма, ни понятия «байты левее границы» — эксплуатировать нечего.
Что делают с UDP вместо этого. UDP-протоколы (в первую очередь QUIC) обходят другими desync-функциями: fake (фейковая датаграмма перед реальной), IP-фрагментация ipfrag (режется на уровне IP, а не TCP), манипуляции с длиной и прочее. То есть UDP работают на других уровнях, а TCP-сегментация как класс к нему неприменима. Обзор всех функций дурения — в desync.
Проще говоря
multisplitрежет один payload на куски и рассчитывает, что получатель склеит их обратно «по номерам». Такое склеивание умеет только TCP. UDP обрабатывает каждый пакет сам по себе и склеивать по номерам не умеет — значит, и резать в этом стиле нечего.
2. Разрез в самом начале данных отбрасывается
delete_pos_1(pos) убирает из списка Lua-позицию 1 — она соответствует маркеру 0, то есть разрезу перед первым байтом, от которого первый сегмент вышел бы пустым. Если после этой чистки не осталось ни одной позиции, multisplit не делает ничего и пакет уходит целым.
Путаться тут легко из-за разной нумерации: в командной строке маркеры считаются от нуля, внутри Lua — от единицы. Поэтому под удаление попадает не pos=1, а pos=0. Самая частая реальная жертва — pos=method для HTTP: метод обычно лежит на смещении 0, маркер разрешается в удаляемую позицию, и разреза не происходит. Рабочий вариант — pos=method+2.
3. Все маркеры могут не разрешиться
Если вы указали pos=midsld,sniext для HTTP-payload, оба маркера (специфичные для TLS) не разрешатся. Multisplit напишет в лог “no valid split positions” и ничего не сделает.
4. nodrop создаёт дублирование
С nodrop multisplit отправляет нарезанные сегменты И пропускает оригинальный пакет. Сервер получит данные дважды. Используйте nodrop только для отладки или когда это осознанно нужно.
5. seqovl=10000 не вызовет ошибку
В отличие от nfqws1, где большие значения seqovl вызывали ошибку, nfqws2 автоматически сегментирует по MSS. Большой seqovl просто создаст много под-сегментов.
6. Fooling применяется ко ВСЕМ сегментам
В multisplit все сегменты — а других, кроме реальных, функция и не шлёт — получают fooling целиком. У fakedsplit и fakeddisorder иначе: там fooling в полном объёме достаётся только фейкам, а к реальным частям применяется единственная опция tcp_ts_up.
Практическое следствие: портящие пакет опции в multisplit вредны. Если задать tcp_ack=-66000, все сегменты получат инвалидный ack, сервер их отбросит — и соединение просто не состоится. Осмысленны здесь лишь те опции, которые не мешают серверу принять данные: tcp_ts_up, политика ip_id, IPv6 extension headers. Подробнее про сами опции — в ts-and-fooling.
7. Порядок инстансов важен
Если перед multisplit стоит инстанс, модифицирующий диссект (например pktmod), то multisplit порежет уже изменённые данные — порядок --lua-desync в профиле определяет порядок применения. Разбор того, как аргументы и инстансы выстраиваются в цепочку, — в последовательность аргументов и desync.
Частое заблуждение: VERDICT_DROP якобы обрывает цепочку. Это не так. Движок проходит все инстансы профиля и агрегирует их вердикты (VERDICT_MODIFY перебивает VERDICT_PASS, VERDICT_DROP перебивает оба). Инстансы, стоящие после multisplit, будут вызваны как обычно и увидят тот же самый диссект; дроп означает лишь, что оригинальный пакет в итоге не будет отправлен. Оборвать выполнение может только явный вызов execution_plan_cancel, которого в multisplit нет.
Отличия от других функций сегментации
| Аспект | multisplit | multidisorder | fakedsplit | fakeddisorder |
|---|---|---|---|---|
| Количество позиций | Список (любое кол-во) | Список (любое кол-во) | Одна | Одна |
| Порядок отправки | Прямой (1→2→3) | Обратный (3→2→1) | Прямой | Обратный |
| Фейковые сегменты | Нет | Нет | Да (до 4 шт.) | Да (до 4 шт.) |
| seqovl тип | Только число | Маркер | Только число | Маркер |
| seqovl к какому сегменту | 1-й | 2-й (предпоследний) | 1-й реальный | 2-й реальный |
| Fooling к | Всем сегментам | Всем сегментам | Фейкам полностью, реальным частям — только tcp_ts_up | Фейкам полностью, реальным частям — только tcp_ts_up |
| ipfrag | Да | Да | Нет | Нет |
Миграция с nfqws1
Соответствие параметров
| nfqws1 | nfqws2 |
|---|---|
--dpi-desync=multisplit | --lua-desync=multisplit |
--dpi-desync-split-pos=midsld | :pos=midsld |
--dpi-desync-split-pos=1,midsld | :pos=1,midsld |
--dpi-desync-split-seqovl=5 | :seqovl=5 |
--dpi-desync-split-seqovl-pattern=0x1603030000 | :seqovl_pattern=0x1603030000 |
--dpi-desync-any-protocol | Не нужно; или payload=all в инстансе |
Пример полной миграции
# nfqws1:
nfqws --dpi-desync=fake,multisplit \
--dpi-desync-fooling=md5sig \
--dpi-desync-split-pos=1,midsld \
--dpi-desync-split-seqovl=5 \
--dpi-desync-split-seqovl-pattern=0x1603030000 \
--dpi-desync-fake-tls-mod=rnd,rndsni,dupsid
# nfqws2 (эквивалент):
nfqws2 \
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--payload=http_req \
--lua-desync=fake:blob=fake_default_http:tcp_md5 \
--payload=tls_client_hello,http_req \
--lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000# nfqws1:
nfqws --dpi-desync=syndata,multisplit --dpi-desync-split-pos=midsld --wssize 1:6
# nfqws2 (порядок инстансов важен!):
nfqws2 \
--lua-desync=wssize:wsize=1:scale=6 \
--lua-desync=syndata \
--lua-desync=multisplit:pos=midsldПрактические примеры
Минимальный (дефолт: pos=2, dir=out, payload=known)
--lua-desync=multisplitРазрезает payload после 2-го байта → 2 сегмента: первые 2 байта и весь остаток.
HTTP: разрез после метода
--payload=http_req --lua-desync=multisplit:pos=method+2Для GET /path... разрежет после GE → DPI не увидит полный метод.
HTTP: несколько разрезов вокруг hostname
--payload=http_req --lua-desync=multisplit:pos=host,midsld,endhostРазрезает: до hostname | первая половина | вторая половина | после hostname → 4 сегмента.
TLS: разрез посередине SNI
--payload=tls_client_hello --lua-desync=multisplit:pos=midsldSNI разрезан пополам — ни в одном сегменте нет полного домена.
TLS: два разреза + seqovl
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x16030300003 сегмента: первый с 5-байтовым TLS-фейком слева (DPI может принять за начало TLS record).
Произвольный blob вместо payload
--blob=mydata:@custom_payload.bin \
--lua-desync=multisplit:blob=mydata:pos=10,100,-20Режет и отправляет произвольные данные из файла вместо реального payload.
Защита от отсутствующего blob
--lua-desync=multisplit:blob=maybe_missing:optional:pos=2Если blob не существует — тихий пропуск, без ошибок и без VERDICT_DROP.
Отладка: не блокировать оригинал
--payload=http_req --lua-desync=multisplit:pos=method+2:nodropОтправляет нарезанные сегменты И пропускает оригинальный пакет (для экспериментов).
С TCP timestamp + IP ID
--payload=tls_client_hello --lua-desync=multisplit:pos=1:tcp_ts_up:ip_id=seq:ip_id_connПовторы отправки
--payload=tls_client_hello --lua-desync=multisplit:pos=1:repeats=2Каждый сегмент отправляется 2 раза. Повторы бинарно идентичны — repeats не трогает содержимое пакета, в том числе ip_id. Если нужны разные ip_id, repeats не подойдёт.
IP-фрагментация поверх TCP-сегментации
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32Каждый TCP-сегмент дополнительно фрагментируется на IP-уровне в обратном порядке.
Комбинация: fake → multisplit
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000Сначала отправляется фейковый TLS ClientHello (с fooling), затем реальный — нарезанный на 3 сегмента с seqovl.
Боевой пример для YouTube
--filter-tcp=443 --hostlist=youtube.txt \
--lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
--lua-desync=multisplit:pos=1,midsld11 фейков подряд + реальный payload разрезан на 3 части.
Как это устроено внутри
Дальше идёт разбор кода функции. Для настройки обхода он не нужен — практическая часть закончилась выше. Читать имеет смысл, если вы правите Lua-функции zapret2 или пишете свои.
Место в общем скелете
Тело multisplit построено по тому же шаблону, что и все остальные функции дурения: восемь стадий от отсева чужого транспорта до вердикта. Разобраны они один раз в жизненный цикл desync-функции — здесь только отклонения:
| Стадия общего скелета | Что делает multisplit |
|---|---|
| 1. Отсев транспорта | только TCP; на не-TCP делает instance_cutoff (кроме связанного ICMP) |
| 2. Направление | dir=out по умолчанию, отключается от входящего |
| 3. Аргументы | обязательных нет; optional + отсутствующий blob → тихий выход |
| 4. Данные | штатная цепочка blob → reasm → payload |
| 5. Гварды | штатные #data>0, direction_check, payload_check (по умолчанию known) |
| 6. replay | штатная развилка: нарезка на первой части, дроп остальных |
| 7. Своя техника | разрешение списка маркеров, нарезка на части, seqovl на первом сегменте, отправка по возрастанию sequence — см. ниже |
| 8. Вердикт | VERDICT_DROP после успешной отправки, VERDICT_PASS при nodrop или сбое rawsend |
Из общего скелета полезно помнить две вещи, о которых спотыкаются чаще всего. Первая: работу функция делает не через return, а вызовами rawsend_payload_segmented — к моменту возврата сегменты уже улетели в сеть, а возвращаемое значение решает судьбу только перехваченного оригинала. Вторая: VERDICT_DROP не обрывает цепочку инстансов, вердикты всех инстансов профиля агрегируются (см. нюанс 7).
Стадия 7: нарезка и отправка
Ниже — код собственно техники (lua/zapret-antidpi.lua:483). Всё, что до него, — общий скелет.
Разрешение позиций разреза. Строку маркеров (desync.arg.pos или дефолт "2") превращаем в массив числовых позиций, затем убираем Lua-позицию 1 — она соответствует маркеру 0, разрезу перед первым байтом:
local pos = resolve_multi_pos(data, desync.l7payload, spos)
delete_pos_1(pos) -- cannot split at the first byteЦикл нарезки и отправки. Если после чистки осталась хоть одна позиция, идём по частям. Для части i начало — pos[i] (для нулевой части — 1), конец — pos[i+1]-1 (для последней — весь остаток #data). Вырезаем кусок, для первой части (i==0) при заданном seqovl дописываем слева паттерн-фейк и отправляем сегмент со сдвинутым sequence number:
for i=0,#pos do
local pos_start = pos[i] or 1
local pos_end = i<#pos and pos[i+1]-1 or #data
local part = string.sub(data,pos_start,pos_end)
local seqovl=0
if i==0 and desync.arg.seqovl and tonumber(desync.arg.seqovl)>0 then
seqovl = tonumber(desync.arg.seqovl)
local pat="\x00"
if desync.arg.seqovl_pattern then
-- ... взять blob или нулевой паттерн ...
pat = blob(desync,desync.arg.seqovl_pattern)
end
part = pattern(pat,1,seqovl)..part
end
if not rawsend_payload_segmented(desync,part,pos_start-1-seqovl) then
return VERDICT_PASS
end
endТретий аргумент rawsend_payload_segmented — это сдвиг sequence number: pos_start-1 переводит 1-based позицию Lua обратно в 0-based смещение байта в payload (то есть даёт правильный seq для сегмента), а вычитание seqovl уводит первый сегмент влево на длину фейка. Если rawsend вернул false — цикл прерывается и оригинал пропускается (VERDICT_PASS), чтобы данные не потерялись.
После успешной отправки всех частей ставится флаг «реасм уже отправлен» и выносится вердикт — это снова общий скелет:
replay_drop_set(desync)
return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROPПсевдокод алгоритма
Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в жизненный цикл desync-функции:
function multisplit(ctx, desync)
-- 1. Проверка: только TCP
if not desync.dis.tcp then cutoff; return end
-- 2. Cutoff противоположного направления
direction_cutoff_opposite(ctx, desync)
-- 3. Проверка optional blob
if optional and blob specified and blob not exists then return end
-- 4. Выбор данных
data = blob_or_def(blob) or reasm_data or dis.payload
-- 5. Проверки: данные не пусты, направление OK, payload OK
if #data > 0 and direction_check() and payload_check() then
-- 6. Только первый replay
if replay_first() then
-- 7. Разрешение маркеров
pos = resolve_multi_pos(data, l7payload, pos_arg or "2")
delete_pos_1(pos) -- удалить Lua-позицию 1 (маркер 0)
if #pos > 0 then
-- 8. Цикл по частям (i=0 до #pos)
for i = 0, #pos do
pos_start = pos[i] or 1
pos_end = (i < #pos) and pos[i+1]-1 or #data
part = data:sub(pos_start, pos_end)
-- 9. seqovl для первого сегмента
seqovl = 0
if i == 0 and arg.seqovl > 0 then
seqovl = tonumber(arg.seqovl)
pat = seqovl_pattern_blob or "\x00"
part = pattern(pat, 1, seqovl) .. part
end
-- 10. Отправка с автосегментацией
rawsend_payload_segmented(part, pos_start - 1 - seqovl)
end
-- 11. Пометить как отправленное
replay_drop_set()
return nodrop and VERDICT_PASS or VERDICT_DROP
end
else
-- 12. Не первый replay — дропнуть если ранее успешно отправлено
if replay_drop() then
return nodrop and VERDICT_PASS or VERDICT_DROP
end
end
end
end📚 См. также
- desync — обзор всех функций
--lua-desyncи общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты - жизненный цикл desync-функции — восемь стадий, общих для всех техник дурения: отсев транспорта, выбор данных, replay, вердикты
- структура desync и диссекта — подробное устройство таблицы
desyncи диссекта пакета - multidisorder — тот же разрез, но сегменты уходят в обратном порядке; там
seqovlработает принципиально иначе - multidisorder_legacy — вариант
multidisorder, полностью совместимый с алгоритмом nfqws1 - fakedsplit · fakeddisorder — разрез по одной позиции с подмешиванием фейковых сегментов
- tcpseg — отправка произвольного диапазона данных, ограниченного двумя маркерами
- oob — сегментация с urgent-байтом вместо разреза
- blob — как объявлять и передавать данные для аргументов
blobиseqovl_pattern - payload — распознавание типов протоколов, от которого зависит работа относительных маркеров
- ts-and-fooling — опции fooling и почему в
multisplitбольшинство из них вредны - последовательность аргументов — как выстраивается цепочка инстансов в профиле
- profile · preset — где
--lua-desyncживёт среди остальных настроек
Источники:
lua/zapret-antidpi.lua:483-539(телоmultisplit),lua/zapret-lib.lua(blob_or_def,pattern,delete_pos_1,rawsend_payload_segmented,direction_check,replay_first/replay_drop),nfq2/lua.cиnfq2/protocol.c(resolve_pos/resolve_multi_pos/ResolveMultiPos),docs/manual.md,docs/readme.mdиз репозитория zapret2.
🤖 Эти статьи открыты — можно обучать на них ИИ
При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.