multidisorder_legacy --- попакетный обратный порядок TCP-сегментов (zapret2 / nfqws2)

Файл: lua/zapret-antidpi.lua:649 nfqws1 эквивалент: --dpi-desync=multidisorder (полная совместимость) Сигнатура: function multidisorder_legacy(ctx, desync)

multidisorder_legacy --- реализация алгоритма multidisorder, полностью совместимая с nfqws1. Ключевое отличие от нового multidisorder: legacy-вариант работает попакетно --- он обрабатывает каждую оригинальную часть replay отдельно, сохраняя исходную TCP-сегментацию. Сегменты отправляются в обратном порядке только внутри каждого пакета, а между пакетами --- в прямом порядке (как они пришли).

Родственные функции: multisplit (прямой порядок), multidisorder (новый, по всему reasm), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок).

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


Оглавление


Зачем нужен multidisorder_legacy

Новый multidisorder в zapret2 работает с полным reasm_data целиком: он собирает весь многопакетный payload, нарезает его и отправляет все сегменты в обратном порядке за один проход. Это изменяет оригинальную TCP-сегментацию и порядок следования частей по сравнению с nfqws1.

multidisorder_legacy нужен для случаев, когда:

  1. Необходима 100%-я совместимость с nfqws1. Если рабочий профиль из nfqws1 использовал --dpi-desync=multidisorder, legacy-вариант воспроизведёт точно такое же поведение
  2. DPI чувствителен к оригинальной сегментации. Некоторые DPI анализируют размеры TCP-сегментов. Legacy сохраняет оригинальные размеры пакетов, меняя только порядок внутри каждого
  3. Нужен контроль seqovl на уровне пакетов. В legacy seqovl применяется только к тому пакету, в который попала нормализованная позиция, а не ко всему reasm

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

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

--lua-desync=multidisorder_legacy

Типовой TLS-разрез посередине SNI:

--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld

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

--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1

Ключевое отличие: попакетная обработка vs целый reasm

Это центральное различие между multidisorder и multidisorder_legacy. Рассмотрим его на примере большого TLS ClientHello, который не влезает в один TCP-сегмент и разбит на 2 пакета.

Диаграмма: новый multidisorder (весь reasm)

Исходные пакеты от ядра:
  Пакет A (500 байт, offset=0):   [AAAAAAAAAAAA...]
  Пакет B (300 байт, offset=500):  [BBBBBBBB...]

reasm_data (800 байт): [AAAAAAAAAAAA...BBBBBBBB...]

Позиции разреза: pos=200,600 (2 разреза -> 3 сегмента)

Разрезанный reasm:
  Часть 1: [AAA..200 байт..]  seq=0    (из пакета A)
  Часть 2: [AAA..400 байт..]  seq=200  (из пакета A + B)
  Часть 3: [BBB..200 байт..]  seq=600  (из пакета B)

Порядок отправки (ОБРАТНЫЙ по всему reasm):
  3 -> 2 -> 1
  [BBB..200] seq=600   <-- первым
  [AAA..400] seq=200   <-- вторым (+ seqovl если задан)
  [AAA..200] seq=0     <-- последним

Оригинальная сегментация (500+300) ПОТЕРЯНА.

Диаграмма: multidisorder_legacy (попакетно)

Исходные пакеты от ядра:
  Пакет A (500 байт, offset=0):   [AAAAAAAAAAAA...]
  Пакет B (300 байт, offset=500):  [BBBBBBBB...]

fulldata (800 байт): [AAAAAAAAAAAA...BBBBBBBB...]  (для resolve_pos)
data = dis.payload (текущий пакет)

Позиции разреза: pos=200,600 (2 разреза -> маркеры)

=== Обработка пакета A (data = 500 байт, range_low=1, range_hi=501) ===

  Разрешённые позиции (на fulldata): [200, 600]
  Нормализация по пакету A:
    200 -> в диапазоне [1, 501) -> нормализован: 200 - 1 + 1 = 200  OK
    600 -> НЕ в диапазоне [1, 501) -> УДАЛЁН

  После нормализации: pos = [200]
  Разрезанный пакет A:
    Часть A1: [AAA..200 байт..]
    Часть A2: [AAA..300 байт..]

  Порядок отправки (обратный ВНУТРИ пакета A):
    A2 -> A1
    [AAA..300] seq=200  <-- первым из пакета A (+ seqovl если попал сюда)
    [AAA..200] seq=0    <-- вторым из пакета A

=== Обработка пакета B (data = 300 байт, range_low=501, range_hi=801) ===

  Разрешённые позиции (на fulldata): [200, 600]
  Нормализация по пакету B:
    200 -> НЕ в диапазоне [501, 801) -> УДАЛЁН
    600 -> в диапазоне [501, 801) -> нормализован: 600 - 501 + 1 = 100  OK

  После нормализации: pos = [100]
  Разрезанный пакет B:
    Часть B1: [BBB..100 байт..]
    Часть B2: [BBB..200 байт..]

  Порядок отправки (обратный ВНУТРИ пакета B):
    B2 -> B1
    [BBB..200] seq=600  <-- первым из пакета B
    [BBB..100] seq=500  <-- вторым из пакета B

=== Итоговый порядок на проводе ===

  A2, A1, B2, B1
  [AAA..300] seq=200   (пакет A, обратный)
  [AAA..200] seq=0     (пакет A, обратный)
  [BBB..200] seq=600   (пакет B, обратный)
  [BBB..100] seq=500   (пакет B, обратный)

Оригинальная сегментация (500+300) СОХРАНЕНА.
Обратный порядок ТОЛЬКО внутри каждого пакета.
Между пакетами --- ПРЯМОЙ порядок.

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

В multidisorder_legacy используются два источника данных одновременно:

data     = desync.dis.payload       -- текущий пакет (для нарезки и отправки)
fulldata = desync.reasm_data        -- полный реассемблированный payload (для resolve_pos)

Почему два источника:

  • fulldata нужен для разрешения маркеров (midsld, host и т.д.), потому что они вычисляются по полному payload (SNI может быть виден только в контексте всего ClientHello)
  • data --- это то, что реально нарезается и отправляется: именно текущий оригинальный пакет

Отличие от нового multidisorder:

multidisorder (новый)multidisorder_legacy
data для нарезкиblob_or_def() or reasm_data or dis.payloaddis.payload (всегда текущий пакет)
data для resolve_posто же самое (data)reasm_data (fulldata)
blob поддержкаДаНет

Следствие: multidisorder_legacy не поддерживает аргумент blob. Нельзя заменить payload на произвольные данные.


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

pos --- главный аргумент. Определяет где внутри 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 ClientHellotls_client_hello
extlenПоле длины всех TLS extensionstls_client_hello

Арифметика маркеров

К любому маркеру можно прибавить (+) или вычесть (-) целое число:

midsld+1      -- один байт ПОСЛЕ середины SLD
midsld-1      -- один байт ДО середины SLD
endhost-2     -- два байта до конца hostname
host+3        -- три байта после начала hostname
sniext+1      -- один байт после начала SNI extension data
-1            -- последний байт payload (абсолютный, не относительный)

Нормализация позиций по пакету

Это ключевая особенность multidisorder_legacy, отсутствующая в новом multidisorder.

Маркеры разрешаются по fulldata (весь reasm), но затем нормализуются по диапазону текущего пакета:

range_low = (desync.reasm_offset or 0) + 1
range_hi  = range_low + #data

Функция pos_array_normalize(pos, range_low, range_hi):

  1. Для каждой позиции проверяет: pos >= range_low AND pos < range_hi
  2. Если позиция в диапазоне --- нормализует: pos_normalized = pos - range_low + 1
  3. Если позиция вне диапазона --- удаляет из массива
Пример:
  fulldata = 800 байт
  Маркеры разрешены: pos = [200, 450, 600]

  Пакет A (offset=0, len=500):
    range_low=1, range_hi=501
    200: 200 >= 1 AND 200 < 501 -> OK, normalize: 200
    450: 450 >= 1 AND 450 < 501 -> OK, normalize: 450
    600: 600 >= 1 AND 600 < 501 -> УДАЛЁН
    Результат: [200, 450]

  Пакет B (offset=500, len=300):
    range_low=501, range_hi=801
    200: 200 >= 501? -> НЕТ -> УДАЛЁН
    450: 450 >= 501? -> НЕТ -> УДАЛЁН
    600: 600 >= 501 AND 600 < 801 -> OK, normalize: 600 - 501 + 1 = 100
    Результат: [100]

Важные нюансы pos

  • Разрез в самом начале данных отбрасывается. delete_pos_1 удаляет Lua-позицию 1, соответствующую маркеру 0 (маркеры в командной строке считаются с нуля). Это выполняется после нормализации, поэтому удаляется и позиция, нормализовавшаяся в начало текущего куска reasm
  • Дублирующиеся позиции объединяются. pos=5,5,5 = pos=5
  • Неразрешимые маркеры пропускаются. Если midsld не разрешается (payload = unknown), он исчезает из списка
  • Позиции сортируются. Независимо от порядка записи, pos=100,5,50 обрабатывается как 5,50,100
  • По умолчанию pos=“2”. Если pos не задан, разрез по позиции 2
  • Если все позиции вышли за пределы пакета --- пакет отправляется как есть (с применёнными опциями fooling, ipid и т.д.) через rawsend_payload_segmented, без разреза

seqovl --- попакетный скрытый фейк

seqovl --- маркер, а не число

В отличие от multisplit, где seqovl принимает только число, в multidisorder_legacy (и в новом multidisorder) seqovl является маркером. Это значит, что можно написать:

:seqovl=midsld-1      -- seqovl = (позиция midsld) - 1
:seqovl=host           -- seqovl = позиция host
:seqovl=5              -- seqovl = 5 (число тоже маркер)

Маркер seqovl разрешается через resolve_pos(fulldata, l7payload, seqovl) --- по полному reasm, точно так же, как и позиции разреза.

Нормализация seqovl по пакету

После разрешения маркера, seqovl нормализуется по диапазону текущего пакета через pos_normalize(seqovl, range_low, range_hi):

seqovl_resolved = resolve_pos(fulldata, l7payload, "midsld-1")  -- например, 245
seqovl_normalized = pos_normalize(245, range_low, range_hi)

Для пакета A (range_low=1, range_hi=501):
  245 >= 1 AND 245 < 501 -> OK -> seqovl = 245 - 1 + 1 = 245

Для пакета B (range_low=501, range_hi=801):
  245 >= 501? -> НЕТ -> seqovl = nil (отменён для этого пакета)

Следствие: seqovl применяется только к тому пакету, в диапазон которого попала нормализованная позиция seqovl. Для остальных пакетов seqovl не действует.

seqovl_pattern

Паттерн заполнения seqovl-области. По умолчанию --- 0x00 (нули). Задаётся как имя blob:

:seqovl_pattern=0x1603030000         -- inline hex
:seqovl_pattern=my_pattern_blob       -- предзагруженный blob

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


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

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

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

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

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

pos

  • Формат: pos=<marker[,marker2,...]>
  • Тип: строка со списком маркеров через запятую
  • По умолчанию: "2"
  • Описание: Точки разреза. Маркеры разрешаются по fulldata (reasm_data), затем нормализуются по диапазону текущего пакета. N маркеров до N+1 сегментов на пакет
  • Примеры:
    • pos=2 --- разрез после 2-го байта (дефолт)
    • pos=1 --- первый байт уходит отдельным сегментом
    • pos=midsld --- разрез посередине SLD
    • pos=1,midsld --- два разреза
    • pos=host,midsld,endhost-2,-10 --- четыре разреза

seqovl

  • Формат: seqovl=<marker> (маркер, не просто число!)
  • Тип: маркер (строка, разрешаемая через resolve_pos)
  • По умолчанию: не задан (нет seqovl)
  • Описание: Разрешается по fulldata, нормализуется по текущему пакету. Применяется ко 2-му сегменту в оригинальной очередности (предпоследнему отсылаемому). seqovl обязательно должен быть меньше первой позиции разреза (в нормализованном виде), иначе отменяется
  • Примеры:
    • seqovl=5 --- 5 байт фейка
    • seqovl=midsld-1 --- seqovl привязан к позиции midsld
    • seqovl=host --- seqovl привязан к позиции host

seqovl_pattern

  • Формат: seqovl_pattern=<blobName>
  • Тип: имя blob-переменной
  • По умолчанию: один байт 0x00, повторяемый до длины seqovl
  • Описание: Данные для заполнения seqovl-области. Blob повторяется функцией pattern() до нужного размера
  • Поведение с optional: если optional задан и blob отсутствует --- используется нулевой паттерн

optional

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

Внимание: blob и nodrop не поддерживаются в multidisorder_legacy. Это ключевое отличие от нового multidisorder.


B) Standard direction

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

Фильтр по направлению пакета. multidisorder_legacy по умолчанию работает только с исходящими (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

D) Standard fooling

Модификации L3/L4 заголовков. Применяются ко всем отправляемым сегментам.

ПараметрОписаниеПример
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

Заметка: Fooling применяется ко ВСЕМ сегментам (и реальным). Если задать tcp_ack=-66000, сервер отбросит все сегменты. Fooling в multidisorder_legacy имеет смысл только для специфических вещей (tcp_ts_up, ip_id, IPv6 extension headers).


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 в рамках соединения---

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)8
ipfrag_next=NIPv6: next protocol во 2-м фрагменте---

G) Standard reconstruct

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

H) Standard rawsend

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

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

multidisorder_legacy использует общую функцию multidisorder_send, которая отправляет сегменты в обратном порядке (от последнего к первому). Но поскольку legacy обрабатывает каждый пакет отдельно, обратный порядок действует только внутри одного пакета.

Пример: однопакетный payload

Payload (600 байт), pos=100,300,450:

Разрезанный payload:
  Часть 1: [0..99]    100 байт
  Часть 2: [100..299] 200 байт
  Часть 3: [300..449] 150 байт
  Часть 4: [450..599] 150 байт

Порядок отправки (ОБРАТНЫЙ):
  Часть 4: [450..599] seq=450   <-- первой
  Часть 3: [300..449] seq=300   <-- второй
  Часть 2: [100..299] seq=100   <-- третьей (+ seqovl если задан)
  Часть 1: [0..99]    seq=0     <-- последней

Пример: многопакетный payload (2 пакета)

Пакет A (400 байт, offset=0), pos=150
Пакет B (300 байт, offset=400), pos=550 (нормализованный: 150)

=== Обработка пакета A ===
  Часть A1: [0..149]   150 байт
  Часть A2: [150..399]  250 байт
  Отправка: A2 (seq=150), A1 (seq=0)

=== Обработка пакета B ===
  Часть B1: [0..149]   150 байт  (соответствует offset 400..549)
  Часть B2: [150..299]  150 байт  (соответствует offset 550..699)
  Отправка: B2 (seq=550), B1 (seq=400)

Итог на проводе: A2, A1, B2, B1
Между пакетами A и B --- ПРЯМОЙ порядок.
Внутри каждого --- ОБРАТНЫЙ.

Пример с seqovl

Payload (600 байт, один пакет), pos=100,300, seqovl=80:

seqovl применяется к сегменту i=1 (второй в оригинальной очередности = предпоследний отсылаемый):

Порядок отправки:
  Часть 3: [300..599]              seq=300   len=300
  Часть 2: [PATTERN(79)][100..299] seq=21    len=279  <-- seqovl=80, ovl=79
  Часть 1: [0..99]                 seq=0     len=100  <-- переписывает паттерн

Часть 1 отправляется последней и переписывает ложные данные из seqovl_pattern
в буфере TCP-стека сервера.

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

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

Стадия общего скелетаЧто делает multidisorder_legacy
1. Отсев транспортатолько TCP; на не-TCP делает instance_cutoff (кроме связанного ICMP)
2. Направлениеdir=out по умолчанию, отключается от входящего
3. Аргументыобязательных нет; optional влияет только на seqovl_pattern
4. Данныеотклонение: режется desync.dis.payload — payload текущего пакета, а reasm_data берётся отдельно, только чтобы разрешить по нему маркеры
5. Гвардыштатные #data>0, direction_check, payload_check (по умолчанию known)
6. replayотклонение: развилка replay_first/replay_drop не используется вовсе — функция работает на каждой части, нормализуя позиции разреза под её границы (pos_array_normalize)
7. Своя техниканормализация позиций под текущий пакет, seqovl внутри того же пакета, отправка сегментов в обратном порядке внутри каждой части
8. ВердиктVERDICT_DROP после успешной отправки; если в этом пакете не оказалось ни одной позиции разреза — пакет отправляется как есть с применёнными опциями и тоже дропается

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


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

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

function multidisorder_legacy(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. Источники данных
    local data = desync.dis.payload           -- текущий пакет (для нарезки)
    local fulldata = desync.reasm_data        -- весь reasm (для resolve_pos)
 
    -- 4. Проверки: данные не пусты, направление OK, payload OK
    if #data > 0 and direction_check() and payload_check() then
 
        -- 5. Вычисление диапазона текущего пакета
        local range_low = (desync.reasm_offset or 0) + 1
        local range_hi = range_low + #data
 
        -- 6. Разрешение маркеров по FULLDATA
        local pos = resolve_multi_pos(fulldata, l7payload, pos_arg or "2")
 
        -- 7. Нормализация позиций по диапазону текущего пакета
        pos_array_normalize(pos, range_low, range_hi)
        delete_pos_1(pos)  -- удалить Lua-позицию 1 (маркер 0)
 
        if #pos > 0 then
            -- 8. Разрешение и нормализация seqovl (если задан)
            local seqovl = nil
            if desync.arg.seqovl then
                seqovl = resolve_pos(fulldata, l7payload, desync.arg.seqovl)
                if seqovl then
                    seqovl = pos_normalize(seqovl, range_low, range_hi)
                    -- seqovl может стать nil если вне диапазона пакета
                end
            end
 
            -- 9. Отправка через общую функцию (обратный порядок)
            return multidisorder_send(desync, data, seqovl, pos)
            -- multidisorder_send: for i = #pos, 0, -1 do ... end
            -- seqovl применяется к сегменту i=1 (2-й по оригинальной очередности)
        else
            -- 10. Нет позиций в этом пакете -> отправить как есть
            if rawsend_payload_segmented(desync) then
                return VERDICT_DROP
            end
        end
    end
end

Обратите внимание: в отличие от нового multidisorder, здесь нет replay_first() / replay_drop() / replay_drop_set(). Функция вызывается для каждого пакета replay и обрабатывает его самостоятельно. Это и есть попакетная обработка.


Сравнительная таблица: multidisorder vs multidisorder_legacy

Аспектmultidisorder (новый)multidisorder_legacy
Совместимость с nfqws1Частичная (порядок может отличаться)Полная
Источник данных для нарезкиblob_or_def() or reasm_data or dis.payloaddis.payload (текущий пакет)
Источник данных для resolve_posто же (data)reasm_data (fulldata)
Оригинальная сегментацияНе сохраняетсяСохраняется
Нормализация позицийНетДа, по диапазону каждого пакета
Порядок сегментовОбратный по всему reasmОбратный только внутри каждого пакета
Между пакетамиОбратный (как часть единого обратного порядка)Прямой (порядок пакетов сохраняется)
seqovlПрименяется к целому reasmНормализуется по текущему пакету
seqovl типМаркерМаркер
blobДаНет
nodropДаНет
optionalДля blob и seqovl_patternТолько для seqovl_pattern
replay_first/replay_dropДа (обрабатывает только первый replay)Нет (обрабатывает каждый пакет)
Пакет без позиций разрезаНе применимо (весь reasm обрабатывается)Отправляется как есть с применёнными опциями

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

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

Если текущий пакет не TCP (UDP, ICMP и т.д.), multidisorder_legacy делает instance_cutoff_shim --- отключает себя для этого потока навсегда. Исключение: related ICMP не вызывает cutoff.

2. Нет blob и nodrop

В отличие от нового multidisorder, multidisorder_legacy не поддерживает аргументы blob и nodrop. Попытка использовать их не вызовет ошибку (Lua просто проигнорирует неизвестные аргументы), но и эффекта не будет:

  • blob --- данные всегда берутся из dis.payload
  • nodrop --- оригинальный пакет всегда дропается при успешной отправке (вердикт VERDICT_DROP из multidisorder_send или из rawsend_payload_segmented)

3. seqovl может не попасть ни в один пакет

Если маркер seqovl разрешается в позицию, которая оказывается на стыке двух пакетов и не попадает точно ни в один диапазон, seqovl будет отменён для всех пакетов. Например:

fulldata = 800 байт
Пакет A: offset=0, len=400   -> range [1, 401)
Пакет B: offset=400, len=400 -> range [401, 801)
seqovl разрешился в позицию 401

Пакет A: 401 >= 1 AND 401 < 401? -> НЕТ (граница не включена)
Пакет B: 401 >= 401 AND 401 < 801? -> ДА -> нормализованный seqovl = 1

Но seqovl=1 означает ovl=0 (seqovl - 1 = 0 в multidisorder_send),
поэтому фактически seqovl не применится.

4. Позиция 1 удаляется после нормализации

delete_pos_1(pos) вызывается после pos_array_normalize. Это значит, что позиция, которая нормализуется в 1 (например, range_low сама по себе), будет удалена. Это правильное поведение --- нельзя разрезать на самом первом байте пакета.

5. Пакет без позиций отправляется как есть

Если после нормализации в пакете не осталось ни одной позиции разреза, пакет не пропускается, а отправляется как есть через rawsend_payload_segmented(desync). Это значит, что к нему всё равно будут применены fooling, ipid, ipfrag и прочие опции.

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

Как и в multisplit / multidisorder, fooling в multidisorder_legacy идёт на все сегменты (включая реальные). Не путайте с fakedsplit / fakeddisorder, где fooling идёт только на фейки.

7. Для однопакетных payload разницы нет

Если payload помещается в один TCP-сегмент (что характерно для большинства HTTP-запросов и многих TLS ClientHello без post-quantum), поведение multidisorder_legacy и нового multidisorder идентично. Разница проявляется только при многопакетных payload.

8. Порядок вызова: каждый пакет --- отдельный вызов

В отличие от нового multidisorder, который обрабатывает весь reasm за один replay_first() и дропает последующие replay через replay_drop(), legacy-функция вызывается для каждого пакета replay. Каждый вызов обрабатывает свой пакет независимо.

9. seqovl может применяться к разным пакетам

Поскольку seqovl нормализуется попакетно, в теории seqovl может применяться к разным пакетам в разных сценариях. Но на практике маркер seqovl разрешается в одну конкретную позицию и попадает ровно в один пакет.


Миграция с nfqws1

Когда использовать multidisorder_legacy

СценарийРекомендация
Миграция рабочего профиля nfqws1 с --dpi-desync=multidisorderИспользуйте multidisorder_legacy для 100%-й совместимости
Новый профиль, однопакетный payloadНет разницы, можно использовать multidisorder
Новый профиль, многопакетный payloadПопробуйте оба варианта --- DPI может реагировать по-разному
Нужен blob или nodropИспользуйте multidisorder (legacy не поддерживает)

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

nfqws1nfqws2 (multidisorder_legacy)
--dpi-desync=multidisorder--lua-desync=multidisorder_legacy
--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,multidisorder \
  --dpi-desync-fooling=md5sig \
  --dpi-desync-split-pos=1,midsld \
  --dpi-desync-split-seqovl=5 \
  --dpi-desync-split-seqovl-pattern=0x1603030000
 
# nfqws2 (эквивалент с multidisorder_legacy):
nfqws2 \
  --payload=tls_client_hello \
    --lua-desync=fake:blob=fake_default_tls:tcp_md5 \
  --payload=http_req \
    --lua-desync=fake:blob=fake_default_http:tcp_md5 \
  --payload=tls_client_hello,http_req \
    --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000

Миграция с нового multidisorder на legacy

Если у вас уже есть профиль с новым multidisorder и вы хотите попробовать legacy-вариант:

# Было (новый multidisorder):
--lua-desync=multidisorder:pos=1,midsld:seqovl=midsld-1:blob=fake_ch:nodrop
 
# Стало (legacy) --- убираем blob и nodrop:
--lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1

Внимание: blob и nodrop потеряны при миграции. Если они необходимы --- legacy не подходит.


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

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

--lua-desync=multidisorder_legacy

Разрезает payload после 2-го байта, отправляет в обратном порядке: сначала основная часть, потом первые 2 байта.

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

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

SNI разрезан пополам. Сначала отправляется вторая половина (с конца SNI), затем первая (с начала ClientHello до середины SNI). DPI получает вторую половину раньше и не может собрать полный hostname.

3. TLS: два разреза с seqovl-маркером

--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1

3 сегмента в обратном порядке. seqovl привязан к позиции midsld-1 --- фейковые данные перекрывают область SNI. Последний отправленный сегмент (1-й байт) переписывает буфер сокета реальными данными.

4. TLS: seqovl с паттерном маскировки под TLS record

--payload=tls_client_hello \
  --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000

5 байт seqovl-области заполнены 0x16 0x03 0x03 0x00 0x00 --- DPI может принять это за начало TLS record.

5. HTTP: разрезы вокруг hostname

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

4 сегмента: отправляются в обратном порядке (после hostname вторая половина hostname первая половина до hostname). DPI видит фрагменты в “неправильном” порядке.

6. Комбинация: fake + multidisorder_legacy

--payload=tls_client_hello \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5 \
  --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1:seqovl_pattern=0x1603030000

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

7. Боевой пример для YouTube (TLS)

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

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

8. Множественные разрезы с арифметикой маркеров

--payload=tls_client_hello \
  --lua-desync=multidisorder_legacy:pos=sniext+1,host,midsld,endhost-2,-10

5 маркеров до 6 сегментов (в зависимости от того, сколько маркеров попадут в текущий пакет после нормализации). Все 6 сегментов отправляются в обратном порядке внутри каждого пакета.

9. С IP-фрагментацией

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

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

10. С TCP timestamp и IP ID

--payload=tls_client_hello \
  --lua-desync=multidisorder_legacy:pos=1,midsld:tcp_ts_up:ip_id=seq:ip_id_conn

TCP timestamp поднят в начало заголовка, IP ID --- последовательные в рамках соединения.

11. Payload=all (обработка unknown протоколов)

--lua-desync=multidisorder_legacy:payload=all:pos=2

Обрабатывает любой payload, включая нераспознанные. Маркеры вроде midsld не разрешатся для unknown, но абсолютные позиции (как 2) сработают.

12. С optional для защиты seqovl_pattern

--payload=tls_client_hello \
  --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1:seqovl_pattern=my_blob:optional

Если blob my_blob не существует --- seqovl всё равно работает, но заполняется нулями вместо паттерна. Без optional отсутствие blob вызвало бы ошибку.


Источники: lua/zapret-antidpi.lua:530-684, lua/zapret-lib.lua, docs/manual.md:4098-4121 из репозитория zapret2.