todo/Zapret2/desync/multidisorder_legacy.md
loop-uh baf0091fa3
Some checks failed
Published content check / validate (push) Failing after 17s
git.zapret.moe → git2.zapret.moe во всём хранилище, защита от возврата старых имён
git.zapret.moe в России заблокирован по имени, как и wiki.zapret.moe. Ссылки
на Forgejo во всех заметках, футерах, wizard.html и главной заменены на
git2.zapret.moe (включая закодированные адреса значков shields.io).

Какое имя рабочее, теперь решают реестры хоста /etc/zapret-*-domains.conf через
zapret-site-names: pre-commit хук vault отклоняет добавленные строки с
заблокированным именем, сборка Quartz сама переводит ссылки и baseUrl на
рабочие имена. Правило в AGENTS.md переписано под этот механизм.

scripts/wiki-stats.py: переходы с любых имён вики и git (wikiN, gitN) больше
не считаются внешними сайтами. content-check: проверка зеркал сторонних
проектов узнаёт любое имя git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:51:32 -04:00

102 KiB
Raw Permalink Blame History

date tags aliases description
2026-02-17
zapret
zapret2
nfqws2
lua
lua-desync
antidpi
tcp
multidisorder
multidisorder_legacy
seqovl
multidisorder_legacy
Справочник multidisorder_legacy в Zapret2: попакетный обратный порядок TCP-сегментов, полная совместимость с nfqws1, маркеры pos, seqovl, примеры.

[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki2.zapret.moe/Zapret2/desync/multidisorder_legacy

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

Коротко и по-простому

Когда браузер открывает сайт по HTTPS, первым сообщением с данными он отправляет TLS ClientHello («привет от клиента»). В нём браузер перечисляет, какое шифрование умеет, и открытым текстом называет нужный сайт: имя лежит в поле SNI (Server Name Indication, «указание имени сервера»). У обычного HTTP без шифрования то же имя стоит в строке Host: запроса. ТСПУ (Технические средства противодействия угрозам — оборудование DPI/DPI, то есть глубокой инспекции трафика, которое стоит у провайдера) ищет это имя в первом пакете соединения и по нему решает, что делать с соединением дальше.

HTTP-запросы и TLS-соединения передаются по протоколу TCP, а TCP отправляет данные кусками — сегментами. У каждого байта в соединении есть порядковый номер (sequence number, коротко seq), и сервер раскладывает принятые куски в свой буфер по этим номерам. Поэтому ему безразлично, пришли данные одним пакетом или несколькими, и куски, которые приехали раньше времени, он придерживает в буфере, пока не придёт недостающее начало. Почему это гарантирует именно TCP, разобрано в нюансе Zapret2/desync/multisplit#1. Работает только с TCP — и почему.

На этой особенности TCP построено целое семейство техник, и multidisorder_legacy — одна из них. Движок Zapret 2 (nfqws2) перехватывает исходящий пакет с ClientHello или HTTP-запросом, режет его данные в заданных местах и отправляет получившиеся куски отдельными TCP-сегментами в обратном порядке, от последнего к первому, каждому ставя правильный seq. Перехваченный оригинал при этом выбрасывается, чтобы те же данные не ушли второй раз. Места разреза задаёт аргумент pos: число («первые N байт уходят отдельным сегментом») или маркер, привязанный к устройству запроса, например midsld — середина домена второго уровня, главной части имени сайта (у www.example.com это example). Маркеров можно перечислить несколько через запятую; все они разобраны в разделе #Маркеры позиций (pos). Общий принцип обратного порядка описан в разделе Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder статьи про Zapret2/desync/multidisorder.

Теперь о том, чем legacy-вариант отличается от нового multidisorder. Иногда ClientHello не помещается в один TCP-сегмент (например, если в нём есть ключи для post-quantum шифрования) и уходит из системы двумя и более пакетами; такой запрос называют многопакетным. Тогда nfqws2 придерживает пакеты, склеивает их данные в общий буфер (reasm_data, reasm — от reassembly, «повторная сборка») и затем перепроигрывает (replay): вызывает desync-функцию на каждой придержанной части, но уже с собранным целым на руках. Подробно это описано в разделе Zapret2/desync/multidisorder#Поведение при replay / reasm. Новый multidisorder режет весь склеенный буфер целиком и разворачивает порядок для всего запроса: последний кусок ClientHello уйдёт первым, даже если исходно он приехал третьим пакетом. multidisorder_legacy повторяет поведение nfqws1 — первой версии zapret: он обрабатывает каждый исходный пакет отдельно, разворачивает порядок кусков только внутри этого пакета, а сами пакеты идут друг за другом в прямом порядке. Исходные размеры пакетов при этом сохраняются. Если запрос уместился в один пакет, разницы между двумя функциями нет: подробнее — в нюансе #7. Для однопакетных 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.

!desync-anim-multidisorder_legacy.svg

Сервер получает все куски, раскладывает их по seq и видит ровно тот запрос, который отправил браузер, поэтому отвечает как обычно. DPI видит другое: имя сайта разорвано границами кусков, и куски идут в обратном порядке. Если DPI разбирает пакеты по одному или не умеет пересобирать поток, пришедший не по порядку, полного имени он не найдёт. DPI, который пересобирает поток, теоретически может собрать запрос, но многие на практике ограничены буфером или таймаутом и не дожидаются всех частей (так это описано в разделе Zapret2/desync/multidisorder#Почему disorder ломает DPI). Отдельный довод в пользу legacy: некоторые DPI анализируют размеры TCP-сегментов, а legacy сохраняет размеры исходных пакетов и меняет только порядок внутри каждого.

К разрезу можно добавить приём seqovl — скрытый фейк. Функция дописывает слева ко второму по порядку куску (в исходной нарезке) несколько байт-заглушки, по умолчанию нулей, и сдвигает его seq назад на ту же длину; этот кусок уходит предпоследним. Последним уходит самый первый кусок и переписывает заглушку настоящими данными в буфере сервера, а DPI может принять заглушку, например, за начало TLS record (заголовка записи TLS). В legacy-варианте seqovl применяется только к тому пакету, в который попало его место; механика — в разделе #seqovl --- попакетный скрытый фейк. Про Windows-серверы, на которых этот приём в режиме disorder не работает, сказано в разделе Zapret2/desync/multidisorder#Ограничение: Windows-серверы.

Самая короткая команда включает multidisorder_legacy со значениями по умолчанию: один разрез после второго байта (pos=2), обработка только распознанных протоколов (payload=known) и только исходящих пакетов (dir=out). Строку --lua-desync=… пишут в Zapret2/profile#Анатомия профиля — набор правил 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 не подойдёт: он их не поддерживает, и тогда берут новый Zapret2/desync/multidisorder#Зачем нужен multidisorder. Работает legacy, как и вся семья, только с TCP. Порядок выбора между двумя вариантами сведён в таблицу раздела #Когда использовать multidisorder_legacy, а полное сравнение — в разделе #Сравнительная таблица: multidisorder vs multidisorder_legacy. Ближайшие соседи: Zapret2/desync/multisplit#Зачем нужен multisplit режет так же, но отправляет куски по порядку; Zapret2/desync/fakedsplit#Зачем нужен fakedsplit и Zapret2/desync/fakeddisorder#Зачем нужен fakeddisorder делают один разрез и подмешивают фейковые сегменты — отдельные пакеты-обманки, которые нарочно портят (такая порча называется fooling, см. Zapret2/ts-and-fooling#Что такое fooling), чтобы сервер их отбросил, а DPI прочитал. Отдельный пакет-обманку с ложными данными отправляет функция Zapret2/desync/fake#Зачем нужен fake, и с ней multidisorder_legacy можно комбинировать (см. #6. Комбинация: fake + multidisorder_legacy).


Оглавление


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

Паспорт функции для тех, кто сверяется с исходниками zapret2: в каком файле и на какой строке лежит код, какой ключ первой версии zapret она заменяет и как объявлена.

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

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

[!note] Проще говоря Payload — полезные данные пакета без сетевых заголовков: сам HTTP-запрос или сам ClientHello. Reasm — те же данные, собранные движком из нескольких пакетов, если запрос в один пакет не влез. Replay — перепроигрывание: движок по очереди вызывает функцию на каждой придержанной части. Слова «попакетно» и «по всему reasm» описывают, как функция с этим обходится: legacy обрабатывает каждую исходную часть отдельно, новый multidisorder берёт собранное целое. Попакетная обработка подробно разобрана в разделе #Ключевое отличие: попакетная обработка vs целый reasm, а место функции среди остальных — в разделе #Место в общем скелете. VERDICT_DROP — решение «перехваченный пакет не отправлять»; как движок складывает такие решения от разных функций, описано в Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация.

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

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


Зачем нужен multidisorder_legacy

Сначала о том, откуда вообще берётся многопакетность. Запрос, который не поместился в один TCP-сегмент (например, большой TLS ClientHello), уходит несколькими пакетами; nfqws2 придерживает их, собирает в буфер reasm_data и перепроигрывает через desync-функции. Как это устроено, разобрано в разделе Zapret2/desync/multidisorder#Поведение при replay / reasm. Новый 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

[!note] Проще говоря Новый multidisorder склеивает многопакетный запрос в одно целое и разворачивает порядок всех кусков разом. multidisorder_legacy не склеивает данные для отправки: каждый исходный пакет он режет сам и разворачивает только внутри него, как делал nfqws1. Выбирать его стоит, когда нужна точная совместимость с nfqws1, когда DPI анализирует размеры TCP-сегментов или когда seqovl должен действовать на уровне пакета, а не всего запроса. Сам приём seqovl объяснён в разделе #seqovl --- попакетный скрытый фейк.


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

Ниже три готовые строки для Zapret2/profile#Анатомия профиля 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.

!wiki-anim-z2-md-legacy.svg

Часть 2 составлена из конца пакета A и начала пакета B, поэтому исходная граница между пакетами (500 байт + 300 байт) исчезает, и порядок 3, 2, 1 распространяется на весь запрос. Это и подчёркивает последняя строка схемы: оригинальная сегментация потеряна.

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

Здесь функция вызывается для каждого пакета отдельно. Для каждого пакета она держит в руках два набора данных: сам пакет (data, его режут и отправляют) и склеенную цепочку целиком (fulldata, по ней только вычисляются места разрезов); подробнее — в разделе #Откуда берутся данные. Затем позиции нормализуются: переводятся в номера внутри текущего пакета, а те, что в пакет не попали, удаляются. Формула и второй пример — в разделе #Нормализация позиций по пакету.

Пакет Позиции на fulldata После нормализации Части Порядок на проводе
A (500 байт, offset 0, диапазон [1, 501)) 200 и 600: 600 вне диапазона, удалён pos=[200] A1 — 200 байт, A2 — 300 байт A2 (seq 200), затем A1 (seq 0)
B (300 байт, offset 500, диапазон [501, 801)) 200 вне диапазона, удалён; 600 → 600 - 501 + 1 = 100 pos=[100] B1 — 100 байт, B2 — 200 байт B2 (seq 600), затем B1 (seq 500)

Итог на проводе: A2, A1, B2, B1. Исходная сегментация 500 + 300 сохранена, обратный порядок только внутри каждого пакета, между пакетами прямой.

[!note] Проще говоря Сначала функция обрабатывает пакет A: позиция 200 ему подходит, позиция 600 нет. Части A2 и A1 уходят в обратном порядке. Потом пакет B: подходит 600, а 200 отбрасывается; уходят B2 и B1. Пакеты идут друг за другом как пришли, исходные размеры 500 и 300 сохранены, а обратный порядок остаётся внутри каждого пакета. Итог на проводе: A2, A1, B2, B1.


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

Чтобы понять, что режется и по чему считаются места разреза, нужно различать два набора данных. Первый — пакет, который прямо сейчас перехватил движок: его полезное содержимое лежит в desync.dis.payload (dis — диссект, то есть пакет, заранее разобранный движком на поля; его устройство описано в структура desync и диссекта). Второй — весь многопакетный запрос, собранный из частей, desync.reasm_data (как он собирается, см. Zapret2/структура desync и диссекта#Приём многопакетных пейлоадов (reasm / decrypt / replay)). 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 на произвольные данные.

[!note] Проще говоря Legacy режет только то, что пришло в текущем пакете, а места разрезов ищет по всему запросу. Blob — заготовленные данные, которые можно подсунуть вместо настоящих (blob); новый multidisorder их принимает, legacy нет. Как выбор данных устроен у нового варианта, описано в разделе Zapret2/desync/multidisorder#Откуда берутся данные для нарезки, а общая цепочка blob → reasm → payload — в Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload.


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

!

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

Маркер — это место в данных, где надо сделать разрез: число байт от начала или от конца либо «смысловая» позиция вроде midsld. Здесь разобраны только те особенности маркеров, которые нужны legacy-варианту. Полный разбор с примерами и тем, как маркеры разрешаются в коде, — в разделах Zapret2/desync/multidisorder#Маркеры позиций (pos) и Zapret2/desync/multisplit#Маркеры позиций (pos). Отсчёт в командной строке идёт от нуля: 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. Как движок определяет тип, описано в заметке Zapret2/payload#Что такое тип 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):

  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, 500 байт) range_low=1, range_hi=501 200 и 450 в диапазоне, остаются; 600 вне диапазона, удалён [200, 450]
B (offset 500, 300 байт) range_low=501, range_hi=801 200 и 450 меньше 501, удалены; 600 в диапазоне: 600 - 501 + 1 = 100 [100]

[!note] Проще говоря Из нескольких пакетов каждый оставляет себе только «свои» места разрезов. В примере пакет A получает 200 и 450, а 600 у него удаляется; пакет B удаляет 200 и 450 и получает единственную позицию 100 (600 − 501 + 1). У пакета A range_low равен 1, у пакета B — 501. Такой пересчёт отсутствует в новом Zapret2/desync/multidisorder#Маркеры позиций (pos), где режется весь запрос сразу.

Важные нюансы 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, без разреза

[!note] Проще говоря Здесь собраны условия, при которых разрез не происходит или происходит не так, как ждёшь. Разрез в самом начале данных бессмысленен, потому что первый кусок вышел бы пустым, поэтому он выбрасывается. Одинаковые места сливаются в одно, порядок записи не важен, а маркер, который в этом типе данных найти нельзя, просто пропадает из списка. Если в пакете не осталось ни одного места разреза, он уходит как есть, но с включёнными fooling, ipid и прочими опциями. Как это выглядит на примере, см. нюансы #4. Позиция 1 удаляется после нормализации и #5. Пакет без позиций отправляется как есть.


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

seqovl (Sequence Overlap, «перекрытие по номерам последовательности») — приём, при котором в настоящий сегмент слева дописываются фальшивые байты, а его seq сдвигается назад на ту же длину. Как приём работает в других функциях, разобрано в разделах Zapret2/desync/multisplit#seqovl — скрытый фейк внутри сегмента (сервер выбрасывает лишнее, потому что оно левее окна приёма) и Zapret2/desync/multidisorder#seqovl — перезапись буфера сокета через перекрытие (лишнее лежит в буфере и потом переписывается настоящими данными). Здесь описана специфика legacy: приём применяется на уровне пакета.

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

Слово «маркер» здесь то же, что в разделе #Маркеры позиций (pos): место в запросе можно задать не только числом, но и смысловой позицией. Подробнее о том же в Zapret2/desync/multidisorder#seqovl как маркер у нового варианта.

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

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

[!note] Проще говоря 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 (отменён для этого пакета)

[!note] Проще говоря Место seqovl попадает в один пакет — в примере в пакет A. Для пакета B пересчитать его нельзя, и там seqovl просто выключается. Пересчёт числа 245 в пакете A по формуле из примера выше даёт 245 − 1 + 1 = 245, потому что для пакета A range_low равен 1.

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

seqovl_pattern

Паттерн — то, чем заполняются лишние байты слева. Blob — заготовленные данные под именем; как их объявлять, описано в заметке blob. О том же аргументе в других функциях см. Zapret2/desync/multidisorder#seqovl_pattern.

Паттерн заполнения 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.

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


B) Standard direction

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

Параметр Значения По умолчанию
dir in, out, any out

Фильтр по направлению пакета. multidisorder_legacy по умолчанию работает только с исходящими (out).

  • dir=out --- только исходящие (от клиента к серверу)
  • dir=in --- только входящие (от сервера к клиенту)
  • dir=any --- оба направления

При первом вызове с указанным dir функция делает direction_cutoff_opposite --- отсекает себя от противоположного направления. Cutoff (отсечение) значит, что этот экземпляр функции отключается от пакетов противоположного направления и движок перестаёт зря вызывать для них Lua; подробно — в Zapret2/жизненный цикл desync-функции#Стадия 2. Отсечение противоположного направления.


C) Standard payload

Тип payload — название распознанного движком протокола: http_req, tls_client_hello и так далее; как движок его определяет, см. Zapret2/payload#Что такое тип 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, то есть медленнее. Поэтому основной отбор лучше делать на уровне профиля (то же самое сказано в разделе Zapret2/desync/multisplit#C) Standard payload).


D) Standard fooling

Fooling — намеренная порча заголовков пакета, обычно для того, чтобы сервер отбросил фейк, а DPI его прочитал (подробно — Zapret2/ts-and-fooling#Что такое 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 обрабатывает каждый пакет отдельно, обратный порядок действует только внутри одного пакета.

Общий обратный порядок описан в разделе Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder. Ниже три примера: один пакет, два пакета и seqovl.

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

Если запрос уместился в один пакет, legacy ведёт себя так же, как новый multidisorder: части отправляются от последней к первой.

!wiki-anim-z2-direction.svg

Здесь три места разреза дают четыре части. Первой уходит самая правая, последней — самая левая. Метка 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 --- ПРЯМОЙ порядок.
Внутри каждого --- ОБРАТНЫЙ.

[!note] Проще говоря Место разреза 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), а причина — в разделе . У части 2 на схеме стоит seq=21 вместо 100, а перед её настоящими данными (с байта 100) идут 79 байт паттерна; 100 − 79 = 21. Часть 1 уходит последней, и, как сказано на схеме, она переписывает ложные данные из seqovl_pattern в буфере TCP-стека сервера. Что происходит в таком буфере по шагам, показано в Zapret2/desync/multidisorder#ASCII-диаграмма: хронология буфера сокета на сервере.


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

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

[!note] Проще говоря Стадия 4 — выбор данных. Большинство функций берут сначала blob, потом собранный reasm, потом текущий пакет (см. Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload); legacy режет только текущий пакет, а reasm нужен ему для разрешения маркеров. Стадия 6 — развилка перепроигрывания: обычная функция работает на первой части replay и дропает остальные, а legacy вызывается на каждой части (Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)). Всё остальное — как у других функций.


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

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

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

[!note] Проще говоря Новый multidisorder вызывает replay_first() и replay_drop(), чтобы обработать запрос один раз и отбросить остальные части. Legacy этих вызовов не делает: движок зовёт его на каждой части, и каждая часть обрабатывается сама по себе. Именно это отличие и называют попакетной обработкой.


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

В таблице по строкам сопоставлены два варианта функции. Последний столбец — multidisorder_legacy, средний — новый Zapret2/desync/multidisorder; ячейки со словами «Да»/«Нет» отвечают на вопрос «поддерживается ли». Разбор причин — в разделах #Ключевое отличие: попакетная обработка vs целый reasm и #Нюансы и подводные камни.

Аспект 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 обрабатывается) Отправляется как есть с применёнными опциями

[!note] Проще говоря Главное в таблице — три строки: «Оригинальная сегментация», «Порядок сегментов» и «Между пакетами». Новый вариант теряет исходное деление на пакеты и разворачивает всё разом; legacy сохраняет пакеты и разворачивает только внутри каждого. Остальные строки — следствия этого (нормализация позиций, отсутствие replay-развилки) и разница в наборе аргументов (blob, nodrop).


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

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

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

Почему эта техника вообще возможна только для TCP — у него сквозная нумерация байтов и сервер собирает поток по номерам — разобрано в нюансе Zapret2/desync/multisplit#1. Работает только с TCP — и почему; стадия отсева описана в Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff. 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 см. Zapret2/desync/multisplit#4. nodrop создаёт дублирование.

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 не применится.

[!note] Проще говоря В примере место seqovl лежит ровно на границе пакетов: в пакет A оно не входит (граница A не включена), в пакете B оно нормализуется в 1, а это означает нулевое перекрытие, поэтому фейк фактически не применяется. Причина нулевого перекрытия описана в разделе ; правило «seqovl должен быть меньше первой позиции разреза» — в разделе Zapret2/desync/multidisorder#Валидация seqovl: должен быть меньше pos[1].

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

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

Проще говоря: если пересчёт поставил разрез на самое начало текущего пакета, разрез удаляется, потому что первая часть вышла бы пустой. Так же поступает и multisplit (см. его Zapret2/desync/multisplit#2. Разрез в самом начале данных отбрасывается).

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

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

Проще говоря: пакет без разрезов не теряется. Он уходит целым, но с теми настройками порчи заголовков и IP ID, которые вы задали, и оригинал дропается.

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

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

Подробнее про то, почему порча заголовков у таких функций вредна, — в Zapret2/desync/multisplit#6. Fooling применяется ко ВСЕМ сегментам; про сами опции — в Zapret2/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 у нового варианта, см. Zapret2/desync/multidisorder#Поведение при replay / reasm. Проще говоря: legacy не «решает» за весь запрос, а отвечает за один пакет и забывает о нём.

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

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

Проще говоря: механизм допускает, что seqovl попадёт в разные пакеты, но на деле его место — одно, поэтому и пакет один.


Миграция с nfqws1

Этот раздел для тех, кто переносит настройки из первой версии zapret (программа nfqws) во вторую (nfqws2). В первой версии техника выбиралась ключом --dpi-desync=…, а её параметры — отдельными ключами --dpi-desync-…; во второй всё это записывается аргументами одного вызова --lua-desync=multidisorder_legacy:…. Именно для такого переноса legacy-вариант и создан.

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

Ниже — короткая шпаргалка выбора между двумя вариантами; полное сравнение — в разделе #Сравнительная таблица: multidisorder vs multidisorder_legacy.

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

[!note] Проще говоря Переносишь рабочий профиль 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, потому что им нужны разные Zapret2/desync/fake#Стандартные 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. В Zapret2/desync/multidisorder#Миграция с 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 не подходит.


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

Каждый пример — готовая строка для Zapret2/profile#Анатомия профиля; под ней объяснено, что получится на выходе.

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 как маркер — #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

Отдельный пакет-фейк — работа функции Zapret2/desync/fake#Зачем нужен fake; tcp_md5 — способ испортить его так, чтобы сервер отбросил (см. Zapret2/ts-and-fooling#Что такое 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/DPI — как устроена инспекция трафика, против которой работает multidisorder_legacy

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


[!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.