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.

multidisorder_legacy: обратный порядок внутри каждого пакета Карта потока для multidisorder_legacy с pos=200,600: ClientHello длиной 800 байт пришёл от ядра двумя пакетами, A (0–499) и B (500–799). Каждый пакет режется отдельно и уходит задом наперёд: A2, A1, затем B2, B1. пакет в порядке отправки место в потоке (seq) → сервер 0 200 A|B 500 600 800 #1 A2 seq=200 len=300 ····youtube. ждёт в буфере #2 A1 seq=0 len=200 ········ принят #3 B2 seq=600 len=200 ········ ждёт в буфере #4 B1 seq=500 len=100 com· принят буфер сервера ····youtube. ········ ········ com· поток собран DPI ClientHello из двух пакетов A и B: задом наперёд части идут только внутри пакета, A по-прежнему раньше B сервер собирает 0–800 по seq; новый multidisorder развернул бы весь поток: 600–800, 200–600, 0–200

Сервер получает все куски, раскладывает их по 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).


Оглавление


Справка: где функция в коде и родственники

Паспорт функции для тех, кто сверяется с исходниками 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 нужен для случаев, когда:

  1. Необходима 100%-я совместимость с nfqws1. Если рабочий профиль из nfqws1 использовал --dpi-desync=multidisorder, legacy-вариант воспроизведёт точно такое же поведение
  2. DPI чувствителен к оригинальной сегментации. Некоторые DPI анализируют размеры TCP-сегментов. Legacy сохраняет оригинальные размеры пакетов, меняя только порядок внутри каждого
  3. Нужен контроль 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.payloaddis.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_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 (абсолютный, не относительный)

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

Когда запрос состоит из нескольких пакетов, места разрезов сначала ищутся по всему запросу, а затем каждый пакет забирает себе только те из них, что попали в его границы, и пересчитывает их в номера внутри себя. Так работает нормализация. Границы пакета задаются двумя числами, 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):

  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]

Проще говоря

Из нескольких пакетов каждый оставляет себе только «свои» места разрезов. В примере пакет 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, потому что для пакета A range_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 --- разрез посередине 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.

Проще говоря

Собственных аргументов здесь четыре: pos (где резать), seqovl (где и сколько фальшивых байт приклеить), seqovl_pattern (чем их заполнить) и optional (мягкий режим для паттерна). Аргументов blob и nodrop у legacy нет: указав их, ничего не сломаешь, но и эффекта не получишь. Причина и последствия — в нюансе 2. Нет blob и nodrop; про nodrop у других функций см. нюанс 4 в multisplit.


B) Standard direction

Направление говорит, в какую сторону идёт пакет: от вашего компьютера к серверу (исходящий) или обратно (входящий).

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

Фильтр по направлению пакета. 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, включая unknown
  • payload=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 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).

Что это значит, сказано в нюансе 6. Fooling применяется ко ВСЕМ сегментам: в отличие от fakedsplit и fakeddisorder, где fooling идёт только на фейки, здесь он достаётся всем сегментам, включая настоящие.


E) Standard ipid

IP ID — идентификатор в заголовке IPv4-пакета. Эти параметры задают, какие значения получат отправляемые сегменты.

ПараметрОписаниеПо умолчанию
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-фрагментация — деление пакета на части уже на уровне 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=NIPv6: next protocol во 2-м фрагменте---

G) Standard reconstruct

Реконструкция — сборка готового raw-пакета («сырого», со всеми заголовками) перед отправкой.

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

H) Standard rawsend

Rawsend — отправка готовых сегментов в сеть самой функцией. Эти параметры управляют отправкой.

ПараметрОписание
repeats=NОтправить каждый сегмент N раз (идентичные повторы)
ifout=<iface>Интерфейс для отправки (по умолчанию определяется автоматически)
fwmark=NFirewall 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.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 обрабатывается)Отправляется как есть с применёнными опциями

Проще говоря

Главное в таблице — три строки: «Оригинальная сегментация», «Порядок сегментов» и «Между пакетами». Новый вариант теряет исходное деление на пакеты и разворачивает всё разом; 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.payload
  • nodrop --- оригинальный пакет всегда дропается при успешной отправке (вердикт 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.

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

В таблице слева — ключ первой версии, справа — то, как он записывается во второй.

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 фейк и разрез задавались одним ключом --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=midsld

SNI разрезан пополам. Сначала отправляется вторая половина (с конца 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=0x1603030000

TLS 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,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

Отдельный пакет-фейк — работа функции 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,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

Раздел с параметрами 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-архивом.