multidisorder — TCP-сегментация в обратном порядке (zapret2 / nfqws2)

Файл: lua/zapret-antidpi.lua:597 nfqws1 эквивалент: --dpi-desync=multidisorder Сигнатура: function multidisorder(ctx, desync)

multidisorder — функция TCP-сегментации в zapret2, которая берёт текущий payload (или reasm, или blob), разрезает его на несколько TCP-сегментов по заданным позициям и отправляет их в обратном порядке (от последнего к первому). Обратный порядок (disorder) эксплуатирует поведение TCP-стека: при получении внеочередных сегментов стек буферизирует их и собирает поток, а DPI зачастую не справляется с реассемблированием разупорядоченных данных. После успешной отправки выносит VERDICT_DROP, чтобы оригинальный пакет не ушёл.

Родственные функции: multisplit (прямой порядок), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок), hostfakesplit (по hostname), tcpseg (диапазон), oob (urgent byte), multidisorder_legacy (совместимый с nfqws1 алгоритм).

Общий для всех техник дурения порядок работы — восемь стадий от отсева чужого транспорта до вердикта — разобран в жизненный цикл desync-функции; здесь описано только то, чем multidisorder от этого скелета отличается.


Оглавление


Зачем нужен multidisorder

DPI анализирует TCP-поток, пытаясь собрать полный payload и найти в нём сигнатуры (hostname в HTTP, SNI в TLS). Разрезание payload на сегменты уже усложняет работу DPI (multisplit), но обратный порядок отправки создаёт дополнительную проблему:

  1. DPI не реассемблирует out-of-order сегменты: многие DPI работают потоково (stream-based) и ожидают сегменты в порядке sequence number. Получив сначала хвост потока, DPI может потерять контекст или не дождаться головы
  2. DPI не может сопоставить hostname: если разрез проходит через SNI/Host, а сегменты приходят задом наперёд, ни попакетный, ни потоковый DPI не найдёт сигнатуру
  3. seqovl в режиме disorder перезаписывает буфер: в отличие от multisplit, где seqovl работает через выход за TCP window, в disorder фейковые данные сначала попадают в буфер сокета, а затем перезаписываются реальными данными из последнего сегмента

Сервер при этом корректно собирает поток — TCP-стек буферизирует внеочередные сегменты и отдаёт данные приложению только после сборки непрерывной последовательности.

multidisorder vs multisplit: disorder работает за счёт разупорядочивания, а не за счёт линейного дробления. Для замешивания фейковых сегментов (отдельные пакеты с fooling) есть fakeddisorder.


Быстрый старт

Минимально (разрез по позиции 2, payload=known, dir=out):

--lua-desync=multidisorder

Типовой TLS-разрез:

--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld

TLS с seqovl (маркер):

--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1

HTTP с разрезом по hostname:

--payload=http_req --lua-desync=multidisorder:pos=host,midsld,endhost

Откуда берутся данные для нарезки

Внутри multidisorder данные (data) выбираются в следующем порядке приоритетов:

1. blob_or_def(desync, desync.arg.blob)    — если задан blob= и он существует
2. desync.reasm_data                        — если есть реассемблированные данные (multi-packet payload)
3. desync.dis.payload                       — текущий пакет (fallback)

Следствие: все маркеры pos, seqovl и прочие аргументы применяются именно к тем данным, которые реально выбраны. Если вы задали blob=myblob, маркеры вроде midsld будут работать только если myblob содержит валидный TLS/HTTP payload, который zapret может распознать.


Маркеры позиций (pos)

pos — главный аргумент multidisorder. Определяет где внутри payload будет произведён разрез. Задаётся как строка со списком маркеров через запятую.

Типы маркеров

ТипОписаниеПример
Абсолютный положительныйСмещение от начала 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_methodeolhttp_req
hostПервый байт имени хоста (Host: в HTTP, SNI в TLS)http_req, tls_client_hello
endhostБайт, следующий за последним байтом имени хоста. Т.е. host..endhost-1 = полный hostnamehttp_req, tls_client_hello
sldПервый байт домена второго уровня (SLD). Для www.example.com — это e в examplehttp_req, tls_client_hello
endsldБайт, следующий за последним байтом SLD. Для example.com — это . после examplehttp_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 extensionstls_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 частей (если все маркеры успешно разрешатся и дадут различные позиции).

Как маркеры разрешаются в коде

Внутри multidisorder вызывается:

local pos = resolve_multi_pos(data, desync.l7payload, spos)

Функция resolve_multi_pos:

  1. Разбивает строку spos по запятым
  2. Для каждого маркера вызывает resolve_pos(blob, l7payload_type, marker)
  3. Если маркер не может быть разрешён (например, midsld для unknown payload) — он молча пропускается
  4. Результаты дедуплицируются и сортируются
  5. Возвращается массив уникальных позиций, уже переведённых в 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 (первый байт отдельным сегментом) работает и применяется часто. Частая жертва этого правила — pos=method для HTTP: метод обычно лежит на смещении 0, разреза не произойдёт; нужен сдвиг вроде pos=method+2
  • Дублирующиеся позиции объединяются. pos=5,5,5 = pos=5
  • Неразрешимые маркеры пропускаются. Если midsld не разрешается (payload = unknown), он просто исчезает из списка. Если все маркеры не разрешились — multidisorder ничего не делает (логирует “no valid split positions”)
  • Позиции сортируются. Независимо от порядка записи, pos=100,5,50 будет обработано как 5,50,100
  • По умолчанию pos=2. Если pos не задан, payload делится на 2 части: первые 2 байта отдельным сегментом, весь остаток — вторым

Обратный порядок отправки — суть disorder

Ключевое отличие multidisorder от multisplit: сегменты отправляются от последнего к первому (в порядке убывания sequence number). Цикл в multidisorder_send:

for i=#pos,0,-1 do
multisplit:      отправка 1 -> 2 -> 3 -> 4  (прямой порядок)
multidisorder:   отправка 4 -> 3 -> 2 -> 1  (обратный порядок)

Почему disorder ломает DPI

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

  1. Потоковые DPI обрабатывают данные по мере поступления. Получив последний сегмент первым, DPI видит данные без начала (без заголовков HTTP/TLS) и не может распознать протокол
  2. Попакетные DPI анализируют каждый пакет отдельно. В каждом отдельном сегменте нет полной сигнатуры
  3. DPI с реассемблированием теоретически могут собрать поток, но на практике многие имеют ограниченный буфер или timeout и не дожидаются всех частей

Ограничение: Windows-серверы

Техника seqovl в режиме disorder не работает с Windows-серверами. При перекрытии sequence numbers:

  • Linux/BSD/macOS: более поздний сегмент перезаписывает ранее буферизированные данные
  • Windows: сохраняет первые полученные данные, игнорируя перекрывающиеся

Это означает, что seqovl_pattern, записанный в буфер предпоследним сегментом, на Windows не будет перезаписан реальными данными из последнего сегмента — сервер получит мусор.

Без seqovl multidisorder работает нормально на всех системах — простое разупорядочивание не зависит от поведения при перекрытии.


seqovl — перезапись буфера сокета через перекрытие

В multidisorder техника seqovl работает принципиально иначе, чем в multisplit. Здесь seqovl эксплуатирует переписывание данных в буфере сокета при получении перекрывающихся сегментов.

Принцип работы seqovl в disorder

Рассмотрим payload из 2 частей: pos=100 (одна точка разреза).

Payload (300 байт): [ЧАСТЬ_1: 100 байт][ЧАСТЬ_2: 200 байт]
                                         ^pos=100

seqovl=midsld (разрешился в 50), ovl = 50 - 1 = 49

Порядок отправки (обратный, i от #pos до 0):

  Шаг 1 (i=1): ЧАСТЬ_2 с seqovl!
     К ЧАСТЬ_2 слева приписывается 49 байт PATTERN
     part = [PATTERN(49)][ЧАСТЬ_2(200)]
     seq  = pos_start - 1 - ovl = 100 - 1 - 49 = 50
     len  = 249
     Отправляется первым

  Шаг 2 (i=0): ЧАСТЬ_1 без seqovl
     part = [ЧАСТЬ_1(100)]
     seq  = 0
     len  = 100
     Отправляется последним

Что происходит на сервере (Linux/BSD):

t1: приходит [PATTERN(49)][ЧАСТЬ_2(200)] seq=50 len=249

  Буфер сокета:
  позиции:  0                50               100             300
            [     пусто     ][PATTERN(49)][ЧАСТЬ_2(200)........]
                              ^--- PATTERN занимает позиции 50..98
                                           ^--- ЧАСТЬ_2 начинается с позиции 99

  Данные НЕ выдаются приложению — нет непрерывной последовательности от 0

t2: приходит [ЧАСТЬ_1(100)] seq=0 len=100

  Буфер сокета (Linux/BSD — позднее ПЕРЕЗАПИСЫВАЕТ):
  позиции:  0                               100             300
            [ЧАСТЬ_1(100)....................][ЧАСТЬ_2(200)........]
            ^--- ЧАСТЬ_1 занимает позиции 0..99
                 PATTERN ПЕРЕЗАПИСАН реальными данными!

  Непрерывная последовательность 0..299 -> выдаётся приложению корректно

На Windows-сервере:

t2: приходит [ЧАСТЬ_1(100)] seq=0 len=100

  Буфер сокета (Windows — СОХРАНЯЕТ старое):
  позиции:  0          50               100             300
            [ЧАСТЬ_1(50)][PATTERN(49)][ЧАСТЬ_2(200)........]
            ^--- только байты 0..49 из ЧАСТЬ_1 записались
                 ^--- байты 50..98 остались от PATTERN (мусор!)

  Приложение получает повреждённые данные!

Отличие seqovl от multisplit

Аспектseqovl в multisplitseqovl в multidisorder
МеханизмВыход за левую границу TCP windowПерезапись буфера перекрывающимся сегментом
К какому сегменту1-й отправляемый (1-й в оригинале, i==0)Предпоследний отправляемый (2-й в оригинале, i==1)
Тип значенияТолько числоМаркер (число, имя маркера, маркер+арифметика)
Кто отбрасывает фейкTCP-стек (данные за пределами window)Перезапись следующим сегментом (последним отправляемым)
Работает на WindowsДа (TCP window — универсальный механизм)Нет (Windows не перезаписывает буфер)

seqovl как маркер

В отличие от multisplit, где seqovl принимает только число, в multidisorder seqovl разрешается через resolve_pos() — полноценный маркер:

seqovl = resolve_pos(data, desync.l7payload, desync.arg.seqovl)

Это означает, что поддерживаются:

seqovl=5               — число (абсолютная позиция, как в multisplit)
seqovl=midsld-1        — маркер с арифметикой (типичное использование)
seqovl=host+2          — маркер с арифметикой
seqovl=sld             — маркер без арифметики
seqovl=-10             — отрицательная позиция (от конца данных)

Типичный паттерн: pos=midsld + seqovl=midsld-1. Разрез по середине SLD, а seqovl перекрывает почти всю первую часть.

Если маркер не резолвится (например, seqovl=midsld-1 для unknown payload) — seqovl отменяется, но сегментация в обратном порядке всё равно происходит. В лог записывается “seqovl cancelled because could not resolve marker”.

Валидация seqovl: должен быть меньше pos[1]

Из multidisorder_send:

if seqovl>=pos[1] then
    DLOG("multidisorder: seqovl cancelled because seqovl "..
         (seqovl-1).." is not less than the first split pos "..(pos[1]-1))

seqovl (разрешённое значение маркера) обязательно должен быть строго меньше pos[1] (первой позиции разреза). Если это условие не выполнено — seqovl отменяется (но сегментация всё равно происходит, просто без перекрытия).

Почему: seqovl приписывается к сегменту i==1 (ЧАСТЬ_2, от pos[1] до конца или до pos[2]-1). Перекрытие заползает назад на ovl байт. Если seqovl >= pos[1], перекрытие вылезло бы за начало данных первого сегмента, что не имеет смысла.

Пример:

pos=10,50       -> pos[1]=10
seqovl=midsld   -> разрешился в 8    -> OK (8 < 10)
seqovl=midsld   -> разрешился в 10   -> CANCELLED (10 >= 10, не строго меньше)
seqovl=midsld   -> разрешился в 15   -> CANCELLED (15 >= 10)

Нюанс реализации: ovl = seqovl - 1

В коде multidisorder_send есть важная деталь:

ovl = seqovl - 1

Реальный размер перекрытия на 1 байт меньше разрешённого значения маркера. Это связано с тем, что seqovl возвращается как позиция (1-based в Lua), а ovl используется как размер смещения (0-based). Для пользователя это означает:

seqovl=midsld  (разрешился, скажем, в 50)
-> ovl = 49
-> К сегменту приписывается 49 байт pattern слева
-> TCP seq уменьшается на 49

Следствие: если seqovl разрешится в 1, то ovl = 0 — seqovl фактически не применяется. Минимальный эффективный seqovl — значение 2 (ovl = 1, 1 байт перекрытия).

seqovl_pattern

Паттерн, которым заполняется seqovl-область (ovl байт слева от реальных данных). По умолчанию — 0x00 (нули).

В multidisorder seqovl_pattern — это имя blob. Паттерн повторяется функцией pattern() до нужной длины ovl (= seqovl - 1).

# Inline hex blob (маскировка под начало TLS record)
--lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000
 
# Предзагруженный blob
--blob=tlspat:0x1603030100 \
--lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=tlspat

Если optional задан и blob seqovl_pattern отсутствует — используется нулевой паттерн (операция не отменяется).


Полный список аргументов

Формат вызова:

--lua-desync=multidisorder[:arg1[=val1][:arg2[=val2]]...]

Все val приходят в Lua как строки. Если =val не указан, значение = пустая строка "" (в Lua это truthy), поэтому флаги пишутся просто как :optional, :nodrop, :tcp_ts_up.

A) Собственные аргументы multidisorder

pos

  • Формат: pos=<marker[,marker2,...]>
  • Тип: строка со списком маркеров через запятую
  • По умолчанию: "2"
  • Описание: Точки разреза. Каждый маркер определяет позицию, по которой payload будет разрезан. N маркеров до N+1 сегментов. Сегменты отправляются в обратном порядке
  • Примеры:
    • pos=2 — разрез после 2-го байта (дефолт)
    • pos=1 — первый байт уходит отдельным сегментом
    • pos=midsld — разрез посередине SLD
    • pos=1,midsld — два разреза: после 1-го байта и посередине SLD 3 сегмента
    • pos=host,midsld,endhost-2,-10 — четыре разреза до 5 сегментов
    • pos=method+2 — после первых 2 символов HTTP-метода

seqovl

  • Формат: seqovl=<marker> (маркер, число, или маркер+арифметика)
  • Тип: маркер (в отличие от multisplit, где только число). Разрешается через resolve_pos()
  • По умолчанию: не задан (нет seqovl)
  • Описание: Применяется к сегменту i==1 (2-й в оригинальном порядке, предпоследний отправляемый). К данным этого сегмента слева добавляется seqovl-1 байт seqovl_pattern, а TCP th_seq уменьшается на seqovl-1. Последний отправляемый сегмент (1-й в оригинале) перезаписывает фейковые данные в буфере сервера
  • Ограничение: разрешённое значение seqovl должно быть строго меньше pos[1], иначе seqovl отменяется
  • Примеры:
    • seqovl=5 — 4 байта фейка слева (ovl = 5-1)
    • seqovl=midsld-1 — типичное использование с pos=midsld
    • seqovl=host+2 — маркер с арифметикой
    • seqovl=sld — маркер без арифметики

seqovl_pattern

  • Формат: seqovl_pattern=<blobName>
  • Тип: имя blob-переменной
  • По умолчанию: один байт 0x00, повторяемый до длины ovl (= seqovl - 1)
  • Описание: Данные для заполнения 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 hex
    • blob=my_custom_ch — предзагруженный blob

optional

  • Формат: optional (флаг, без значения)
  • Описание: Мягкий режим:
    • Если задан blob=... и blob отсутствует multidisorder ничего не делает (тихий skip, без ошибок)
    • Если задан seqovl_pattern=... и blob отсутствует используется нулевой паттерн (seqovl не отменяется)
  • Использование: защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией)

nodrop

  • Формат: nodrop (флаг, без значения)
  • Описание: После успешной отправки сегментов не выносить VERDICT_DROP (вместо этого вернуть VERDICT_PASS). Это означает, что оригинальный пакет тоже будет отправлен (наряду с нарезанными сегментами)
  • Использование: для отладки, для отправки произвольных данных без блокировки оригинала
  • Предупреждение: в боевых профилях nodrop обычно нежелателен — оригинал ещё раз уйдёт, что создаст дублирование и может ухудшить обход

B) Standard direction

ПараметрЗначенияПо умолчанию
dirin, out, anyout

Фильтр по направлению пакета. multidisorder по умолчанию работает только с исходящими (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, включая unknown
  • payload=tls_client_hello,http_req — конкретные типы
  • payload=~unknown — инверсия: всё кроме unknown

Важно: лучше ставить --payload=... на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.


D) Standard fooling

Модификации L3/L4 заголовков. В multidisorder применяются ко всем отправляемым сегментам (в отличие от fakeddisorder, где fooling идёт только на фейки).

ПараметрОписаниеПример
ip_ttl=NУстановить IPv4 TTLip_ttl=6
ip6_ttl=NУстановить IPv6 Hop Limitip6_ttl=6
ip_autottl=delta,min-maxАвтоматический TTL (delta от серверного TTL)ip_autottl=-2,40-64
ip6_autottl=delta,min-maxАналогично для IPv6ip6_autottl=-2,40-64
ip6_hopbyhop[=HEX]Вставить extension header hop-by-hop (по умолчанию 6 нулей)ip6_hopbyhop
ip6_hopbyhop2[=HEX]Второй hop-by-hop headerip6_hopbyhop2
ip6_destopt[=HEX]Destination options headerip6_destopt
ip6_destopt2[=HEX]Второй destination optionsip6_destopt2
ip6_routing[=HEX]Routing headerip6_routing
ip6_ah[=HEX]Authentication headerip6_ah
tcp_seq=NСместить TCP sequence (+ или -)tcp_seq=-10000
tcp_ack=NСместить TCP ack (+ или -)tcp_ack=-66000
tcp_ts=NСместить TCP timestamptcp_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-функция foolingfool=my_fooler

Заметка про tcp_ts_up: На Linux-серверах пакеты с инвалидным ACK стабильно отбрасываются только если TCP timestamp option идёт первой в заголовке. tcp_ts_up перемещает её в начало, обеспечивая корректную работу badseq-fooling.


E) Standard ipid

ПараметрОписаниеПо умолчанию
ip_id=seqПоследовательные IP IDseq
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). Для multidisorder бесполезно — он только TCP8
ipfrag_next=NIPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы)

G) Standard reconstruct

ПараметрОписание
badsumИспортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет

H) Standard rawsend

ПараметрОписание
repeats=NОтправить каждый сегмент N раз (идентичные повторы)
ifout=<iface>Интерфейс для отправки (по умолчанию определяется автоматически)
fwmark=NFirewall mark (только Linux, nftables/iptables)

Порядок отправки сегментов

multidisorder всегда отправляет сегменты в обратном порядке — от последнего к первому (в порядке убывания TCP sequence). Цикл в multidisorder_send: for i=#pos,0,-1.

Пример с 3 позициями разреза (без seqovl)

Payload (600 байт):
[AAA...100 байт...AAA][BBB...200 байт...BBB][CCC...150 байт...CCC][DDD...150 байт...DDD]
                       ^pos=100               ^pos=300               ^pos=450

                        Оригинальный порядок          Порядок отправки
                        -------------------          ----------------
Сегмент 0 (i=0):       [AAA...100] seq=0             4-й (последний)
Сегмент 1 (i=1):       [BBB...200] seq=100           3-й
Сегмент 2 (i=2):       [CCC...150] seq=300           2-й
Сегмент 3 (i=3):       [DDD...150] seq=450           1-й (первым отправляется)

Реальная последовательность отправки:
  1. [DDD...150] seq=450  len=150   (i=3, последний сегмент данных)
  2. [CCC...150] seq=300  len=150   (i=2)
  3. [BBB...200] seq=100  len=200   (i=1)
  4. [AAA...100] seq=0    len=100   (i=0, первый сегмент данных)

Пример с seqovl (2 сегмента)

Payload (300 байт), pos=100, seqovl=midsld (разрешился в 50):
ovl = 50 - 1 = 49

                        Оригинальный порядок          Порядок отправки
                        -------------------          ----------------
Сегмент 0 (i=0):       [ЧАСТЬ_1: 100 байт] seq=0    2-й (последний)
Сегмент 1 (i=1):       [ЧАСТЬ_2: 200 байт] seq=100  1-й (первым)

Реальная последовательность отправки:
  1. i=1: [PATTERN(49)][ЧАСТЬ_2(200)]  seq=100-1-49=50   len=249  (seqovl!)
  2. i=0: [ЧАСТЬ_1(100)]               seq=0              len=100  (перезаписывает PATTERN)

ASCII-диаграмма: хронология буфера сокета на сервере

Время -->

  t1: приходит [PATTERN(49)][ЧАСТЬ_2(200)]    seq=50  len=249

      Буфер сокета:
      позиция: 0           50          99  100                    299
               [  пусто   ][PATRN(49)][?? ][ЧАСТЬ_2(200)..............]
                            ^^^^^^^^^^^^--- PATTERN записан в позиции 50-98
                                            ^^^--- ЧАСТЬ_2 в позициях 99-298
      Данные НЕ выдаются приложению (нет непрерывности от 0)

  t2: приходит [ЧАСТЬ_1(100)]    seq=0  len=100

      Linux/BSD (поздний ПЕРЕЗАПИСЫВАЕТ):
      позиция: 0                        100                    299
               [ЧАСТЬ_1(100)............][ЧАСТЬ_2(200)..............]
               ^--- ЧАСТЬ_1 перезаписала позиции 0-99
                    PATTERN уничтожен, данные корректны!

      Windows (СОХРАНЯЕТ раннее):
      позиция: 0           50          99  100                    299
               [ЧАСТЬ_1(50)][PATRN(49)][?? ][ЧАСТЬ_2(200)..............]
               ^--- только позиции 0-49 обновились
                    позиции 50-98 остались от PATTERN = МУСОР

Пример с 3 позициями + seqovl

Payload (600 байт), pos=100,300,450, seqovl=host (разрешился в 80):
ovl = 80 - 1 = 79

Порядок отправки:
  1. i=3: [DDD...150]                  seq=450      len=150   (без seqovl)
  2. i=2: [CCC...150]                  seq=300      len=150   (без seqovl)
  3. i=1: [PATTERN(79)][BBB...200]     seq=100-1-79=20  len=279  (seqovl! только i==1)
  4. i=0: [AAA...100]                  seq=0        len=100   (перезаписывает PATTERN)

Обратите внимание: seqovl применяется только к i==1 (2-й сегмент в оригинальном порядке, предпоследний в порядке отправки), независимо от количества позиций разреза.


Поведение при replay / reasm

Многопакетный запрос (большой TLS ClientHello с post-quantum ключами и т. п.) nfqws2 придерживает, собирает в буфер reasm_data и перепроигрывает через desync-функции. multidisorder использует штатную развилку перепроигрывания из общего скелета desync-функций (жизненный цикл desync-функции, стадия 6) без изменений: нарезает и отправляет весь собранный буфер на первой части, а остальные части дропает, потому что их содержимое уже ушло в составе нарезки. Если отправка сорвалась, флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть.

Специфика multidisorder — в следствии этого поведения. Раз режется весь реасм целиком, обратный порядок распространяется на весь запрос, а не на отдельные пакеты: последний сегмент всего ClientHello уйдёт первым, даже если исходно он приехал третьим пакетом.

Отличие от multidisorder_legacy: legacy-версия развилку не использует вовсе — она обрабатывает каждую часть replay отдельно, нормализуя позиции разреза под границы конкретного пакета, как это делал nfqws1. Из-за этого порядок сегментов при многопакетных запросах у двух функций разный. Для полной совместимости с nfqws1 нужен именно legacy-вариант.


Автосегментация по MSS

О размерах TCP-сегментов думать не нужно. Отправкой занимается rawsend_payload_segmented из zapret-lib.lua, и она сама доводит куски до допустимого размера:

  1. MSS берётся из desync.tcp_mss — движок отслеживает его для каждого TCP-соединения
  2. Если кусок вместе с заголовками превышает MSS — он дополнительно режется по MSS
  3. Каждый под-сегмент отправляется с корректным TCP sequence

Пример: если seqovl разрешился в большое значение (скажем, 5000), это не вызовет ошибку. rawsend_payload_segmented отправит несколько TCP-сегментов общим размером, равным seqovl_pattern + данные сегмента.


Место в общем скелете

Тело multidisorder построено по тому же шаблону, что и все остальные функции дурения: восемь стадий от отсева чужого транспорта до вердикта. Разобраны они один раз в жизненный цикл desync-функции — здесь только отклонения:

Стадия общего скелетаЧто делает multidisorder
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 на втором по оригинальному порядку сегменте, отправка от последнего сегмента к первому — этому посвящена вся заметка
8. ВердиктVERDICT_DROP после успешной отправки, VERDICT_PASS при nodrop или сбое rawsend

Отличие от multisplit сосредоточено ровно в одной стадии — седьмой. Всё остальное у двух функций совпадает буквально построчно, вплоть до общего хелпера multidisorder_send, который и разворачивает порядок отправки.


Псевдокод алгоритма

Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в жизненный цикл desync-функции:

function multidisorder(ctx, desync)
    -- 1. Проверка: только TCP
    if not desync.dis.tcp then
        if not desync.dis.icmp then instance_cutoff_shim() end
        return
    end
 
    -- 2. Cutoff противоположного направления
    direction_cutoff_opposite(ctx, desync)
 
    -- 3. Проверка optional blob
    if optional and blob specified and blob not exists then
        DLOG("blob not found. skipped")
        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. Разрешение seqovl (МАРКЕР, не число!)
                seqovl = nil
                if arg.seqovl then
                    seqovl = resolve_pos(data, l7payload, arg.seqovl)
                    if not seqovl then
                        DLOG("seqovl cancelled: marker not resolved")
                    end
                end
 
                -- 9. multidisorder_send: цикл ОБРАТНЫЙ
                for i = #pos, 0, -1 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)
 
                    -- 10. seqovl только для i==1 (предпоследний отправляемый)
                    ovl = 0
                    if i == 1 and seqovl and seqovl > 0 then
                        if seqovl >= pos[1] then
                            DLOG("seqovl cancelled: not less than first split pos")
                        else
                            ovl = seqovl - 1     -- ВАЖНО: минус 1!
                            pat = seqovl_pattern_blob or "\x00"
                            part = pattern(pat, 1, ovl) .. part
                        end
                    end
 
                    -- 11. Отправка с автосегментацией
                    rawsend_payload_segmented(part, pos_start - 1 - ovl)
                end
 
                -- 12. Пометить как отправленное
                replay_drop_set()
                return nodrop and VERDICT_PASS or VERDICT_DROP
            else
                DLOG("no valid split positions")
            end
        else
            -- 13. Не первый replay
            DLOG("not acting on further replay pieces")
        end
        -- 14. Дропнуть если ранее успешно отправлено
        if replay_drop() then
            return nodrop and VERDICT_PASS or VERDICT_DROP
        end
    end
end

Нюансы и подводные камни

1. Работает только с TCP

Если текущий пакет не TCP (UDP, ICMP и т.д.), multidisorder делает instance_cutoff_shim — отключает себя для этого потока навсегда (кроме ICMP-ответов, для которых cutoff не вызывается).

2. Разрез в самом начале данных отбрасывается

delete_pos_1(pos) убирает из списка Lua-позицию 1 — она соответствует маркеру 0, то есть разрезу перед первым байтом, от которого первый сегмент вышел бы пустым. Если после этой чистки не осталось ни одной позиции, multidisorder не делает ничего.

Путаться тут легко из-за разной нумерации: в командной строке маркеры считаются от нуля, внутри Lua — от единицы. Поэтому под удаление попадает не pos=1, а pos=0. Самая частая реальная жертва — pos=method для HTTP: метод обычно лежит на смещении 0, маркер разрешается в удаляемую позицию, и разреза не происходит. Рабочий вариант — pos=method+2.

3. Все маркеры могут не разрешиться

Если вы указали pos=midsld,sniext для HTTP-payload, оба маркера (специфичные для TLS) не разрешатся. Multidisorder напишет в лог “no valid split positions” и ничего не сделает.

4. seqovl как маркер тоже может не разрешиться

В отличие от multisplit (где seqovl — число и всегда валидно), в multidisorder seqovl=midsld-1 может не разрешиться (например, для unknown payload). В этом случае seqovl отменяется (логируется), но сегментация в обратном порядке всё равно происходит.

5. seqovl НЕ работает на Windows-серверах

Если целевой сервер работает под Windows, seqovl бесполезен: Windows сохраняет первые полученные данные и не перезаписывает буфер при получении перекрывающихся сегментов. Результат — сервер получит мусор (seqovl_pattern) вместо реальных данных. Без seqovl multidisorder работает на всех ОС нормально.

6. ovl = seqovl - 1: размер перекрытия на 1 меньше

Из-за строки ovl = seqovl - 1 в multidisorder_send, если seqovl разрешился в 1, то ovl будет 0 — фактически seqovl не применяется. Минимальный эффективный seqovl — значение 2 (ovl = 1, 1 байт перекрытия).

7. seqovl >= pos[1] отмена seqovl

Если разрешённое значение seqovl больше или равно первой позиции разреза, seqovl отменяется. Это частая ошибка при использовании маркеров, которые разрешаются в большие значения. Логируется как “seqovl cancelled because seqovl N is not less than the first split pos M”.

8. nodrop создаёт дублирование

С nodrop multidisorder отправляет нарезанные сегменты И пропускает оригинальный пакет. Сервер получит данные дважды. Используйте nodrop только для отладки или когда это осознанно нужно.

9. Fooling применяется ко ВСЕМ сегментам

В отличие от fakeddisorder, где fooling идёт только на фейки, в multidisorder все сегменты получают fooling. Если задать tcp_ack=-66000, все сегменты получат инвалидный ack — сервер их отбросит, и ничего не заработает. Fooling в multidisorder имеет смысл только для специфических вещей (например, tcp_ts_up, ip_id, IPv6 extension headers).

10. Алгоритм отличается от nfqws1

Из комментария в коде: “algorithm is not 100% the same as in nfqws1. multi-segment queries can produce different segment ordering.” nfqws2 работает с целым reasm, а nfqws1 — по отдельным частям replay. Для полной совместимости с nfqws1 используйте multidisorder_legacy.

11. Порядок инстансов важен

Если перед multidisorder стоит fake, он отправит фейк первым. Если после multidisorder стоит ещё один инстанс — он увидит VERDICT_DROP и не получит оригинальный payload.


Отличия от других функций сегментации

Аспектmultisplitmultidisorderfakedsplitfakeddisorder
Количество позицийСписок (любое кол-во)Список (любое кол-во)ОднаОдна
Порядок отправкиПрямой (123)Обратный (321)ПрямойОбратный
Фейковые сегментыНетНетДа (до 4 шт.)Да (до 4 шт.)
seqovl типТолько числоМаркерТолько числоМаркер
seqovl к какому сегменту1-й (первый отправляемый, i==0)2-й в оригинале (предпоследний отправляемый, i==1)1-й реальный2-й реальный
seqovl механизмВыход за TCP windowПерезапись буфераВыход за TCP windowПерезапись буфера
Windows-серверы (seqovl)РаботаетНе работаетРаботаетНе работает
Fooling кВсем сегментамВсем сегментамТолько к фейкамТолько к фейкам
ipfragДаДаНетНет
blob аргументДаДаНетНет
reasmЦелый reasm на replay_firstЦелый reasm на replay_firstПо частямПо частям

Миграция с nfqws1

Соответствие параметров

nfqws1nfqws2
--dpi-desync=multidisorder--lua-desync=multidisorder
--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=multidisorder --dpi-desync-split-pos=midsld
 
# nfqws2:
nfqws2 --lua-desync=multidisorder:pos=midsld

Миграция с fake + multidisorder

# nfqws1:
nfqws --dpi-desync=fake,multidisorder \
  --dpi-desync-split-pos=1,midsld \
  --dpi-desync-repeats=11 \
  --dpi-desync-fooling=md5sig
 
# nfqws2:
nfqws2 \
  --lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
  --lua-desync=multidisorder:pos=1,midsld

Миграция с seqovl

# nfqws1:
nfqws --dpi-desync=multidisorder \
  --dpi-desync-split-pos=midsld \
  --dpi-desync-split-seqovl=5 \
  --dpi-desync-split-seqovl-pattern=0x1603030000
 
# nfqws2 (seqovl теперь может быть маркером!):
nfqws2 --lua-desync=multidisorder:pos=midsld:seqovl=5:seqovl_pattern=0x1603030000
# или с маркером (новая возможность nfqws2):
nfqws2 --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000

Полная миграция: fake + disorder + seqovl + fooling

# nfqws1:
nfqws --dpi-desync=fake,multidisorder \
  --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=multidisorder:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000

Замечание о совместимости: алгоритм multidisorder в nfqws2 не на 100% совпадает с nfqws1 при многопакетных запросах (multi-segment queries). nfqws2 работает с целым reasm, а nfqws1 — по частям. Для полной совместимости используйте multidisorder_legacy.


Практические примеры

1. Минимальный (дефолт: pos=2, dir=out, payload=known)

--lua-desync=multidisorder

Разрезает payload после 2-го байта 2 сегмента (первые 2 байта и остаток), отправляемых в обратном порядке.

2. TLS: разрез посередине SNI

--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld

SNI разрезан пополам, хвост отправлен первым — DPI не может собрать домен.

3. TLS: разрез + seqovl маркером (классическая комбинация)

--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1

Разрез по midsld, seqovl перекрывает почти всю первую часть фейковыми данными. Типичный паттерн для disorder.

4. TLS: seqovl + кастомный паттерн

--payload=tls_client_hello \
  --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000

seqovl-область заполнена данными, похожими на начало TLS record — DPI может принять за легитимный TLS-трафик.

5. HTTP: разрез после метода (disorder)

--payload=http_req --lua-desync=multidisorder:pos=method+2

Для GET /path... разрежет после GE хвост уйдёт первым, голова — вторым.

6. HTTP: несколько разрезов вокруг hostname

--payload=http_req --lua-desync=multidisorder:pos=host,midsld,endhost

Разрезает: до hostname | первая половина | вторая половина | после hostname 4 сегмента в обратном порядке.

7. Два разреза с seqovl

--payload=tls_client_hello --lua-desync=multidisorder:pos=2,midsld:seqovl=sld

3 сегмента, seqovl применяется к сегменту i=1 (второй в оригинале). Маркер sld определяет размер перекрытия.

8. Произвольный blob вместо payload

--blob=mydata:@custom_payload.bin \
--lua-desync=multidisorder:blob=mydata:pos=10,100,-20

Режет и отправляет произвольные данные из файла вместо реального payload, в обратном порядке.

9. Защита от отсутствующего blob

--lua-desync=multidisorder:blob=maybe_missing:optional:pos=midsld

Если blob не существует — тихий пропуск, без ошибок и без VERDICT_DROP.

10. Отладка: не блокировать оригинал

--payload=http_req --lua-desync=multidisorder:pos=host,midsld:nodrop

Отправляет нарезанные сегменты в обратном порядке И пропускает оригинальный пакет (для экспериментов).

11. IP-фрагментация поверх TCP-сегментации (двойной disorder)

--payload=tls_client_hello \
  --lua-desync=multidisorder:pos=1,midsld:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32

Каждый TCP-сегмент дополнительно фрагментируется на IP-уровне в обратном порядке. Двойной disorder: TCP-сегменты в обратном порядке + IP-фрагменты каждого сегмента тоже в обратном порядке.

12. Комбинация: fake multidisorder (типичный боевой профиль)

--payload=tls_client_hello \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
  --lua-desync=multidisorder:pos=1,midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000

Сначала отправляется фейковый TLS ClientHello (с fooling), затем реальный — нарезанный на 3 сегмента в обратном порядке с seqovl.

13. Боевой пример для YouTube

--filter-tcp=443 --hostlist=youtube.txt \
  --lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
  --lua-desync=multidisorder:pos=1,midsld

11 фейков подряд + реальный payload разрезан на 3 части в обратном порядке.

14. С TCP timestamp + IP ID

--payload=tls_client_hello --lua-desync=multidisorder:pos=1:tcp_ts_up:ip_id=seq:ip_id_conn

15. Повторы отправки

--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:repeats=2

Каждый сегмент отправляется 2 раза (бинарные повторы).


Источники: lua/zapret-antidpi.lua:546-629, lua/zapret-lib.lua, docs/manual.md:4067-4097, docs/readme.md из репозитория zapret2.