multidisorder_legacy --- попакетный обратный порядок TCP-сегментов (zapret2 / nfqws2)
Коротко и по-простому
Когда браузер открывает сайт по HTTPS, первым сообщением с данными он отправляет TLS ClientHello («привет от клиента»). В нём браузер перечисляет, какое шифрование умеет, и открытым текстом называет нужный сайт: имя лежит в поле SNI (Server Name Indication, «указание имени сервера»). У обычного HTTP без шифрования то же имя стоит в строке Host: запроса. ТСПУ (Технические средства противодействия угрозам — оборудование DPI, то есть глубокой инспекции трафика, которое стоит у провайдера) ищет это имя в первом пакете соединения и по нему решает, что делать с соединением дальше.
HTTP-запросы и TLS-соединения передаются по протоколу TCP, а TCP отправляет данные кусками — сегментами. У каждого байта в соединении есть порядковый номер (sequence number, коротко seq), и сервер раскладывает принятые куски в свой буфер по этим номерам. Поэтому ему безразлично, пришли данные одним пакетом или несколькими, и куски, которые приехали раньше времени, он придерживает в буфере, пока не придёт недостающее начало. Почему это гарантирует именно TCP, разобрано в нюансе «Работает только с TCP» в статье про multisplit.
На этой особенности TCP построено целое семейство техник, и multidisorder_legacy — одна из них. Движок Zapret 2 (nfqws2) перехватывает исходящий пакет с ClientHello или HTTP-запросом, режет его данные в заданных местах и отправляет получившиеся куски отдельными TCP-сегментами в обратном порядке, от последнего к первому, каждому ставя правильный seq. Перехваченный оригинал при этом выбрасывается, чтобы те же данные не ушли второй раз. Места разреза задаёт аргумент pos: число («первые N байт уходят отдельным сегментом») или маркер, привязанный к устройству запроса, например midsld — середина домена второго уровня, главной части имени сайта (у www.example.com это example). Маркеров можно перечислить несколько через запятую; все они разобраны в разделе Маркеры позиций (pos). Общий принцип обратного порядка описан в разделе «Обратный порядок отправки — суть disorder» статьи про multidisorder.
Теперь о том, чем legacy-вариант отличается от нового multidisorder. Иногда ClientHello не помещается в один TCP-сегмент (например, если в нём есть ключи для post-quantum шифрования) и уходит из системы двумя и более пакетами; такой запрос называют многопакетным. Тогда nfqws2 придерживает пакеты, склеивает их данные в общий буфер (reasm_data, reasm — от reassembly, «повторная сборка») и затем перепроигрывает (replay): вызывает desync-функцию на каждой придержанной части, но уже с собранным целым на руках. Подробно это описано в разделе reasm» статьи про multidisorder. Новый multidisorder режет весь склеенный буфер целиком и разворачивает порядок для всего запроса: последний кусок ClientHello уйдёт первым, даже если исходно он приехал третьим пакетом. multidisorder_legacy повторяет поведение nfqws1 — первой версии zapret: он обрабатывает каждый исходный пакет отдельно, разворачивает порядок кусков только внутри этого пакета, а сами пакеты идут друг за другом в прямом порядке. Исходные размеры пакетов при этом сохраняются. Если запрос уместился в один пакет, разницы между двумя функциями нет: подробнее — в нюансе «Для однопакетных payload разницы нет».
Отличие от multidisorder заметно, только когда ClientHello не помещается в один пакет. На схеме ClientHello длиной 800 байт ядро отправило двумя пакетами: A (байты 0–499) и B (байты 500–799); разрезы стоят на позициях 200 и 600. multidisorder_legacy режет каждый пакет отдельно и разворачивает части только внутри него: A2, A1, затем B2, B1. Новый multidisorder развернул бы весь поток целиком: 600–800, 200–600, 0–200. Как читать схему: строки — пакеты в порядке отправки, подписанные в тех же обозначениях, что и таблицы в этой статье; по горизонтали — место данных в TCP-потоке (номер последовательности, seq); в колонке справа — что сделал с пакетом сервер; внизу — буфер сервера, в который принятые куски встают по своим seq.
Сервер получает все куски, раскладывает их по seq и видит ровно тот запрос, который отправил браузер, поэтому отвечает как обычно. DPI видит другое: имя сайта разорвано границами кусков, и куски идут в обратном порядке. Если DPI разбирает пакеты по одному или не умеет пересобирать поток, пришедший не по порядку, полного имени он не найдёт. DPI, который пересобирает поток, теоретически может собрать запрос, но многие на практике ограничены буфером или таймаутом и не дожидаются всех частей (так это описано в разделе «Почему disorder ломает DPI»). Отдельный довод в пользу legacy: некоторые DPI анализируют размеры TCP-сегментов, а legacy сохраняет размеры исходных пакетов и меняет только порядок внутри каждого.
К разрезу можно добавить приём seqovl — скрытый фейк. Функция дописывает слева ко второму по порядку куску (в исходной нарезке) несколько байт-заглушки, по умолчанию нулей, и сдвигает его seq назад на ту же длину; этот кусок уходит предпоследним. Последним уходит самый первый кусок и переписывает заглушку настоящими данными в буфере сервера, а DPI может принять заглушку, например, за начало TLS record (заголовка записи TLS). В legacy-варианте seqovl применяется только к тому пакету, в который попало его место; механика — в разделе seqovl --- попакетный скрытый фейк. Про Windows-серверы, на которых этот приём в режиме disorder не работает, сказано в разделе «Ограничение: Windows-серверы».
Самая короткая команда включает multidisorder_legacy со значениями по умолчанию: один разрез после второго байта (pos=2), обработка только распознанных протоколов (payload=known) и только исходящих пакетов (dir=out). Строку --lua-desync=… пишут в профиль — набор правил Zapret 2 для определённого трафика:
--lua-desync=multidisorder_legacyДля HTTPS берут типовой вариант --payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld; здесь --payload=tls_client_hello значит «срабатывать только на TLS ClientHello». Остальные готовые команды собраны в разделах Быстрый старт и Практические примеры.
Выбирать legacy стоит, когда вы переносите рабочий профиль из nfqws1 с ключом --dpi-desync=multidisorder и нужна полная совместимость, либо когда DPI чувствителен к исходной сегментации. Если нужны аргументы blob или nodrop, legacy не подойдёт: он их не поддерживает, и тогда берут новый multidisorder. Работает legacy, как и вся семья, только с TCP. Порядок выбора между двумя вариантами сведён в таблицу раздела Когда использовать multidisorder_legacy, а полное сравнение — в разделе Сравнительная таблица. Ближайшие соседи: multisplit режет так же, но отправляет куски по порядку; fakedsplit и fakeddisorder делают один разрез и подмешивают фейковые сегменты — отдельные пакеты-обманки, которые нарочно портят (такая порча называется fooling, см. Что такое fooling), чтобы сервер их отбросил, а DPI прочитал. Отдельный пакет-обманку с ложными данными отправляет функция fake, и с ней multidisorder_legacy можно комбинировать (см. пример 6).
Оглавление
- Коротко и по-простому
- Справка: где функция в коде и родственники
- Зачем нужен multidisorder_legacy
- Быстрый старт
- Ключевое отличие: попакетная обработка vs целый reasm
- Откуда берутся данные
- Маркеры позиций (pos)
- seqovl --- попакетный скрытый фейк
- Полный список аргументов
- Порядок отправки сегментов
- Место в общем скелете
- Псевдокод алгоритма
- Сравнительная таблица: multidisorder vs multidisorder_legacy
- Нюансы и подводные камни
- Миграция с nfqws1
- Практические примеры
- 📚 См. также
Справка: где функция в коде и родственники
Паспорт функции для тех, кто сверяется с исходниками zapret2: в каком файле и на какой строке лежит код, какой ключ первой версии zapret она заменяет и как объявлена.
Файл: lua/zapret-antidpi.lua:649
nfqws1 эквивалент: --dpi-desync=multidisorder (полная совместимость)
Сигнатура: function multidisorder_legacy(ctx, desync)
multidisorder_legacy --- реализация алгоритма multidisorder, полностью совместимая с nfqws1. Ключевое отличие от нового multidisorder: legacy-вариант работает попакетно --- он обрабатывает каждую оригинальную часть replay отдельно, сохраняя исходную TCP-сегментацию. Сегменты отправляются в обратном порядке только внутри каждого пакета, а между пакетами --- в прямом порядке (как они пришли).
Проще говоря
Payload — полезные данные пакета без сетевых заголовков: сам HTTP-запрос или сам ClientHello. Reasm — те же данные, собранные движком из нескольких пакетов, если запрос в один пакет не влез. Replay — перепроигрывание: движок по очереди вызывает функцию на каждой придержанной части. Слова «попакетно» и «по всему reasm» описывают, как функция с этим обходится: legacy обрабатывает каждую исходную часть отдельно, новый
multidisorderберёт собранное целое. Попакетная обработка подробно разобрана в разделе Ключевое отличие: попакетная обработка vs целый reasm, а место функции среди остальных — в разделе Место в общем скелете.VERDICT_DROP— решение «перехваченный пакет не отправлять»; как движок складывает такие решения от разных функций, описано в стадии 8 жизненного цикла.
Родственные функции: multisplit (прямой порядок), multidisorder (новый, по всему reasm), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок).
Общий для всех техник дурения порядок работы — восемь стадий от отсева чужого транспорта до вердикта — разобран в жизненный цикл desync-функции; здесь описано только то, чем multidisorder_legacy от этого скелета отличается.
Зачем нужен multidisorder_legacy
Сначала о том, откуда вообще берётся многопакетность. Запрос, который не поместился в один TCP-сегмент (например, большой TLS ClientHello), уходит несколькими пакетами; nfqws2 придерживает их, собирает в буфер reasm_data и перепроигрывает через desync-функции. Как это устроено, разобрано в разделе reasm» статьи про multidisorder. Новый multidisorder и его legacy-вариант по-разному обходятся с этим буфером, и весь смысл 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
Проще говоря
Новый
multidisorderсклеивает многопакетный запрос в одно целое и разворачивает порядок всех кусков разом.multidisorder_legacyне склеивает данные для отправки: каждый исходный пакет он режет сам и разворачивает только внутри него, как делал nfqws1. Выбирать его стоит, когда нужна точная совместимость с nfqws1, когда DPI анализирует размеры TCP-сегментов или когдаseqovlдолжен действовать на уровне пакета, а не всего запроса. Сам приёмseqovlобъяснён в разделе seqovl --- попакетный скрытый фейк.
Быстрый старт
Ниже три готовые строки для профиля Zapret 2, от самой короткой к более сложной. Флаг --payload=… работает как фильтр для всех вызовов --lua-desync, которые идут после него (до следующего --payload или до конца профиля): они срабатывают только на данных указанного типа. Поэтому --payload пишут перед --lua-desync (подробно — в последовательность аргументов).
Минимально (разрез по позиции 2, payload=known, dir=out):
--lua-desync=multidisorder_legacyЗдесь payload=known означает «только распознанные протоколы», а dir=out — «только исходящие пакеты»; оба аргумента разобраны в разделах B) Standard direction и C) Standard payload.
Типовой TLS-разрез посередине SNI:
--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsldФункция срабатывает только на TLS ClientHello и режет каждый пакет по маркерам 1 (после первого байта) и midsld (посередине домена второго уровня). Для ClientHello, уместившегося в один пакет, это три сегмента, и они уходят в обратном порядке. Если пакетов несколько, разрезы делятся между ними, и обратный порядок действует внутри каждого пакета (см. A) Собственные аргументы multidisorder_legacy).
TLS с seqovl (маркер):
--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1Здесь к разрезу добавлен seqovl — скрытый фейк. Он задан маркером midsld-1, а не числом. Как он действует и почему применяется только к одному пакету, разобрано в разделах seqovl --- маркер, а не число и Нормализация seqovl по пакету.
Ключевое отличие: попакетная обработка vs целый reasm
Это центральное различие между multidisorder и multidisorder_legacy. Рассмотрим его на примере большого TLS ClientHello, который не влезает в один TCP-сегмент и разбит на 2 пакета.
Сначала о словах. Многопакетный запрос — запрос, который не поместился в один TCP-сегмент и ушёл из системы двумя и более пакетами; ниже их два, A и B. Reasm (reasm_data) — те же данные, склеенные в одну цепочку байт, как если бы пакет был один. Позиции разрезов (в примере 200 и 600) считаются по этой склеенной цепочке. А вот отправляют два варианта функции по-разному: новый режет саму цепочку, а legacy возвращается к исходным пакетам.
На схемах offset — число байт запроса, которые идут перед пакетом (у A — 0, у B — 500, то есть длина A); в коде оно называется reasm_offset, и от него отсчитывается range_low. Число seq у частей на схемах отсчитывается от начала запроса и показывает, в какое место потока встанет кусок на стороне сервера. Обе схемы построены одинаково, поэтому их удобно сравнивать построчно.
Диаграмма: новый multidisorder (весь reasm)
Здесь нужно проследить, как исходные пакеты теряют свои границы. nfqws2 склеивает A и B в буфер на 800 байт, режет его в позициях 200 и 600 на три части и отправляет их в порядке 3, 2, 1.
Исходные пакеты от ядра:
Пакет 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) ПОТЕРЯНА.
Часть 2 составлена из конца пакета A и начала пакета B, поэтому исходная граница между пакетами (500 байт + 300 байт) исчезает, и порядок 3, 2, 1 распространяется на весь запрос. Это и подчёркивает последняя строка схемы: оригинальная сегментация потеряна.
Диаграмма: multidisorder_legacy (попакетно)
Здесь функция вызывается для каждого пакета отдельно. Для каждого пакета она держит в руках два набора данных: сам пакет (data, его режут и отправляют) и склеенную цепочку целиком (fulldata, по ней только вычисляются места разрезов); подробнее — в разделе Откуда берутся данные. Затем позиции нормализуются: переводятся в номера внутри текущего пакета, а те, что в пакет не попали, удаляются. Формула и второй пример — в разделе Нормализация позиций по пакету.
Исходные пакеты от ядра:
Пакет 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) СОХРАНЕНА.
Обратный порядок ТОЛЬКО внутри каждого пакета.
Между пакетами --- ПРЯМОЙ порядок.
Проще говоря
Сначала функция обрабатывает пакет A: позиция 200 ему подходит, позиция 600 нет. Части A2 и A1 уходят в обратном порядке. Потом пакет B: подходит 600, а 200 отбрасывается; уходят B2 и B1. Пакеты идут друг за другом как пришли, исходные размеры 500 и 300 сохранены, а обратный порядок остаётся внутри каждого пакета. Итог на проводе: A2, A1, B2, B1.
Откуда берутся данные
Чтобы понять, что режется и по чему считаются места разреза, нужно различать два набора данных. Первый — пакет, который прямо сейчас перехватил движок: его полезное содержимое лежит в desync.dis.payload (dis — диссект, то есть пакет, заранее разобранный движком на поля; его устройство описано в структура desync и диссекта). Второй — весь многопакетный запрос, собранный из частей, desync.reasm_data (как он собирается, см. Приём многопакетных пейлоадов). multidisorder_legacy использует оба одновременно.
В multidisorder_legacy используются два источника данных одновременно:
data = desync.dis.payload -- текущий пакет (для нарезки и отправки)
fulldata = desync.reasm_data -- полный реассемблированный payload (для resolve_pos)
Почему два источника:
fulldataнужен для разрешения маркеров (midsld,hostи т.д.), потому что они вычисляются по полному payload (SNI может быть виден только в контексте всего ClientHello)data--- это то, что реально нарезается и отправляется: именно текущий оригинальный пакет
Простыми словами: маркер вроде midsld нужно искать во всём ClientHello, потому что имя сайта может оказаться в другом пакете, чем тот, что сейчас перехвачен. А резать и отправлять можно только то, что реально пришло в этом пакете.
Отличие от нового 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 на произвольные данные.
Проще говоря
Legacy режет только то, что пришло в текущем пакете, а места разрезов ищет по всему запросу. Blob — заготовленные данные, которые можно подсунуть вместо настоящих (blob); новый
multidisorderих принимает, legacy нет. Как выбор данных устроен у нового варианта, описано в разделе «Откуда берутся данные для нарезки», а общая цепочка blob → reasm → payload — в стадии 4 жизненного цикла.
Маркеры позиций (pos)
pos --- главный аргумент. Определяет где внутри payload будет произведён разрез. Задаётся как строка со списком маркеров через запятую.
Маркер — это место в данных, где надо сделать разрез: число байт от начала или от конца либо «смысловая» позиция вроде midsld. Здесь разобраны только те особенности маркеров, которые нужны legacy-варианту. Полный разбор с примерами и тем, как маркеры разрешаются в коде, — в разделах «Маркеры позиций (pos)» у multidisorder и у multisplit. Отсчёт в командной строке идёт от нуля: pos=N значит «первые N байт уходят отдельным сегментом».
Типы маркеров
Маркеры бывают трёх видов. Абсолютные отсчитывают байты от начала или от конца данных и не зависят от их содержимого. Относительные ищут место по смыслу — начало имени хоста, середину домена — и поэтому работают, только если движок распознал протокол.
| Тип | Описание | Пример |
|---|---|---|
| Абсолютный положительный | Смещение от начала payload, считая с 0. pos=N → первые N байт становятся отдельным сегментом | 1, 5, 100 |
| Абсолютный отрицательный | Смещение от конца payload. -1 = последний байт | -1, -10, -50 |
| Относительный | Логическая позиция внутри распознанного payload. Привязана к структуре протокола | midsld, host, sniext |
Относительные маркеры
В правой колонке указано, в каких типах payload маркер имеет смысл: http_req — HTTP-запрос, tls_client_hello — TLS ClientHello. Как движок определяет тип, описано в заметке о типах payload. SLD (second-level domain) — домен второго уровня: в www.example.com это example. SNI extension — расширение ClientHello, в котором браузер передаёт имя сайта.
| Маркер | Описание | Для каких 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 (абсолютный, не относительный)
Нормализация позиций по пакету
Когда запрос состоит из нескольких пакетов, места разрезов сначала ищутся по всему запросу, а затем каждый пакет забирает себе только те из них, что попали в его границы, и пересчитывает их в номера внутри себя. Так работает нормализация. Границы пакета задаются двумя числами, range_low и range_hi.
Это ключевая особенность 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]
Проще говоря
Из нескольких пакетов каждый оставляет себе только «свои» места разрезов. В примере пакет A получает 200 и 450, а 600 у него удаляется; пакет B удаляет 200 и 450 и получает единственную позицию 100 (600 − 501 + 1). У пакета A
range_lowравен 1, у пакета B — 501. Такой пересчёт отсутствует в новом multidisorder, где режется весь запрос сразу.
Важные нюансы 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, без разреза
Проще говоря
Здесь собраны условия, при которых разрез не происходит или происходит не так, как ждёшь. Разрез в самом начале данных бессмысленен, потому что первый кусок вышел бы пустым, поэтому он выбрасывается. Одинаковые места сливаются в одно, порядок записи не важен, а маркер, который в этом типе данных найти нельзя, просто пропадает из списка. Если в пакете не осталось ни одного места разреза, он уходит как есть, но с включёнными fooling, ipid и прочими опциями. Как это выглядит на примере, см. нюансы 4 и 5.
seqovl --- попакетный скрытый фейк
seqovl (Sequence Overlap, «перекрытие по номерам последовательности») — приём, при котором в настоящий сегмент слева дописываются фальшивые байты, а его seq сдвигается назад на ту же длину. Как приём работает в других функциях, разобрано в разделах «seqovl — скрытый фейк внутри сегмента» у multisplit (сервер выбрасывает лишнее, потому что оно левее окна приёма) и «seqovl — перезапись буфера сокета через перекрытие» у multidisorder (лишнее лежит в буфере и потом переписывается настоящими данными). Здесь описана специфика legacy: приём применяется на уровне пакета.
seqovl --- маркер, а не число
Слово «маркер» здесь то же, что в разделе Маркеры позиций (pos): место в запросе можно задать не только числом, но и смысловой позицией. Подробнее о том же в разделе про seqovl как маркер у нового варианта.
В отличие от multisplit, где seqovl принимает только число, в multidisorder_legacy (и в новом multidisorder) seqovl является маркером. Это значит, что можно написать:
:seqovl=midsld-1 -- seqovl = (позиция midsld) - 1
:seqovl=host -- seqovl = позиция host
:seqovl=5 -- seqovl = 5 (число тоже маркер)Проще говоря
seqovlможно записать так же, как места разрезов:midsld-1,hostили просто число. Как и места разрезов, маркер разрешается по всему запросу, а не по одному пакету.
Маркер seqovl разрешается через resolve_pos(fulldata, l7payload, seqovl) --- по полному reasm, точно так же, как и позиции разреза.
Нормализация seqovl по пакету
После того как маркер 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попадает в один пакет — в примере в пакет A. Для пакета B пересчитать его нельзя, и тамseqovlпросто выключается. Пересчёт числа 245 в пакете A по формуле из примера выше даёт 245 − 1 + 1 = 245, потому что для пакета Arange_lowравен 1.
Следствие: seqovl применяется только к тому пакету, в диапазон которого попала нормализованная позиция seqovl. Для остальных пакетов seqovl не действует.
seqovl_pattern
Паттерн — то, чем заполняются лишние байты слева. Blob — заготовленные данные под именем; как их объявлять, описано в заметке blob. О том же аргументе в других функциях см. seqovl_pattern у multidisorder.
Паттерн заполнения seqovl-области. По умолчанию --- 0x00 (нули). Задаётся как имя blob:
:seqovl_pattern=0x1603030000 -- inline hex
:seqovl_pattern=my_pattern_blob -- предзагруженный blobЕсли optional задан и blob seqovl_pattern отсутствует --- используется нулевой паттерн (seqovl не отменяется).
Полный список аргументов
Аргументы делятся на собственные аргументы multidisorder_legacy (раздел A) и стандартные (разделы B–H): их понимают и другие функции дурения, а общий обзор есть в desync.
Формат вызова:
--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.
Проще говоря
Собственных аргументов здесь четыре:
pos(где резать),seqovl(где и сколько фальшивых байт приклеить),seqovl_pattern(чем их заполнить) иoptional(мягкий режим для паттерна). Аргументовblobиnodropу legacy нет: указав их, ничего не сломаешь, но и эффекта не получишь. Причина и последствия — в нюансе 2. Нет blob и nodrop; проnodropу других функций см. нюанс 4 в multisplit.
B) Standard direction
Направление говорит, в какую сторону идёт пакет: от вашего компьютера к серверу (исходящий) или обратно (входящий).
| Параметр | Значения | По умолчанию |
|---|---|---|
dir | in, out, any | out |
Фильтр по направлению пакета. multidisorder_legacy по умолчанию работает только с исходящими (out).
dir=out--- только исходящие (от клиента к серверу)dir=in--- только входящие (от сервера к клиенту)dir=any--- оба направления
При первом вызове с указанным dir функция делает direction_cutoff_opposite --- отсекает себя от противоположного направления. Cutoff (отсечение) значит, что этот экземпляр функции отключается от пакетов противоположного направления и движок перестаёт зря вызывать для них Lua; подробно — в стадии 2 жизненного цикла.
C) Standard payload
Тип payload — название распознанного движком протокола: http_req, tls_client_hello и так далее; как движок его определяет, см. заметку о типах payload.
| Параметр | Значения | По умолчанию |
|---|---|---|
payload | список типов через запятую | known |
Фильтр по типу payload на уровне Lua. Это дополнительный фильтр к --payload=... на уровне профиля.
payload=known--- только распознанные протоколы (http_req,tls_client_hello,quic_initialи т.д.)payload=all--- любой payload, включаяunknownpayload=tls_client_hello,http_req--- конкретные типыpayload=~unknown--- инверсия: всё кроме unknown
Проще говоря: фильтр --payload= на уровне профиля работает в быстром ядре программы, написанном на C, а аргумент payload= проверяется уже в Lua, то есть медленнее. Поэтому основной отбор лучше делать на уровне профиля (то же самое сказано в разделе C) Standard payload у multisplit).
D) Standard fooling
Fooling — намеренная порча заголовков пакета, обычно для того, чтобы сервер отбросил фейк, а DPI его прочитал (подробно — Что такое fooling). Ниже — модификации L3/L4 заголовков, то есть заголовков IP (сетевой уровень) и TCP (транспортный уровень).
Модификации 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).
Что это значит, сказано в нюансе 6. Fooling применяется ко ВСЕМ сегментам: в отличие от fakedsplit и fakeddisorder, где fooling идёт только на фейки, здесь он достаётся всем сегментам, включая настоящие.
E) Standard ipid
IP ID — идентификатор в заголовке IPv4-пакета. Эти параметры задают, какие значения получат отправляемые сегменты.
| Параметр | Описание | По умолчанию |
|---|---|---|
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-фрагментация — деление пакета на части уже на уровне IP; получатель склеивает их обратно на том же уровне.
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
Реконструкция — сборка готового raw-пакета («сырого», со всеми заголовками) перед отправкой.
| Параметр | Описание |
|---|---|
badsum | Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
H) Standard rawsend
Rawsend — отправка готовых сегментов в сеть самой функцией. Эти параметры управляют отправкой.
| Параметр | Описание |
|---|---|
repeats=N | Отправить каждый сегмент N раз (идентичные повторы) |
ifout=<iface> | Интерфейс для отправки (по умолчанию определяется автоматически) |
fwmark=N | Firewall mark (только Linux, nftables/iptables) |
Порядок отправки сегментов
multidisorder_legacy использует общую функцию multidisorder_send, которая отправляет сегменты в обратном порядке (от последнего к первому). Но поскольку legacy обрабатывает каждый пакет отдельно, обратный порядок действует только внутри одного пакета.
Общий обратный порядок описан в разделе «Обратный порядок отправки — суть disorder». Ниже три примера: один пакет, два пакета и seqovl.
Пример: однопакетный payload
Если запрос уместился в один пакет, legacy ведёт себя так же, как новый multidisorder: части отправляются от последней к первой.
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 <-- последней
Здесь три места разреза дают четыре части. Первой уходит самая правая, последней — самая левая. Метка seq у каждой части равна её смещению в исходных данных.
Пример: многопакетный 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 --- ПРЯМОЙ порядок.
Внутри каждого --- ОБРАТНЫЙ.
Проще говоря
Место разреза 550 принадлежит пакету B, который начинается со смещения 400. Внутри пакета B оно превращается в 150 (550 − 400). Пакет A уходит целиком раньше пакета B, а внутри каждого пакета порядок обратный: A2, A1, B2, B1.
Пример с seqovl
Теперь то же с 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-стека сервера.
Как читать пример. Для seqovl=80 перекрытие ovl на единицу меньше (79 байт): так указано на схеме (ovl=79), а причина — в разделе «Нюанс реализации: ovl = seqovl - 1». У части 2 на схеме стоит seq=21 вместо 100, а перед её настоящими данными (с байта 100) идут 79 байт паттерна; 100 − 79 = 21. Часть 1 уходит последней, и, как сказано на схеме, она переписывает ложные данные из seqovl_pattern в буфере TCP-стека сервера. Что происходит в таком буфере по шагам, показано в хронологии буфера у multidisorder.
Место в общем скелете
Тело 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: сохраняет исходную сегментацию потока и переставляет сегменты только внутри каждого отдельного пакета. Поэтому при многопакетных запросах порядок сегментов у двух функций получается разным.
Проще говоря
Стадия 4 — выбор данных. Большинство функций берут сначала blob, потом собранный reasm, потом текущий пакет (см. стадию 4); legacy режет только текущий пакет, а reasm нужен ему для разрешения маркеров. Стадия 6 — развилка перепроигрывания: обычная функция работает на первой части replay и дропает остальные, а legacy вызывается на каждой части (стадия 6). Всё остальное — как у других функций.
Псевдокод алгоритма
Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в жизненный цикл desync-функции:
Код ниже читается сверху вниз как рассказ: проверить транспорт, определить направление, взять текущий пакет и весь запрос, вычислить границы пакета, найти места разрезов, пересчитать их, при наличии seqovl пересчитать и его, отправить части. Комментарии в коде нумеруют шаги самой функции. Тот же порядок в общем виде описан в жизненном цикле 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вызываетreplay_first()иreplay_drop(), чтобы обработать запрос один раз и отбросить остальные части. Legacy этих вызовов не делает: движок зовёт его на каждой части, и каждая часть обрабатывается сама по себе. Именно это отличие и называют попакетной обработкой.
Сравнительная таблица: multidisorder vs multidisorder_legacy
В таблице по строкам сопоставлены два варианта функции. Последний столбец — multidisorder_legacy, средний — новый multidisorder; ячейки со словами «Да»/«Нет» отвечают на вопрос «поддерживается ли». Разбор причин — в разделах Ключевое отличие и Нюансы и подводные камни.
| Аспект | 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 обрабатывается) | Отправляется как есть с применёнными опциями |
Проще говоря
Главное в таблице — три строки: «Оригинальная сегментация», «Порядок сегментов» и «Между пакетами». Новый вариант теряет исходное деление на пакеты и разворачивает всё разом; legacy сохраняет пакеты и разворачивает только внутри каждого. Остальные строки — следствия этого (нормализация позиций, отсутствие replay-развилки) и разница в наборе аргументов (
blob,nodrop).
Нюансы и подводные камни
1. Работает только с TCP
Если текущий пакет не TCP (UDP, ICMP и т.д.), multidisorder_legacy делает instance_cutoff_shim --- отключает себя для этого потока навсегда. Исключение: related ICMP не вызывает cutoff.
Почему эта техника вообще возможна только для TCP — у него сквозная нумерация байтов и сервер собирает поток по номерам — разобрано в нюансе «Работает только с TCP — и почему» у multisplit; стадия отсева описана в стадии 1 жизненного цикла. Instance cutoff (отсечение экземпляра) — отключение этого экземпляра функции от потока: как сказано в стадии 1 жизненного цикла, движок после этого не вызывает Lua для пакетов данного соединения.
2. Нет blob и nodrop
В отличие от нового multidisorder, multidisorder_legacy не поддерживает аргументы blob и nodrop. Попытка использовать их не вызовет ошибку (Lua просто проигнорирует неизвестные аргументы), но и эффекта не будет:
blob--- данные всегда берутся изdis.payloadnodrop--- оригинальный пакет всегда дропается при успешной отправке (вердиктVERDICT_DROPизmultidisorder_sendили изrawsend_payload_segmented)
Проще говоря: blob подсовывал бы вместо настоящих данных заготовленные, а nodrop оставлял бы оригинал в отправке (и он ушёл бы второй раз). Legacy делает и то, и другое по-своему: данные берёт из пакета, оригинал дропает всегда. О дублировании при nodrop см. нюанс 4 у multisplit.
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 не применится.
Проще говоря
В примере место
seqovlлежит ровно на границе пакетов: в пакет A оно не входит (граница A не включена), в пакете B оно нормализуется в 1, а это означает нулевое перекрытие, поэтому фейк фактически не применяется. Причина нулевого перекрытия описана в разделе «Нюанс реализации: ovl = seqovl - 1»; правило «seqovlдолжен быть меньше первой позиции разреза» — в разделе [[Zapret2/desync/multidisorder#Валидация seqovl: должен быть меньше pos[1]|«Валидация seqovl: должен быть меньше pos[1]»]].
4. Позиция 1 удаляется после нормализации
delete_pos_1(pos) вызывается после pos_array_normalize. Это значит, что позиция, которая нормализуется в 1 (например, range_low сама по себе), будет удалена. Это правильное поведение --- нельзя разрезать на самом первом байте пакета.
Проще говоря: если пересчёт поставил разрез на самое начало текущего пакета, разрез удаляется, потому что первая часть вышла бы пустой. Так же поступает и multisplit (см. его нюанс 2).
5. Пакет без позиций отправляется как есть
Если после нормализации в пакете не осталось ни одной позиции разреза, пакет не пропускается, а отправляется как есть через rawsend_payload_segmented(desync). Это значит, что к нему всё равно будут применены fooling, ipid, ipfrag и прочие опции.
Проще говоря: пакет без разрезов не теряется. Он уходит целым, но с теми настройками порчи заголовков и IP ID, которые вы задали, и оригинал дропается.
6. Fooling применяется ко ВСЕМ сегментам
Как и в multisplit / multidisorder, fooling в multidisorder_legacy идёт на все сегменты (включая реальные). Не путайте с fakedsplit / fakeddisorder, где fooling идёт только на фейки.
Подробнее про то, почему порча заголовков у таких функций вредна, — в нюансе 6 у multisplit; про сами опции — в ts-and-fooling.
7. Для однопакетных payload разницы нет
Если payload помещается в один TCP-сегмент (что характерно для большинства HTTP-запросов и многих TLS ClientHello без post-quantum), поведение multidisorder_legacy и нового multidisorder идентично. Разница проявляется только при многопакетных payload.
Проще говоря: разница между legacy и новым вариантом видна, только когда запрос состоит из нескольких пакетов. Если пакет один, подойдёт любой из двух.
8. Порядок вызова: каждый пакет --- отдельный вызов
В отличие от нового multidisorder, который обрабатывает весь reasm за один replay_first() и дропает последующие replay через replay_drop(), legacy-функция вызывается для каждого пакета replay. Каждый вызов обрабатывает свой пакет независимо.
Как устроена развилка replay у нового варианта, см. reasm. Проще говоря: legacy не «решает» за весь запрос, а отвечает за один пакет и забывает о нём.
9. seqovl может применяться к разным пакетам
Поскольку seqovl нормализуется попакетно, в теории seqovl может применяться к разным пакетам в разных сценариях. Но на практике маркер seqovl разрешается в одну конкретную позицию и попадает ровно в один пакет.
Проще говоря: механизм допускает, что seqovl попадёт в разные пакеты, но на деле его место — одно, поэтому и пакет один.
Миграция с nfqws1
Этот раздел для тех, кто переносит настройки из первой версии zapret (программа nfqws) во вторую (nfqws2). В первой версии техника выбиралась ключом --dpi-desync=…, а её параметры — отдельными ключами --dpi-desync-…; во второй всё это записывается аргументами одного вызова --lua-desync=multidisorder_legacy:…. Именно для такого переноса legacy-вариант и создан.
Когда использовать multidisorder_legacy
Ниже — короткая шпаргалка выбора между двумя вариантами; полное сравнение — в разделе Сравнительная таблица.
| Сценарий | Рекомендация |
|---|---|
Миграция рабочего профиля nfqws1 с --dpi-desync=multidisorder | Используйте multidisorder_legacy для 100%-й совместимости |
| Новый профиль, однопакетный payload | Нет разницы, можно использовать multidisorder |
| Новый профиль, многопакетный payload | Попробуйте оба варианта --- DPI может реагировать по-разному |
| Нужен blob или nodrop | Используйте multidisorder (legacy не поддерживает) |
Проще говоря
Переносишь рабочий профиль nfqws1 — бери legacy. Пишешь новый профиль и запрос однопакетный — разницы нет. Запрос многопакетный — пробуй оба и смотри, как отреагирует DPI. Нужны
blobилиnodrop— только новыйmultidisorder.
Соответствие параметров
В таблице слева — ключ первой версии, справа — то, как он записывается во второй.
| 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 фейк и разрез задавались одним ключом --dpi-desync=fake,multidisorder. В nfqws2 каждая техника становится отдельным инстансом --lua-desync (инстанс — одна запись --lua-desync=… в профиле), а фейки для TLS и HTTP — разными инстансами под своими --payload, потому что им нужны разные стандартные blob-ы:
# 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Обратите внимание, что --dpi-desync-fooling=md5sig из nfqws1 в nfqws2 записан как аргумент tcp_md5 у fake, а не у multidisorder_legacy. В Миграции с fake + multidisorder то же разобрано для нового варианта.
Миграция с нового 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 при переходе исчезают, а всё остальное остаётся.
Внимание: 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=midsldSNI разрезан пополам. Сначала отправляется вторая половина (с конца SNI), затем первая (с начала ClientHello до середины SNI). DPI получает вторую половину раньше и не может собрать полный hostname.
3. TLS: два разреза с seqovl-маркером
--payload=tls_client_hello --lua-desync=multidisorder_legacy:pos=1,midsld:seqovl=midsld-1Про seqovl как маркер — раздел выше.
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=0x1603030000TLS record — заголовок записи TLS. Пять байт 0x16 0x03 0x03 0x00 0x00 из примера — паттерн, который DPI может принять за начало TLS record.
5 байт seqovl-области заполнены 0x16 0x03 0x03 0x00 0x00 --- DPI может принять это за начало TLS record.
5. HTTP: разрезы вокруг hostname
--payload=http_req --lua-desync=multidisorder_legacy:pos=host,midsld,endhost4 сегмента: отправляются в обратном порядке (после 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Отдельный пакет-фейк — работа функции fake; tcp_md5 — способ испортить его так, чтобы сервер отбросил (см. Что такое fooling).
Сначала отправляется фейковый 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,midsld11 фейков подряд + реальный payload разрезан и отправлен в обратном порядке. Полная совместимость с поведением nfqws1.
8. Множественные разрезы с арифметикой маркеров
--payload=tls_client_hello \
--lua-desync=multidisorder_legacy:pos=sniext+1,host,midsld,endhost-2,-105 маркеров → до 6 сегментов (в зависимости от того, сколько маркеров попадут в текущий пакет после нормализации). Все 6 сегментов отправляются в обратном порядке внутри каждого пакета.
9. С IP-фрагментацией
--payload=tls_client_hello \
--lua-desync=multidisorder_legacy:pos=1,midsld:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32Раздел с параметрами IP-фрагментации — F) Standard ipfrag.
Каждый 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Разделы с параметрами: D) Standard fooling и E) Standard ipid.
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 вызвало бы ошибку.
📚 См. также
- desync — обзор всех функций
--lua-desyncи общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты - жизненный цикл desync-функции — восемь стадий, общих для всех техник дурения: отсев транспорта, выбор данных, replay, вердикты
- структура desync и диссекта — устройство таблицы
desync, диссекта и приёма многопакетных запросов - multidisorder — новый вариант: тот же обратный порядок, но по всему reasm, с поддержкой
blobиnodrop - multisplit — тот же разрез, но куски идут по порядку
- fakedsplit · fakeddisorder — разрез по одной позиции с подмешиванием фейковых сегментов
- fake — отдельный фейковый пакет, с которым
multidisorder_legacyчаще всего комбинируют - blob — как объявлять и передавать данные для аргумента
seqovl_pattern - payload — распознавание типов протоколов, от которого зависит работа относительных маркеров
- ts-and-fooling — опции fooling; в
multidisorder_legacyосмысленны лишь немногие из них - verify-strategy — как честно проверить, работает ли подобранная комбинация
- последовательность аргументов — как выстраивается цепочка инстансов в профиле
- profile — где
--lua-desyncживёт среди остальных настроек - DPI и ТСПУ — как устроена инспекция трафика, против которой работает
multidisorder_legacy
Источники:
lua/zapret-antidpi.lua:530-684,lua/zapret-lib.lua,docs/manual.md:4098-4121из репозитория zapret2.
🤖 Эти статьи открыты — можно обучать на них ИИ
При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.