51 KiB
| date | tags | aliases | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-02-17 |
|
|
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
- Быстрый старт
- Ключевое отличие: попакетная обработка vs целый reasm
- Откуда берутся данные
- Маркеры позиций (pos)
- seqovl --- попакетный скрытый фейк
- Полный список аргументов
- Порядок отправки сегментов
- Место в общем скелете
- Псевдокод алгоритма
- Сравнительная таблица: multidisorder vs multidisorder_legacy
- Нюансы и подводные камни
- Миграция с nfqws1
- Практические примеры
Зачем нужен multidisorder_legacy
Новый multidisorder в zapret2 работает с полным reasm_data целиком: он собирает весь многопакетный payload, нарезает его и отправляет все сегменты в обратном порядке за один проход. Это изменяет оригинальную TCP-сегментацию и порядок следования частей по сравнению с nfqws1.
multidisorder_legacy нужен для случаев, когда:
- Необходима 100%-я совместимость с nfqws1. Если рабочий профиль из nfqws1 использовал
--dpi-desync=multidisorder, legacy-вариант воспроизведёт точно такое же поведение - DPI чувствителен к оригинальной сегментации. Некоторые DPI анализируют размеры TCP-сегментов. Legacy сохраняет оригинальные размеры пакетов, меняя только порядок внутри каждого
- Нужен контроль 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.payload |
dis.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_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 | tls_client_hello |
extlen |
Поле длины всех TLS extensions | tls_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):
- Для каждой позиции проверяет:
pos >= range_low AND pos < range_hi - Если позиция в диапазоне --- нормализует:
pos_normalized = pos - range_low + 1 - Если позиция вне диапазона --- удаляет из массива
Пример:
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--- разрез посередине SLDpos=1,midsld--- два разрезаpos=host,midsld,endhost-2,-10--- четыре разреза
seqovl
- Формат:
seqovl=<marker>(маркер, не просто число!) - Тип: маркер (строка, разрешаемая через
resolve_pos) - По умолчанию: не задан (нет seqovl)
- Описание: Разрешается по fulldata, нормализуется по текущему пакету. Применяется ко 2-му сегменту в оригинальной очередности (предпоследнему отсылаемому). seqovl обязательно должен быть меньше первой позиции разреза (в нормализованном виде), иначе отменяется
- Примеры:
seqovl=5--- 5 байт фейкаseqovl=midsld-1--- seqovl привязан к позиции midsldseqovl=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
| Параметр | Значения | По умолчанию |
|---|---|---|
dir |
in, out, any |
out |
Фильтр по направлению пакета. 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, включаяunknownpayload=tls_client_hello,http_req--- конкретные типыpayload=~unknown--- инверсия: всё кроме unknown
D) Standard fooling
Модификации L3/L4 заголовков. Применяются ко всем отправляемым сегментам.
| Параметр | Описание | Пример |
|---|---|---|
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 |
Заметка: Fooling применяется ко ВСЕМ сегментам (и реальным). Если задать tcp_ack=-66000, сервер отбросит все сегменты. Fooling в multidisorder_legacy имеет смысл только для специфических вещей (tcp_ts_up, ip_id, IPv6 extension headers).
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 в рамках соединения | --- |
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=N |
IPv6: next protocol во 2-м фрагменте | --- |
G) Standard reconstruct
| Параметр | Описание |
|---|---|
badsum |
Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
H) Standard rawsend
| Параметр | Описание |
|---|---|
repeats=N |
Отправить каждый сегмент N раз (идентичные повторы) |
ifout=<iface> |
Интерфейс для отправки (по умолчанию определяется автоматически) |
fwmark=N |
Firewall 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.payload |
dis.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.payloadnodrop--- оригинальный пакет всегда дропается при успешной отправке (вердикт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 не поддерживает) |
Соответствие параметров
| nfqws1 | nfqws2 (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.