> [!mirror] Резервное зеркало
> Актуальная версия этой страницы — на основной вики: [wiki.zapret.moe/Zapret2/desync/multisplit](https://wiki.zapret.moe/Zapret2/desync/multisplit)
# `multisplit` — последовательная TCP-сегментация (zapret2 / nfqws2)
> [!info] О чём заметка
> `multisplit` — функция дурения DPI в [[Zapret2/Zapret2|Zapret 2]] (движок `nfqws2`), которая режет исходящий TCP-пакет на несколько кусков, чтобы цензор не увидел в нём имя запрашиваемого сайта. Здесь разобраны все аргументы функции, маркеры позиций разреза, техника `seqovl` и готовые примеры команд. Общий контракт `--lua-desync`-функций (кто и когда их вызывает) описан в [[desync]], общий для всех техник порядок работы — в [[жизненный цикл desync-функции]], устройство входной таблицы `desync` — в [[структура desync и диссекта]].
## Коротко и по-простому
Когда браузер открывает сайт по HTTPS, первым сообщением с данными он отправляет TLS ClientHello («привет от клиента»). В нём браузер перечисляет, какое шифрование умеет, и открытым текстом называет нужный сайт: имя лежит в поле SNI (Server Name Indication, «указание имени сервера»). У обычного HTTP без шифрования то же имя стоит в строке `Host:` запроса. ТСПУ (Технические средства противодействия угрозам — оборудование [[DPI/DPI|DPI]], то есть глубокой инспекции трафика, которое стоит у провайдера) ищет это имя в первом пакете соединения и по нему решает, что делать с соединением дальше.
HTTP-запросы и TLS-соединения передаются по протоколу TCP, а TCP отправляет данные кусками — сегментами. У каждого байта в соединении есть порядковый номер (sequence number, коротко seq), и сервер раскладывает принятые куски в свой буфер строго по этим номерам. Поэтому ему безразлично, пришли данные одним пакетом или пятью и в каком порядке куски доехали: исходный поток он всё равно соберёт. Почему это гарантирует именно TCP, разобрано в нюансе [[#1. Работает только с TCP — и почему|«Работает только с TCP»]].
На этой особенности TCP и построен `multisplit`. Движок Zapret 2 (`nfqws2`) перехватывает исходящий пакет с ClientHello или HTTP-запросом, а `multisplit` делает с ним три шага. Сначала режет данные пакета в заданных местах, например после первого байта и посередине домена второго уровня — главной части имени сайта (у `www.youtube.com` это `youtube`). Потом отправляет получившиеся куски отдельными TCP-сегментами по порядку, от первого к последнему, и каждому ставит правильный номер seq. В конце выбрасывает перехваченный оригинал, чтобы те же данные не ушли второй раз. Места разреза задаёт аргумент `pos`: число («первые N байт уходят отдельным сегментом») или маркер, привязанный к устройству запроса, например `midsld` — середина домена второго уровня. Маркеров можно перечислить несколько через запятую; все они разобраны в разделе [[#Маркеры позиций (pos)|Маркеры позиций (pos)]].
Сервер получает все куски, раскладывает их по номерам seq и видит ровно тот запрос, который отправил браузер, поэтому отвечает как обычно. DPI видит другое: имени сайта целиком нет ни в одном пакете. Если DPI ищет имя как сигнатуру (заранее известную строку) внутри отдельного пакета и не склеивает поток обратно, он имя не найдёт. DPI, который умеет пересобирать TCP-поток, одним разрезом не обмануть; подробнее об этом — в разделе [[#Зачем нужен multisplit|Зачем нужен multisplit]].
Команда из быстрого старта `multisplit:pos=1,midsld` режет ClientHello после первого байта и посередине домена второго уровня, и три сегмента уходят по порядку. Имя `youtube.com` оказывается разорвано между вторым и третьим сегментом, поэтому DPI, который ищет сигнатуру внутри одного пакета и не пересобирает поток, её не находит. Как читать схему: строки — пакеты в порядке отправки, подписанные в тех же обозначениях, что и таблицы в этой статье; по горизонтали — место данных в TCP-потоке (номер последовательности, seq); в колонке справа — что сделал с пакетом сервер; внизу — буфер сервера, в который принятые куски встают по своим seq.
![[desync-anim-multisplit.svg|Анимация multisplit: ClientHello уходит тремя сегментами по порядку, имя youtube.com разорвано между пакетами]]
Самая короткая команда включает `multisplit` со значениями по умолчанию: один разрез после второго байта (`pos=2`), обработка только распознанных протоколов (`payload=known`) и только исходящих пакетов (`dir=out`). Строку `--lua-desync=…` пишут в [[Zapret2/profile#Анатомия профиля|профиль]] — набор правил Zapret 2 для определённого трафика:
```bash
--lua-desync=multisplit
```
Для HTTPS обычно берут вариант со схемы: `--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld`; здесь `--payload=tls_client_hello` значит «срабатывать только на TLS ClientHello». Остальные готовые команды собраны в разделах [[#Быстрый старт|Быстрый старт]] и [[#Практические примеры|Практические примеры]].
К разрезу можно добавить приём `seqovl`. Тогда `multisplit` дописывает перед первым куском несколько байт фальшивки и сдвигает номер seq этого куска назад на ту же длину. Для сервера эти байты оказываются левее места, с которого он ждёт данные, и он их выбрасывает. DPI же может прочитать их вместе с остальным сегментом и принять за начало запроса. Заголовки пакета при этом остаются правильными, портить их не приходится. Механика с раскладкой по байтам — в разделе [[#seqovl — скрытый фейк внутри сегмента|seqovl — скрытый фейк внутри сегмента]].
`multisplit` рассчитан на DPI, который разбирает каждый пакет отдельно. Против системы, которая пересобирает поток, одного разреза мало. Тогда его сочетают с `seqovl` и с [[Zapret2/desync/fake#Зачем нужен fake|fake]] — отдельным пакетом-обманкой с ложными данными, который нарочно испорчен так, чтобы сервер его отбросил, а DPI прочитал (такую порчу называют fooling, см. [[Zapret2/ts-and-fooling#Что такое fooling|Что такое fooling]]). Рабочую комбинацию для своего провайдера подбирают перебором и проверяют по [[Zapret2/verify-strategy#Короткий чек-лист честной проверки|чек-листу честной проверки]]. Техника работает только с TCP: UDP-трафик (другой способ передачи, в котором нет сквозной нумерации байтов и получатель не склеивает пакеты в поток), включая QUIC, так не режут, и почему — объяснено в нюансе [[#1. Работает только с TCP — и почему|«Работает только с TCP»]].
У `multisplit` есть близкие родственники. [[Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder|multidisorder]] режет так же, но отправляет куски в обратном порядке, и `seqovl` там работает принципиально иначе. [[Zapret2/desync/fakedsplit#Зачем нужен fakedsplit|fakedsplit]] и [[Zapret2/desync/fakeddisorder#Зачем нужен fakeddisorder|fakeddisorder]] делают один разрез и подмешивают к настоящим кускам фейковые сегменты. [[Zapret2/desync/hostfakesplit#Зачем нужен hostfakesplit|hostfakesplit]] режет запрос по границам имени хоста и подмешивает фейковое имя, [[Zapret2/desync/tcpseg#Зачем нужен tcpseg|tcpseg]] отправляет диапазон данных между двумя маркерами, а [[Zapret2/desync/oob#Зачем нужен oob|oob]] вместо разреза вставляет «срочный» (urgent) байт. Все отличия сведены в таблицу в разделе [[#Отличия от других функций сегментации|Отличия от других функций сегментации]].
## TL;DR
- `multisplit` разрезает прикладные данные (HTTP-запрос, TLS ClientHello) на несколько TCP-сегментов, отправляет их по порядку, а перехваченный оригинал выбрасывает (дропает). Сервер собирает поток обратно штатными средствами TCP — для него ничего не меняется.
- Смысл приёма: DPI ищет имя сайта (`Host:` в HTTP, SNI в TLS) сигнатурой, то есть известной заранее строкой, внутри одного пакета. Если имя разорвано границей сегментов, а DPI не пересобирает TCP-поток, сигнатура не находится.
- Где резать, задаёт аргумент `pos`: либо числом («первые N байт уходят отдельным сегментом»), либо маркером, привязанным к структуре протокола (`midsld` — середина домена второго уровня). Маркеров можно перечислить несколько через запятую.
- `seqovl=N` дополнительно приклеивает слева к первому сегменту N байт фальшивки и сдвигает sequence number назад: сервер эти байты отбросит как вышедшие за границу окна приёма, а DPI их прочитает и может принять за начало запроса.
- Типовой рабочий вариант для TLS: `--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld`. Это прямой аналог `--dpi-desync=multisplit --dpi-desync-split-pos=1,midsld` из первой версии zapret.
- Если открыли заметку впервые, начните с раздела [[#Коротко и по-простому|Коротко и по-простому]] выше, а потом читайте подряд до раздела [[#Практические примеры|Практические примеры]]; разбор кода вынесен в конец и нужен, только если вы правите Lua-функции сами.
---
## Оглавление
- [Коротко и по-простому](#коротко-и-по-простому)
- [TL;DR](#tldr)
- [Справка: где функция в коде и родственники](#справка-где-функция-в-коде-и-родственники)
- [Зачем нужен multisplit](#зачем-нужен-multisplit)
- [Быстрый старт](#быстрый-старт)
- [Откуда берутся данные для нарезки](#откуда-берутся-данные-для-нарезки)
- [Маркеры позиций (pos)](#маркеры-позиций-pos)
- [Типы маркеров](#типы-маркеров)
- [Относительные маркеры](#относительные-маркеры)
- [Арифметика маркеров](#арифметика-маркеров)
- [Пример списка маркеров](#пример-списка-маркеров)
- [Как маркеры разрешаются в коде](#как-маркеры-разрешаются-в-коде)
- [Важные нюансы pos](#важные-нюансы-pos)
- [seqovl — скрытый фейк внутри сегмента](#seqovl--скрытый-фейк-внутри-сегмента)
- [Принцип работы seqovl](#принцип-работы-seqovl)
- [Зачем seqovl лучше обычного fooling](#зачем-seqovl-лучше-обычного-fooling)
- [seqovl_pattern](#seqovl_pattern)
- [Полный список аргументов](#полный-список-аргументов)
- [A) Собственные аргументы multisplit](#a-собственные-аргументы-multisplit)
- [B) Standard direction](#b-standard-direction)
- [C) Standard payload](#c-standard-payload)
- [D) Standard fooling](#d-standard-fooling)
- [E) Standard ipid](#e-standard-ipid)
- [F) Standard ipfrag](#f-standard-ipfrag)
- [G) Standard reconstruct](#g-standard-reconstruct)
- [H) Standard rawsend](#h-standard-rawsend)
- [Порядок отправки сегментов](#порядок-отправки-сегментов)
- [Пример с 3 позициями разреза](#пример-с-3-позициями-разреза)
- [Пример с seqovl=10](#пример-с-seqovl10)
- [Поведение при replay / reasm](#поведение-при-replay--reasm)
- [Автосегментация по MSS](#автосегментация-по-mss)
- [Нюансы и подводные камни](#нюансы-и-подводные-камни)
- [1. Работает только с TCP — и почему](#1-работает-только-с-tcp--и-почему)
- [2. Разрез в самом начале данных отбрасывается](#2-разрез-в-самом-начале-данных-отбрасывается)
- [3. Все маркеры могут не разрешиться](#3-все-маркеры-могут-не-разрешиться)
- [4. nodrop создаёт дублирование](#4-nodrop-создаёт-дублирование)
- [5. seqovl=10000 не вызовет ошибку](#5-seqovl10000-не-вызовет-ошибку)
- [6. Fooling применяется ко ВСЕМ сегментам](#6-fooling-применяется-ко-всем-сегментам)
- [7. Порядок инстансов важен](#7-порядок-инстансов-важен)
- [Отличия от других функций сегментации](#отличия-от-других-функций-сегментации)
- [Миграция с nfqws1](#миграция-с-nfqws1)
- [Соответствие параметров](#соответствие-параметров)
- [Пример полной миграции](#пример-полной-миграции)
- [Практические примеры](#практические-примеры)
- [Как это устроено внутри](#как-это-устроено-внутри)
- [Место в общем скелете](#место-в-общем-скелете)
- [Стадия 7: нарезка и отправка](#стадия-7-нарезка-и-отправка)
- [Псевдокод алгоритма](#псевдокод-алгоритма)
- [📚 См. также](#-см-также)
---
## Справка: где функция в коде и родственники
Паспорт функции для тех, кто сверяется с исходниками zapret2: в каком файле и на какой строке лежит код, какой ключ первой версии zapret она заменяет и как объявлена.
**Файл:** `lua/zapret-antidpi.lua:483`
**nfqws1 эквивалент:** `--dpi-desync=multisplit`
**Сигнатура:** `function multisplit(ctx, desync)`
`multisplit` — базовая функция TCP-сегментации в zapret2. Она берёт текущий payload (или reasm, или blob), разрезает его на несколько TCP-сегментов по заданным позициям и отправляет их **последовательно** (от первого к последнему). После успешной отправки выносит `VERDICT_DROP`, чтобы оригинальный пакет не ушёл.
> [!note] Проще говоря
> Payload — полезные данные пакета без сетевых заголовков: сам HTTP-запрос или сам ClientHello. Reasm — те же данные, собранные движком из нескольких пакетов, если запрос в один пакет не влез. Blob — заранее заготовленные данные, которые можно подсунуть вместо настоящих. Какой из трёх источников выбирается, разобрано в разделе [[#Откуда берутся данные для нарезки|Откуда берутся данные для нарезки]]. `VERDICT_DROP` — решение «перехваченный пакет не отправлять»; как движок складывает такие решения от разных функций, описано в [[Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация|стадии 8 жизненного цикла]].
Родственные функции: [[multidisorder]] (обратный порядок), [[fakedsplit]] (с фейками), [[fakeddisorder]] (фейки + обратный порядок), [[hostfakesplit]] (по hostname), [[tcpseg]] (диапазон), [[oob]] (urgent byte).
---
## Зачем нужен multisplit
DPI анализирует TCP-поток: пытается собрать полный payload, то есть сами прикладные данные без сетевых заголовков, и найти в нём сигнатуры — известные заранее строки, по которым узнаётся сайт (hostname в HTTP, SNI в TLS). Если мы разрежем пакет на несколько TCP-сегментов, DPI может:
1. **Не собрать данные:** некоторые DPI работают попакетно, то есть разбирают каждый пакет отдельно, и не реассемблируют TCP — не склеивают сегменты обратно в поток
2. **Не найти сигнатуру:** если разрез проходит через `Host:` или SNI, ни в одном отдельном сегменте полного hostname нет
3. **Принять фейк за реальные данные:** с помощью [[#seqovl — скрытый фейк внутри сегмента|seqovl]] можно замешать ложную информацию, которую DPI проглотит, а сервер — нет
Сервер при этом корректно собирает поток — TCP-стек гарантирует это через sequence numbers, сквозные номера байтов в потоке.
Проще всего увидеть смысл на байтах. Браузер отправляет HTTP-запрос одним пакетом, и цензору достаточно найти в нём строку с именем сайта:
```
GET /watch HTTP/1.1\r\nHost: www.youtube.com\r\n...
^^^^^^^^^^^^^^^^^ сигнатура целиком в одном пакете
```
С `--lua-desync=multisplit:pos=midsld` тот же запрос уходит двумя TCP-сегментами с разрезом посередине домена второго уровня (`youtube`):
```
сегмент 1: GET /watch HTTP/1.1\r\nHost: www.you
сегмент 2: tube.com\r\n...
```
Строки `www.youtube.com` теперь нет ни в одном пакете — она появляется только после сборки потока. DPI, который сигнатуру ищет в границах отдельного пакета, её не найдёт. Сервер соберёт запрос как обычно и ответит нормально: для него это просто данные, пришедшие двумя порциями вместо одной.
Оговорка: приём рассчитан на DPI, который не пересобирает TCP-поток. Системы, умеющие реассемблирование, разрез сам по себе не обманет — поэтому `multisplit` обычно комбинируют с [[fake|фейковыми пакетами]] и `seqovl`, а рабочую комбинацию подбирают перебором (см. [[verify-strategy]]).
**multisplit** — самый простой вариант: режем и шлём **по порядку**. Для обратного порядка есть [[multidisorder]], для замешивания фейковых сегментов — [[fakedsplit]]/[[fakeddisorder]].
---
## Быстрый старт
Ниже четыре готовые строки для [[Zapret2/profile#Анатомия профиля|профиля]] Zapret 2, от самой короткой к более сложным. Флаг `--payload=…` работает как фильтр для всех вызовов `--lua-desync`, которые идут после него (до следующего `--payload` или до конца профиля): они срабатывают только на данных указанного типа. Поэтому `--payload` пишут перед `--lua-desync`, а не после (подробно — в [[последовательность аргументов]]).
Минимально (дефолты: `pos=2` — первые 2 байта отдельным сегментом, `payload=known`, `dir=out`):
```bash
--lua-desync=multisplit
```
Здесь `payload=known` означает «только распознанные протоколы», а `dir=out` — «только исходящие пакеты»; оба аргумента разобраны в разделах [B) Standard direction](#b-standard-direction) и [C) Standard payload](#c-standard-payload).
Типовой TLS-разрез:
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld
```
Функция срабатывает только на TLS ClientHello и режет каждый дважды — после первого байта и посередине домена второго уровня, поэтому уходит три сегмента.
TLS с seqovl:
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1:seqovl=5:seqovl_pattern=0x1603030000
```
Здесь один разрез после первого байта, а к первому сегменту слева приклеиваются 5 байт `0x1603030000`, похожих на начало TLS record — заголовка записи TLS. Как это работает, разобрано в разделе [seqovl](#seqovl--скрытый-фейк-внутри-сегмента).
HTTP с разрезом по hostname:
```bash
--payload=http_req --lua-desync=multisplit:pos=host,midsld,endhost
```
Три разреза — в начале имени хоста, посередине домена второго уровня и сразу после имени — дают 4 сегмента.
---
## Откуда берутся данные для нарезки
Прежде чем что-то резать, `multisplit` должна выбрать, *какие именно* данные она режет. Это делает всего одна строка, но за ней стоит цепочка приоритетов:
```lua
local data = blob_or_def(desync, desync.arg.blob) or desync.reasm_data or desync.dis.payload
```
Lua-оператор `or` возвращает первый истинный (не `nil` и не `false`) операнд, поэтому строка читается как «сверху вниз, берём первое, что есть». Разберём три источника по порядку. Та же цепочка общая для большинства функций дурения и описана в [[Zapret2/жизненный цикл desync-функции|жизненном цикле desync-функции]] как стадия 4; там же перечислены функции-исключения, которые берут данные иначе.
**Первый приоритет — явный blob.** Если в аргументах задан `blob=имя`, функция `blob_or_def` достаёт содержимое этого blob и режет именно его, полностью игнорируя реальный payload пакета. Это способ отправить *произвольные* данные вместо того, что реально шлёт приложение: заранее заготовленный фейковый ClientHello, модифицированный запрос, содержимое файла. Что такое blob и как их объявлять (inline-hex `0x...`, `@файл`, именованные переменные) — см. отдельную заметку [[blob]]. Если `blob=` не задан, `blob_or_def` возвращает `nil`, и `or` идёт дальше.
**Второй приоритет — реассемблированные данные.** Реассемблирование (reasm, от английского reassembly — «повторная сборка») — это склейка прикладного запроса из нескольких TCP-пакетов. Если blob нет, но движок собрал многопакетный payload в `desync.reasm_data` (например, TLS ClientHello, растянувшийся на несколько TCP-сегментов), режется этот собранный буфер целиком. Благодаря этому разрез по `midsld` попадёт в реальную середину доменного имени, даже если само имя физически лежало во втором или третьем пакете исходного потока. Механику сборки и однократной отправки reasm разбирает раздел [Поведение при replay / reasm](#поведение-при-replay--reasm).
**Третий приоритет — payload текущего пакета.** Если ни blob, ни reasm нет (обычный одиночный пакет, всё поместилось), берётся `desync.dis.payload` — тело именно этого перехваченного TCP-сегмента. Здесь `dis` — диссект, то есть пакет, заранее разобранный движком на поля; его устройство описано в [[структура desync и диссекта]]. Это дефолтный путь для большинства простых случаев.
**Почему порядок именно такой и что из этого следует.** Все дальнейшие аргументы — маркеры `pos`, длина `seqovl`, проверки — применяются к тем данным, которые реально выбраны на этом шаге, а не к «пакету вообще». Практическое следствие: если вы задали `blob=myblob`, то относительные маркеры вроде `midsld` или `host` сработают только при условии, что содержимое `myblob` — это валидный TLS- или HTTP-payload, который движок сумеет распознать (иначе `desync.l7payload` для blob окажется `unknown` и такие маркеры молча отвалятся, см. [Важные нюансы pos](#важные-нюансы-pos)). Поле `desync.l7payload` хранит [[Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока)|тип payload]] — название распознанного протокола вроде `tls_client_hello` или `http_req`.
> [!note] Проще говоря
> Функция сначала спрашивает: «мне явно подсунули blob? — режу его». Если нет: «есть собранный из кусков payload? — режу его». Если и его нет: «режу тело текущего пакета». Маркеры и seqovl всегда работают по тем данным, которые выиграли этот выбор.
---
## Маркеры позиций (pos)
`pos` — главный аргумент `multisplit`. Определяет **где** внутри payload будет произведён разрез. Задаётся как строка со списком маркеров через запятую.
Маркер — это **смещение от начала данных, считая с нуля**: разрез происходит *перед* байтом с таким номером. Отсюда простое правило, которым удобно пользоваться на практике: `pos=N` означает «первые N байт уходят отдельным сегментом». `pos=1` → первый сегмент длиной 1 байт, `pos=5` → 5 байт, дефолтный `pos=2` → 2 байта. Внутри Lua те же позиции хранятся как 1-based индексы строки, то есть с нумерацией от единицы (`resolve_multi_pos` прибавляет единицу), но в командной строке считать нужно от нуля.
> [!note] Проще говоря
> В командной строке пишите, сколько байт должно уйти в первом куске. Сдвиг на единицу внутри Lua пригодится, только когда читаете код функции (раздел [Как это устроено внутри](#как-это-устроено-внутри)) или разбираетесь, почему `pos=0` не срабатывает.
### Типы маркеров
Маркеры бывают трёх видов. Абсолютные отсчитывают байты от начала или от конца данных и не зависят от их содержимого. Относительные ищут место по смыслу — начало имени хоста, середину домена — и поэтому работают, только если движок распознал протокол.
| Тип | Описание | Пример |
|:----|:---------|:-------|
| **Абсолютный положительный** | Смещение от начала 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 (и чем он отличается от протокола потока)|о типах 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. Extension состоит из type (2 байта) + length (2 байта) + **данные** — sniext указывает на начало данных | `tls_client_hello` |
| `extlen` | Поле длины всех TLS extensions | `tls_client_hello` |
### Арифметика маркеров
Маркер можно сдвинуть на несколько байт вперёд или назад, если разрез нужен рядом с ним, а не точно в нём. К любому маркеру можно прибавить (+) или вычесть (-) целое число:
```
midsld+1 — один байт ПОСЛЕ середины SLD
midsld-1 — один байт ДО середины SLD
endhost-2 — два байта до конца hostname
method+2 — два байта после начала метода (разрежет "GET " после "GE")
sniext+1 — один байт после начала SNI extension data
host+3 — три байта после начала hostname
-1 — последний байт payload (абсолютный, не относительный)
```
Арифметика работает и с абсолютными маркерами, хотя это избыточно (`5+3` = `8`).
### Пример списка маркеров
Маркеры разных типов можно смешивать в одном списке:
```
pos=100,midsld,sniext+1,endhost-2,-10
```
Здесь 5 маркеров → payload разрежется максимум на 6 частей (если все маркеры успешно разрешатся и дадут различные позиции). В списке есть абсолютный маркер от начала (`100`), три относительных (`midsld`, `sniext+1`, `endhost-2`) и абсолютный от конца (`-10`).
### Как маркеры разрешаются в коде
Разрешить маркер — значит превратить его название в конкретный номер байта в выбранных данных. Внутри `multisplit` вызывается:
```lua
local pos = resolve_multi_pos(data, desync.l7payload, spos)
```
Функция `resolve_multi_pos`:
1. Разбивает строку `spos` по запятым
2. Для каждого маркера вызывает `resolve_pos(blob, l7payload_type, marker)`
3. Если маркер не может быть разрешён (например, `midsld` для `unknown` payload) — он **молча пропускается**
4. Результаты дедуплицируются (повторы убираются) и сортируются
5. Возвращается массив **уникальных** абсолютных позиций, уже переведённых в 1-based индексы Lua (маркер `1` превращается в позицию `2`, маркер `0` — в позицию `1`)
Затем вызывается:
```lua
delete_pos_1(pos) -- удалить Lua-позицию 1, то есть маркер 0
```
### Важные нюансы pos
- **Разрез в самом начале данных отбрасывается.** `delete_pos_1` удаляет Lua-позицию 1, которая соответствует маркеру `0`. Резать «перед первым байтом» бессмысленно: первый сегмент получился бы пустым. Практическое следствие — `pos=0` не сработает, а `pos=1` (первый байт отдельным сегментом) работает и используется очень часто, в том числе в связке с [seqovl](#seqovl--скрытый-фейк-внутри-сегмента)
- **Реальный случай, когда маркер выпадает по этой причине — `pos=method` для HTTP.** Метод (`GET`, `POST`) обычно лежит на смещении 0, поэтому маркер разрешится в позицию 1 и будет удалён; разреза не произойдёт вовсе. Чтобы резать внутри метода, нужен сдвиг: `pos=method+2`
- **Дублирующиеся позиции объединяются.** `pos=5,5,5` = `pos=5`
- **Неразрешимые маркеры пропускаются.** Если `midsld` не разрешается (payload = unknown), он просто исчезает из списка. Если все маркеры не разрешились — multisplit ничего не делает (логирует "no valid split positions")
- **Позиции сортируются.** Независимо от порядка записи, `pos=100,5,50` будет обработано как `5,50,100`
- **По умолчанию `pos=2`.** Если `pos` не задан, payload делится на 2 части: первые 2 байта отдельным сегментом, весь остаток — вторым. Тот же дефолт был у `--dpi-desync-split-pos` в nfqws1 (соответствие ключей nfqws1 и nfqws2 — в разделе [Миграция с nfqws1](#миграция-с-nfqws1))
---
## seqovl — скрытый фейк внутри сегмента
**seqovl** (Sequence Overlap, «перекрытие по номерам последовательности») — техника скрытого замешивания фейковых данных в реальный TCP-сегмент через манипуляцию TCP sequence number. В `multisplit` seqovl применяется **только к первому** отправляемому сегменту.
В [[Zapret2/desync/multidisorder#seqovl — перезапись буфера сокета через перекрытие|multidisorder]] приём с тем же названием устроен иначе: там он перезаписывает буфер сокета на сервере через перекрытие сегментов, поэтому переносить знания о seqovl между функциями нужно осторожно.
### Принцип работы seqovl
Чтобы понять seqovl, нужно вспомнить, как TCP-получатель решает, какие байты принять. У каждого байта в TCP-потоке есть свой sequence number — сквозной номер позиции в потоке. Приёмная сторона держит «окно» (receive window) и принимает только байты, чей номер попадает в него, начиная с номера следующего *ожидаемого* байта. Всё, что левее этой границы (номера, которые сторона уже получила или ещё не ждёт с этого места), молча отбрасывается как дубликат или мусор — это штатное поведение TCP-стека, а не ошибка.
seqovl эксплуатирует ровно этот механизм. Возьмём для примера первый сегмент, который в норме начинался бы с sequence number 1000 и нёс реальные данные. **Без seqovl** сегмент уходит как есть: seq = 1000, тело — реальная первая часть, сервер принимает её целиком, потому что 1000 — как раз следующий ожидаемый им байт.
**С `seqovl=10`** функция делает две вещи одновременно. Во-первых, дописывает слева к телу сегмента 10 байт паттерна-фейка (`seqovl_pattern`, по умолчанию нули). Во-вторых, уменьшает объявленный в заголовке sequence number на те же 10 — с 1000 до 990. В результате в сеть уходит один-единственный, совершенно обычный с виду TCP-сегмент: seq = 990, тело = `[10 байт паттерна][реальная первая часть]`.
Дальше этот сегмент по-разному воспринимают две стороны.
**Что видит DPI.** Для системы глубокого анализа это цельный TCP-сегмент, начинающийся с sequence 990. Многие DPI разбирают содержимое сегмента «как есть», от его начала, не сверяясь с тем, какие именно байты примет получатель. Поэтому DPI анализирует весь блок — вместе с 10 байтами паттерна. Если в паттерн подложен, например, фрагмент, похожий на начало легитимного TLS-record или ложный SNI, DPI может принять фейк за реальные данные и вынести решение по нему, а не по настоящему домену.
**Что видит сервер (TCP-стек получателя).** Следующий ожидаемый им байт по-прежнему имеет номер 1000. Байты с номерами 990–999 (те самые 10 байт паттерна) оказываются левее границы окна — сервер отбрасывает их автоматически, как повтор/устаревшие данные. А байты, начиная с 1000 (реальная первая часть), попадают в окно и принимаются. В итоге сервер собирает ровно исходный поток, будто никакого паттерна и не было.
Наглядно расклад по одному sequence-байту такой:
| Диапазон seq | Содержимое | DPI | Сервер (TCP-стек) |
|:-------------|:-----------|:----|:------------------|
| 990–999 | `PATTERN` (10 байт фейка) | анализирует вместе со всем сегментом | **отбрасывает** — левее границы окна |
| с 1000 | реальная первая часть | анализирует | **принимает** |
На этом и держится обход: одни и те же 10 байт для DPI — «полноценные данные для анализа», а для сервера — «мусор за левым краем окна». Две стороны по-разному трактуют один и тот же сегмент.
> [!note] Проще говоря
> `seqovl` отправляет перед настоящим началом запроса несколько лишних байт и помечает их номерами, которые сервер уже не ждёт. Сервер эти байты молча выкидывает, а DPI, читающий сегмент целиком, видит фальшивое начало.
### Зачем seqovl лучше обычного fooling
Обычный способ спрятать фейк от сервера — [[Zapret2/ts-and-fooling#Что такое fooling|fooling]]: пакету-обманке портят заголовки (TTL, номера seq или ack, опцию md5sig), и сервер (или маршрутизатор по пути) отбрасывает такой пакет целиком. `seqovl` добивается похожего результата другим путём, и таблица ниже сравнивает оба подхода.
| Критерий | Обычный fooling (TTL, badseq, md5sig) | seqovl |
|:---------|:---------------------------------------|:-------|
| Заголовки | Модифицируются (TTL, seq, ack, md5) | **Не модифицируются** — пакет выглядит полностью легитимным |
| Обнаружение | DPI может детектировать подозрительные заголовки | DPI видит "честный" сегмент с правильными заголовками |
| Механизм отбрасывания | Сервер отбрасывает весь пакет из-за невалидных заголовков | Сервер отбрасывает только часть, выходящую за TCP window (окно приёма) |
| Надёжность | Зависит от поведения конкретного стека | Основан на фундаментальном свойстве TCP |
**Вывод:** seqovl — средство создания скрытых фейков, не требующее fooling. В этом его главное преимущество.
### seqovl_pattern
Паттерн, которым заполняется seqovl-область (N байт слева от реальных данных). По умолчанию — `0x00` (нули).
В `multisplit` `seqovl_pattern` — это **имя blob**. Паттерн повторяется до нужной длины `seqovl`. Blob можно записать прямо в команде шестнадцатеричной строкой (inline hex, `0x…`) или заранее объявить флагом `--blob` и сослаться на него по имени; подробно — в заметке [[blob]].
```bash
# Inline hex blob (маскировка под начало TLS record)
--lua-desync=multisplit:pos=1:seqovl=5:seqovl_pattern=0x1603030000
# Предзагруженный blob
--blob=tlspat:0x1603030100 \
--lua-desync=multisplit:pos=1:seqovl=8:seqovl_pattern=tlspat
```
Если `optional` задан и blob `seqovl_pattern` отсутствует — используется нулевой паттерн (операция не отменяется). Сам флаг описан в разделе [A) Собственные аргументы multisplit](#a-собственные-аргументы-multisplit).
**Важно в multisplit:** `seqovl` — только **число**, маркеры не поддерживаются (в отличие от `multidisorder` и `fakeddisorder`, где seqovl может быть маркером). Как seqovl задаётся маркером там, разобрано в разделах [[Zapret2/desync/multidisorder#seqovl как маркер|seqovl как маркер]] и [[Zapret2/desync/fakeddisorder#seqovl — маркер, а не число|seqovl — маркер, а не число]].
---
## Полный список аргументов
Аргументы делятся на собственные аргументы `multisplit` (раздел A) и стандартные (разделы B–H): их понимают и другие функции дурения, а общий обзор есть в [[desync]]. Формат вызова:
```
--lua-desync=multisplit[:arg1[=val1][:arg2[=val2]]...]
```
Все `val` приходят в Lua как строки. Если `=val` не указан, значение = пустая строка `""` (в Lua это truthy, то есть считается истинным значением), поэтому флаги пишутся просто как `:optional`, `:nodrop`, `:tcp_ts_up`.
### A) Собственные аргументы multisplit
#### `pos`
- **Формат:** `pos=<marker[,marker2,...]>`
- **Тип:** строка со списком маркеров через запятую
- **По умолчанию:** `"2"`
- **Описание:** Точки разреза, смещения считаются от начала данных с нуля. N маркеров → до N+1 сегментов. Виды маркеров — в разделе [Маркеры позиций (pos)](#маркеры-позиций-pos)
- **Примеры:**
- `pos=2` — разрез после 2-го байта (дефолт)
- `pos=1` — первый байт уходит отдельным сегментом
- `pos=midsld` — разрез посередине SLD
- `pos=1,midsld` — два разреза: после 1-го байта и посередине SLD → 3 сегмента
- `pos=host,midsld,endhost-2,-10` — четыре разреза → до 5 сегментов
- `pos=method+2` — после первых 2 символов HTTP-метода (сам `pos=method` не сработает, см. [Важные нюансы pos](#важные-нюансы-pos))
#### `seqovl`
- **Формат:** `seqovl=N` (где N > 0)
- **Тип:** только число (маркеры **не поддерживаются** — в отличие от multidisorder)
- **По умолчанию:** не задан (нет seqovl)
- **Описание:** Применяется **только к первому** отправляемому сегменту. К данным первого сегмента слева добавляется N байт `seqovl_pattern`, а TCP `th_seq` (поле номера последовательности в TCP-заголовке) уменьшается на N. Сервер отбросит левую часть, DPI — может не отбросить. Механика — в разделе [seqovl](#seqovl--скрытый-фейк-внутри-сегмента)
- **Примеры:**
- `seqovl=5` — 5 байт фейка слева
- `seqovl=13` — 13 байт фейка слева
- `seqovl=10000` — 10000 байт (если превысит MSS — автосегментация разобьёт на несколько TCP-сегментов)
#### `seqovl_pattern`
- **Формат:** `seqovl_pattern=<blobName>`
- **Тип:** имя blob-переменной
- **По умолчанию:** один байт `0x00`, повторяемый до длины `seqovl`
- **Описание:** Данные для заполнения seqovl-области. Blob повторяется функцией `pattern()` до нужного размера
- **Поведение с `optional`:** если `optional` задан и blob отсутствует — используется нулевой паттерн, seqovl не отменяется
- **Примеры:**
- `seqovl_pattern=0x1603030000` — inline hex (маскировка под TLS)
- `seqovl_pattern=my_pattern_blob` — предзагруженный blob
#### `blob`
- **Формат:** `blob=<blobName>`
- **Тип:** имя blob-переменной
- **По умолчанию:** не задан
- **Описание:** Заменить текущий payload/reasm на указанный blob и резать/слать его. Используется для отправки произвольных данных (фейковых payload, модифицированных ClientHello и т.д.). Порядок выбора данных — в разделе [Откуда берутся данные для нарезки](#откуда-берутся-данные-для-нарезки)
- **Примеры:**
- `blob=fake_default_tls` — стандартный TLS-фейк (что внутри, см. [[Zapret2/desync/fake#Стандартные blob-ы|стандартные blob-ы]])
- `blob=0xDEADBEEF` — inline hex
- `blob=my_custom_ch` — предзагруженный blob
#### `optional`
- **Формат:** `optional` (флаг, без значения)
- **Описание:** Мягкий режим:
- Если задан `blob=...` и blob отсутствует → multisplit **ничего не делает** (тихий skip, без ошибок)
- Если задан `seqovl_pattern=...` и blob отсутствует → используется нулевой паттерн (seqovl не отменяется)
- **Использование:** защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией)
#### `nodrop`
- **Формат:** `nodrop` (флаг, без значения)
- **Описание:** После успешной отправки сегментов **не выносить** `VERDICT_DROP` (вместо этого вернуть `VERDICT_PASS` — «пропустить пакет»). Это означает, что оригинальный пакет тоже будет отправлен (наряду с нарезанными сегментами)
- **Использование:** для отладки, для отправки произвольных данных без блокировки оригинала
- **Предупреждение:** в боевых профилях `nodrop` обычно нежелателен — оригинал ещё раз уйдёт, что создаст дублирование и может ухудшить обход (см. [нюанс 4](#4-nodrop-создаёт-дублирование))
---
### B) Standard direction
Направление говорит, в какую сторону идёт пакет: от вашего компьютера к серверу (исходящий) или обратно (входящий).
| Параметр | Значения | По умолчанию |
|:---------|:---------|:-------------|
| `dir` | `in`, `out`, `any` | `out` |
Фильтр по направлению пакета. `multisplit` по умолчанию работает только с исходящими (`out`).
- `dir=out` — только исходящие (от клиента к серверу)
- `dir=in` — только входящие (от сервера к клиенту)
- `dir=any` — оба направления
При первом вызове с указанным `dir` функция делает `direction_cutoff_opposite` — отсекает себя от противоположного направления. Cutoff (отсечение) значит, что движок больше не будет вызывать функцию на пакетах этого направления в текущем соединении; подробно — в [[Zapret2/жизненный цикл desync-функции#Стадия 2. Отсечение противоположного направления|стадии 2 жизненного цикла]].
---
### C) Standard payload
| Параметр | Значения | По умолчанию |
|:---------|:---------|:-------------|
| `payload` | список типов через запятую | `known` |
Фильтр по [[Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока)|типу payload]] на уровне Lua. Это **дополнительный** фильтр к `--payload=...` на уровне [[Zapret2/profile#Анатомия профиля|профиля]].
- `payload=known` — только распознанные протоколы (`http_req`, `tls_client_hello`, `quic_initial` и т.д.)
- `payload=all` — любой payload, включая `unknown`
- `payload=tls_client_hello,http_req` — конкретные типы
- `payload=~unknown` — инверсия: всё кроме unknown
**Важно:** лучше ставить `--payload=...` на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.
Проще говоря: фильтр `--payload=` на уровне профиля работает в быстром ядре программы, написанном на C, а аргумент `payload=` проверяется уже в Lua, то есть медленнее. Поэтому основной отбор лучше делать на уровне профиля.
---
### D) Standard fooling
Fooling — намеренная порча заголовков пакета, обычно для того, чтобы сервер отбросил фейк, а DPI его прочитал (подробно — [[Zapret2/ts-and-fooling#Что такое fooling|Что такое fooling]]). Модификации L3/L4 заголовков, то есть заголовков IP (сетевой уровень) и TCP (транспортный уровень). В `multisplit` применяются **ко всем** отправляемым сегментам (в отличие от fakedsplit, где fooling идёт только на фейки). Поэтому большинство опций из таблицы в `multisplit` вредны: почему — в [нюансе 6](#6-fooling-применяется-ко-всем-сегментам).
| Параметр | Описание | Пример |
|:---------|:---------|:-------|
| `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` |
**Заметка про tcp_ts_up:** На Linux-серверах пакеты с инвалидным ACK стабильно отбрасываются **только если** TCP timestamp option идёт первой в заголовке. `tcp_ts_up` перемещает её в начало, обеспечивая корректную работу badseq-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 в рамках соединения (требует tracking — отслеживания соединения) | — |
`ip_id` применяется к **каждому** отправляемому сегменту (включая под-сегменты при MSS-сегментации, см. [Автосегментация по MSS](#автосегментация-по-mss)).
---
### F) Standard ipfrag
IP-фрагментация **поверх** TCP-сегментации. Каждый TCP-сегмент дополнительно фрагментируется на уровне IP. Проще говоря: TCP-сегмент, уже отрезанный `multisplit`, при отправке ещё раз делится на части, но уже средствами протокола IP, а собирает их обратно получатель на уровне IP.
| Параметр | Описание | По умолчанию |
|:---------|:---------|:-------------|
| `ipfrag[=func]` | Включить IP-фрагментацию. Если без значения → `ipfrag2` | — |
| `ipfrag_disorder` | Отправить IP-фрагменты в обратном порядке | — |
| `ipfrag_pos_tcp=N` | Позиция фрагментации TCP (кратно 8) | `32` |
| `ipfrag_pos_udp=N` | Позиция фрагментации UDP (кратно 8). Для multisplit бесполезно — он только TCP | `8` |
| `ipfrag_next=N` | IPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы) | — |
---
### G) Standard reconstruct
Реконструкция — сборка готового raw-пакета («сырого», со всеми заголовками) перед отправкой.
| Параметр | Описание |
|:---------|:---------|
| `badsum` | Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
---
### H) Standard rawsend
Rawsend — отправка готовых сегментов в сеть самой функцией; в `multisplit` её выполняет `rawsend_payload_segmented` (см. [Автосегментация по MSS](#автосегментация-по-mss)). Эти параметры управляют отправкой.
| Параметр | Описание |
|:---------|:---------|
| `repeats=N` | Отправить каждый сегмент N раз (идентичные повторы) |
| `ifout=<iface>` | Интерфейс для отправки (по умолчанию определяется автоматически) |
| `fwmark=N` | Firewall mark (только Linux, nftables/iptables) — метка пакета для правил фаервола |
---
## Порядок отправки сегментов
`multisplit` всегда отправляет сегменты **последовательно** — от первого к последнему (в порядке возрастания TCP sequence, то есть номеров байтов в потоке). Обратный порядок — отличительная черта [[Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder|multidisorder]].
### Пример с 3 позициями разреза
Возьмём payload длиной 600 байт и три позиции разреза: `pos=100,300,450`. Они делят данные на четыре куска (0–99, 100–299, 300–449, 450–599), и `multisplit` отправляет их четырьмя TCP-сегментами по возрастанию sequence number. Sequence каждого сегмента равен смещению его первого байта в исходном payload, поэтому сервер собирает поток обратно без пропусков:
| Сегмент | Байты payload | Длина | Смещение (seq) |
|:--------|:--------------|:------|:---------------|
| 1 | 0–99 | 100 | 0 |
| 2 | 100–299 | 200 | 100 |
| 3 | 300–449 | 150 | 300 |
| 4 | 450–599 | 150 | 450 |
### Пример с seqovl=10
Тот же payload 600 байт, но `pos=100` (один разрез, два сегмента) и `seqovl=10`. К первому сегменту слева дописываются 10 байт паттерна, а его sequence сдвигается на те же 10 в минус — поэтому паттерн уходит за левую границу окна и сервер его отбрасывает, приняв только реальную часть с seq 0:
| Сегмент | Содержимое | Длина | Смещение (seq) | Что с ним на сервере |
|:--------|:-----------|:------|:---------------|:---------------------|
| 1 | 10 байт паттерна + байты 0–99 | 110 | −10 | паттерн отброшен, реальная часть принята |
| 2 | байты 100–599 | 500 | 100 | принят целиком |
---
## Поведение при replay / reasm
Многопакетный запрос (например, большой TLS ClientHello с post-quantum ключами, не влезающий в один TCP-сегмент) `nfqws2` придерживает, собирает в буфер `reasm_data` (reasm — повторная сборка) и затем перепроигрывает через desync-функции. Перепроигрывание (replay) означает, что движок по очереди вызывает функции на каждой придержанной части, но уже с собранным целым на руках. `multisplit` использует штатную развилку перепроигрывания из общего скелета desync-функций ([[Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)|жизненный цикл desync-функции, стадия 6]]) без изменений: режет и отправляет **весь** собранный буфер на первой части, а остальные части дропает, потому что их содержимое уже ушло в составе нарезки.
Специфика именно `multisplit` тут одна: [маркеры](#маркеры-позиций-pos) разрешаются по всему `reasm_data`, а не по отдельному пакету. Поэтому разрез по `midsld` попадёт в реальную середину домена, даже если имя хоста физически лежало во втором или третьем пакете исходного потока.
Если отправка сорвалась (`rawsend` вернул `false`), флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть — данные не теряются.
---
## Автосегментация по MSS
MSS (Maximum Segment Size) — наибольший объём данных, который помещается в один TCP-сегмент данного соединения. О размерах TCP-сегментов думать **не нужно**. Отправкой занимается `rawsend_payload_segmented` из `zapret-lib.lua`, и она сама доводит куски до допустимого размера:
1. MSS берётся из `desync.tcp_mss` — движок отслеживает его для каждого TCP-соединения
2. Если кусок вместе с заголовками превышает MSS — он дополнительно режется по MSS
3. Каждый под-сегмент отправляется с корректным TCP sequence (`th_seq` увеличивается на длину предыдущего)
Есть тонкость с тем, что применяется к под-сегментам, а что нет: [fooling](#d-standard-fooling) применяется один раз к исходному диссекту (разобранному на поля пакету), то есть достаётся всем под-сегментам одинаково, а политика [ip_id](#e-standard-ipid) применяется отдельно к каждому под-сегменту.
**Пример:** `seqovl=10000` не вызовет ошибку. Функция отправит несколько TCP-сегментов подряд, начиная со сдвига −10000 относительно текущего sequence: сначала пойдут полные сегменты паттерна, а в последнем к остатку паттерна будет пристыковано начало реальных данных.
---
## Нюансы и подводные камни
### 1. Работает только с TCP — и почему
`multisplit` обрабатывает исключительно TCP. Первая же стадия общего скелета отсеивает всё остальное: на не-TCP пакете функция делает `instance_cutoff` и навсегда отключается от этого потока, чтобы не тратить CPU (исключение — связанный ICMP; механика описана в [[Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff|жизненный цикл desync-функции, стадия 1]]).
Ограничение не техническая недоработка, а следствие сути приёма — он опирается на два свойства, которые есть у TCP и отсутствуют у UDP.
**TCP — это поток байтов со сквозной нумерацией, UDP — набор независимых датаграмм.** В TCP у каждого байта есть свой sequence number, и получатель обязан по этим номерам собрать поток в исходном порядке — это гарантия протокола. Поэтому один payload можно разрезать на несколько сегментов, разослать их (по порядку — `multisplit`, вперемешку — [[multidisorder]]), и сервер прозрачно склеит всё обратно: ему безразлично, пришли данные одним пакетом или пятью. В UDP такого нет — каждая датаграмма самодостаточна, никакой пересборки «потока» по номерам байт на приёмной стороне не происходит. Разрезать UDP-payload на две датаграммы бессмысленно: принимающее приложение получит два отдельных куска и не обязано (и не будет) склеивать их так, как задумано. Резать в TCP-стиле там просто нечего.
**seqovl тем более завязан на TCP.** Трюк [seqovl](#seqovl--скрытый-фейк-внутри-сегмента) держится на конкретной детали TCP-стека: получатель отбрасывает байты, чей sequence number ушёл левее границы окна приёма. Дописываем фейк слева, сдвигаем seq в минус — сервер выкидывает фейк, а DPI его анализирует. У UDP нет ни sequence number, ни окна приёма, ни понятия «байты левее границы» — эксплуатировать нечего.
**Что делают с UDP вместо этого.** UDP-протоколы (в первую очередь QUIC) обходят другими desync-функциями: [[fake]] (фейковая датаграмма перед реальной), IP-фрагментация `ipfrag` (режется на уровне IP, а не TCP), манипуляции с длиной и прочее. То есть UDP работают на других уровнях, а TCP-сегментация как класс к нему неприменима. Обзор всех функций дурения — в [[desync]].
> [!note] Проще говоря
> `multisplit` режет один payload на куски и рассчитывает, что получатель склеит их обратно «по номерам». Такое склеивание умеет только TCP. UDP обрабатывает каждый пакет сам по себе и склеивать по номерам не умеет — значит, и резать в этом стиле нечего.
### 2. Разрез в самом начале данных отбрасывается
`delete_pos_1(pos)` убирает из списка Lua-позицию 1 — она соответствует маркеру `0`, то есть разрезу перед первым байтом, от которого первый сегмент вышел бы пустым. Если после этой чистки не осталось ни одной позиции, `multisplit` не делает ничего и пакет уходит целым.
Путаться тут легко из-за разной нумерации: в командной строке маркеры считаются от нуля, внутри Lua — от единицы. Поэтому под удаление попадает не `pos=1`, а `pos=0`. Самая частая реальная жертва — `pos=method` для HTTP: метод обычно лежит на смещении 0, маркер разрешается в удаляемую позицию, и разреза не происходит. Рабочий вариант — `pos=method+2`.
### 3. Все маркеры могут не разрешиться
Если вы указали `pos=midsld,sniext` для HTTP-payload, оба маркера (специфичные для TLS) не разрешатся. Multisplit напишет в лог "no valid split positions" и ничего не сделает. Проще говоря: если ни один маркер из списка не нашёлся в данных этого [[Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока)|типа payload]], пакет уходит целым, без единого разреза. Какие маркеры к каким типам подходят, показано в таблице [относительных маркеров](#относительные-маркеры).
### 4. nodrop создаёт дублирование
С `nodrop` multisplit отправляет нарезанные сегменты И пропускает оригинальный пакет. Сервер получит данные дважды. Используйте `nodrop` только для отладки или когда это осознанно нужно.
### 5. seqovl=10000 не вызовет ошибку
В отличие от nfqws1, где большие значения seqovl вызывали ошибку, nfqws2 автоматически сегментирует по MSS. Большой seqovl просто создаст много под-сегментов. Как именно они строятся, описано в разделе [Автосегментация по MSS](#автосегментация-по-mss).
### 6. Fooling применяется ко ВСЕМ сегментам
В `multisplit` все сегменты — а других, кроме реальных, функция и не шлёт — получают [fooling](#d-standard-fooling) целиком. У [[fakedsplit]] и [[fakeddisorder]] иначе: там fooling в полном объёме достаётся только фейкам, а к реальным частям применяется единственная опция `tcp_ts_up` (см. [[Zapret2/desync/fakedsplit#D) Standard fooling|раздел о fooling в fakedsplit]]).
Практическое следствие: портящие пакет опции в `multisplit` вредны. Если задать `tcp_ack=-66000`, **все** сегменты получат инвалидный ack, сервер их отбросит — и соединение просто не состоится. Осмысленны здесь лишь те опции, которые не мешают серверу принять данные: `tcp_ts_up`, политика `ip_id`, IPv6 extension headers. Подробнее про сами опции — в [[ts-and-fooling]].
### 7. Порядок инстансов важен
Инстанс — одна запись `--lua-desync=…` в профиле; в профиле их может быть несколько, и движок вызывает их по очереди. Если перед `multisplit` стоит инстанс, модифицирующий диссект (например `pktmod`), то `multisplit` порежет уже изменённые данные — порядок `--lua-desync` в профиле определяет порядок применения. Разбор того, как аргументы и инстансы выстраиваются в цепочку, — в [[последовательность аргументов]] и [[desync]].
Частое заблуждение: `VERDICT_DROP` якобы обрывает цепочку. Это не так. Движок проходит **все** инстансы профиля и агрегирует их вердикты (`VERDICT_MODIFY` перебивает `VERDICT_PASS`, `VERDICT_DROP` перебивает оба). Инстансы, стоящие после `multisplit`, будут вызваны как обычно и увидят тот же самый диссект; дроп означает лишь, что оригинальный пакет в итоге не будет отправлен. Оборвать выполнение может только явный вызов `execution_plan_cancel`, которого в `multisplit` нет. Как складываются вердикты, подробно описано в [[Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация|стадии 8 жизненного цикла]].
---
## Отличия от других функций сегментации
Четыре функции ниже режут данные на TCP-сегменты, но различаются числом разрезов, порядком отправки, наличием фейков и тем, как устроен `seqovl`. Подробности о каждой — в разделах [[Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder|multidisorder]], [[Zapret2/desync/fakedsplit#Последовательность отправки (6 пакетов)|fakedsplit]] и [[Zapret2/desync/fakeddisorder#Порядок отправки сегментов (6 пакетов)|fakeddisorder]].
| Аспект | `multisplit` | `multidisorder` | `fakedsplit` | `fakeddisorder` |
|:-------|:-------------|:----------------|:-------------|:----------------|
| Количество позиций | Список (любое кол-во) | Список (любое кол-во) | **Одна** | **Одна** |
| Порядок отправки | Прямой (1→2→3) | Обратный (3→2→1) | Прямой | Обратный |
| Фейковые сегменты | **Нет** | **Нет** | Да (до 4 шт.) | Да (до 4 шт.) |
| seqovl тип | Только число | **Маркер** | Только число | **Маркер** |
| seqovl к какому сегменту | 1-й | 2-й (предпоследний) | 1-й реальный | 2-й реальный |
| Fooling к | Всем сегментам | Всем сегментам | Фейкам полностью, реальным частям — только `tcp_ts_up` | Фейкам полностью, реальным частям — только `tcp_ts_up` |
| ipfrag | Да | Да | **Нет** | **Нет** |
Проще говоря: `multisplit` и `multidisorder` только режут настоящие данные (по порядку или задом наперёд), а `fakedsplit` и `fakeddisorder` делают один разрез и прячут настоящие куски среди фейков.
---
## Миграция с nfqws1
Этот раздел для тех, кто переносит настройки из первой версии zapret (программа `nfqws`) во вторую (`nfqws2`). В первой версии техника выбиралась ключом `--dpi-desync=…`, а её параметры — отдельными ключами `--dpi-desync-…`; во второй всё это записывается аргументами одного вызова `--lua-desync=multisplit:…`.
### Соответствие параметров
| nfqws1 | nfqws2 |
|:-------|:-------|
| `--dpi-desync=multisplit` | `--lua-desync=multisplit` |
| `--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,multisplit`. В nfqws2 каждая техника становится отдельным инстансом `--lua-desync`, а фейки для TLS и HTTP — разными инстансами под своими `--payload`, потому что им нужны разные [[Zapret2/desync/fake#Стандартные blob-ы|стандартные blob-ы]]:
```bash
# nfqws1:
nfqws --dpi-desync=fake,multisplit \
--dpi-desync-fooling=md5sig \
--dpi-desync-split-pos=1,midsld \
--dpi-desync-split-seqovl=5 \
--dpi-desync-split-seqovl-pattern=0x1603030000 \
--dpi-desync-fake-tls-mod=rnd,rndsni,dupsid
# nfqws2 (эквивалент):
nfqws2 \
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--payload=http_req \
--lua-desync=fake:blob=fake_default_http:tcp_md5 \
--payload=tls_client_hello,http_req \
--lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000
```
Второй пример переносит связку с [[Zapret2/desync/syndata#Зачем нужен syndata|syndata]] и ключом `--wssize`. Здесь каждый ключ nfqws1 тоже становится своим инстансом, и их порядок в nfqws2 задаёт порядок применения (см. [нюанс 7](#7-порядок-инстансов-важен)):
```bash
# nfqws1:
nfqws --dpi-desync=syndata,multisplit --dpi-desync-split-pos=midsld --wssize 1:6
# nfqws2 (порядок инстансов важен!):
nfqws2 \
--lua-desync=wssize:wsize=1:scale=6 \
--lua-desync=syndata \
--lua-desync=multisplit:pos=midsld
```
---
## Практические примеры
Каждый пример — готовая строка для [[Zapret2/profile#Анатомия профиля|профиля]]; под ней объяснено, что получится на выходе.
### Минимальный (дефолт: pos=2, dir=out, payload=known)
```bash
--lua-desync=multisplit
```
Разрезает payload после 2-го байта → 2 сегмента: первые 2 байта и весь остаток.
### HTTP: разрез после метода
```bash
--payload=http_req --lua-desync=multisplit:pos=method+2
```
Для `GET /path...` разрежет после `GE` → DPI не увидит полный метод. Сдвиг `+2` нужен потому, что голый `pos=method` указывает на самое начало данных и отбрасывается (см. [нюанс 2](#2-разрез-в-самом-начале-данных-отбрасывается)).
### HTTP: несколько разрезов вокруг hostname
```bash
--payload=http_req --lua-desync=multisplit:pos=host,midsld,endhost
```
Разрезает: до hostname | первая половина | вторая половина | после hostname → 4 сегмента.
### TLS: разрез посередине SNI
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=midsld
```
SNI разрезан пополам — ни в одном сегменте нет полного домена.
### TLS: два разреза + seqovl
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000
```
3 сегмента: первый с 5-байтовым TLS-фейком слева (DPI может принять за начало TLS record). Механика — в разделе [seqovl](#seqovl--скрытый-фейк-внутри-сегмента).
### Произвольный blob вместо payload
```bash
--blob=mydata:@custom_payload.bin \
--lua-desync=multisplit:blob=mydata:pos=10,100,-20
```
Режет и отправляет произвольные данные из файла вместо реального payload. Как объявлять такие данные, описано в заметке [[blob]].
### Защита от отсутствующего blob
```bash
--lua-desync=multisplit:blob=maybe_missing:optional:pos=2
```
Если blob не существует — тихий пропуск, без ошибок и без VERDICT_DROP.
### Отладка: не блокировать оригинал
```bash
--payload=http_req --lua-desync=multisplit:pos=method+2:nodrop
```
Отправляет нарезанные сегменты И пропускает оригинальный пакет (для экспериментов). В рабочей настройке так не делают: сервер получит данные дважды (см. [нюанс 4](#4-nodrop-создаёт-дублирование)).
### С TCP timestamp + IP ID
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1:tcp_ts_up:ip_id=seq:ip_id_conn
```
Первый байт уходит отдельным сегментом. `tcp_ts_up` поднимает опцию TCP timestamp в начало заголовка, а `ip_id=seq` вместе с `ip_id_conn` дают сегментам последовательные IP ID со сквозной нумерацией в рамках соединения. Эти опции не мешают серверу принять данные, поэтому их можно применять ко всем сегментам (см. [нюанс 6](#6-fooling-применяется-ко-всем-сегментам)).
### Повторы отправки
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1:repeats=2
```
Каждый сегмент отправляется 2 раза. Повторы бинарно идентичны — `repeats` не трогает содержимое пакета, в том числе `ip_id`. Если нужны разные `ip_id`, `repeats` не подойдёт.
### IP-фрагментация поверх TCP-сегментации
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32
```
Каждый TCP-сегмент дополнительно фрагментируется на IP-уровне в обратном порядке. Параметры фрагментации описаны в разделе [F) Standard ipfrag](#f-standard-ipfrag).
### Комбинация: fake → multisplit
```bash
--payload=tls_client_hello \
--lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
--lua-desync=multisplit:pos=1,midsld:seqovl=5:seqovl_pattern=0x1603030000
```
Сначала отправляется фейковый TLS ClientHello (с fooling), затем реальный — нарезанный на 3 сегмента с seqovl. Как устроен сам фейк и его опции `tls_mod`, разобрано в заметке [[Zapret2/desync/fake#Зачем нужен fake|fake]].
### Боевой пример для YouTube
```bash
--filter-tcp=443 --hostlist=youtube.txt \
--lua-desync=fake:blob=fake_default_tls:repeats=11:tcp_md5 \
--lua-desync=multisplit:pos=1,midsld
```
11 фейков подряд + реальный payload разрезан на 3 части. Профиль срабатывает только на TCP-порт 443 и на домены из списка `youtube.txt`; фейки испорчены опцией `tcp_md5`, чтобы сервер их отбросил.
---
## Как это устроено внутри
Дальше идёт разбор кода функции. Для настройки обхода он не нужен — практическая часть закончилась выше. Читать имеет смысл, если вы правите Lua-функции zapret2 или пишете свои.
### Место в общем скелете
Тело `multisplit` построено по тому же шаблону, что и все остальные функции дурения: восемь стадий от отсева чужого транспорта до вердикта. Разобраны они один раз в [[жизненный цикл desync-функции]] — здесь только отклонения:
| Стадия общего скелета | Что делает `multisplit` |
|:----------------------|:------------------------|
| 1. Отсев транспорта | только TCP; на не-TCP делает `instance_cutoff` (кроме связанного ICMP) |
| 2. Направление | `dir=out` по умолчанию, отключается от входящего |
| 3. Аргументы | обязательных нет; `optional` + отсутствующий `blob` → тихий выход |
| 4. Данные | штатная цепочка `blob → reasm → payload` |
| 5. Гварды | штатные `#data>0`, `direction_check`, `payload_check` (по умолчанию `known`) |
| 6. replay | штатная развилка: нарезка на первой части, дроп остальных |
| 7. **Своя техника** | разрешение списка маркеров, нарезка на части, `seqovl` на первом сегменте, отправка по возрастанию sequence — **см. ниже** |
| 8. Вердикт | `VERDICT_DROP` после успешной отправки, `VERDICT_PASS` при `nodrop` или сбое `rawsend` |
Гварды в пятой строке — проверки-предохранители: данные не пусты, направление и тип payload подходят ([[Zapret2/жизненный цикл desync-функции#Стадия 5. Три гварда|стадия 5]]).
Из общего скелета полезно помнить две вещи, о которых спотыкаются чаще всего. Первая: работу функция делает **не через `return`**, а вызовами `rawsend_payload_segmented` — к моменту возврата сегменты уже улетели в сеть, а возвращаемое значение решает судьбу только перехваченного оригинала. Вторая: `VERDICT_DROP` не обрывает цепочку инстансов, вердикты всех инстансов профиля агрегируются (см. [нюанс 7](#7-порядок-инстансов-важен)).
### Стадия 7: нарезка и отправка
Ниже — код собственно техники (`lua/zapret-antidpi.lua:483`). Всё, что до него, — общий скелет.
**Разрешение позиций разреза.** Строку маркеров (`desync.arg.pos` или дефолт `"2"`) превращаем в массив числовых позиций, затем убираем Lua-позицию 1 — она соответствует маркеру `0`, разрезу перед первым байтом:
```lua
local pos = resolve_multi_pos(data, desync.l7payload, spos)
delete_pos_1(pos) -- cannot split at the first byte
```
**Цикл нарезки и отправки.** Если после чистки осталась хоть одна позиция, идём по частям. Для части `i` начало — `pos[i]` (для нулевой части — 1), конец — `pos[i+1]-1` (для последней — весь остаток `#data`). Вырезаем кусок, для первой части (`i==0`) при заданном `seqovl` дописываем слева паттерн-фейк и отправляем сегмент со сдвинутым sequence number:
```lua
for i=0,#pos do
local pos_start = pos[i] or 1
local pos_end = i<#pos and pos[i+1]-1 or #data
local part = string.sub(data,pos_start,pos_end)
local seqovl=0
if i==0 and desync.arg.seqovl and tonumber(desync.arg.seqovl)>0 then
seqovl = tonumber(desync.arg.seqovl)
local pat="\x00"
if desync.arg.seqovl_pattern then
-- ... взять blob или нулевой паттерн ...
pat = blob(desync,desync.arg.seqovl_pattern)
end
part = pattern(pat,1,seqovl)..part
end
if not rawsend_payload_segmented(desync,part,pos_start-1-seqovl) then
return VERDICT_PASS
end
end
```
Третий аргумент `rawsend_payload_segmented` — это сдвиг sequence number: `pos_start-1` переводит 1-based позицию Lua обратно в 0-based смещение байта в payload (то есть даёт правильный seq для сегмента), а вычитание `seqovl` уводит первый сегмент влево на длину фейка. Если `rawsend` вернул `false` — цикл прерывается и оригинал пропускается (`VERDICT_PASS`), чтобы данные не потерялись.
После успешной отправки всех частей ставится флаг «реасм уже отправлен» и выносится вердикт — это снова общий скелет:
```lua
replay_drop_set(desync)
return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP
```
### Псевдокод алгоритма
Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в [[жизненный цикл desync-функции]]:
```lua
function multisplit(ctx, desync)
-- 1. Проверка: только TCP
if not desync.dis.tcp then cutoff; return end
-- 2. Cutoff противоположного направления
direction_cutoff_opposite(ctx, desync)
-- 3. Проверка optional blob
if optional and blob specified and blob not exists then return end
-- 4. Выбор данных
data = blob_or_def(blob) or reasm_data or dis.payload
-- 5. Проверки: данные не пусты, направление OK, payload OK
if #data > 0 and direction_check() and payload_check() then
-- 6. Только первый replay
if replay_first() then
-- 7. Разрешение маркеров
pos = resolve_multi_pos(data, l7payload, pos_arg or "2")
delete_pos_1(pos) -- удалить Lua-позицию 1 (маркер 0)
if #pos > 0 then
-- 8. Цикл по частям (i=0 до #pos)
for i = 0, #pos do
pos_start = pos[i] or 1
pos_end = (i < #pos) and pos[i+1]-1 or #data
part = data:sub(pos_start, pos_end)
-- 9. seqovl для первого сегмента
seqovl = 0
if i == 0 and arg.seqovl > 0 then
seqovl = tonumber(arg.seqovl)
pat = seqovl_pattern_blob or "\x00"
part = pattern(pat, 1, seqovl) .. part
end
-- 10. Отправка с автосегментацией
rawsend_payload_segmented(part, pos_start - 1 - seqovl)
end
-- 11. Пометить как отправленное
replay_drop_set()
return nodrop and VERDICT_PASS or VERDICT_DROP
end
else
-- 12. Не первый replay — дропнуть если ранее успешно отправлено
if replay_drop() then
return nodrop and VERDICT_PASS or VERDICT_DROP
end
end
end
end
```
---
## 📚 См. также
- [[desync]] — обзор всех функций `--lua-desync` и общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты
- [[жизненный цикл desync-функции]] — восемь стадий, общих для всех техник дурения: отсев транспорта, выбор данных, replay, вердикты
- [[структура desync и диссекта]] — подробное устройство таблицы `desync` и диссекта пакета
- [[multidisorder]] — тот же разрез, но сегменты уходят в обратном порядке; там `seqovl` работает принципиально иначе
- [[multidisorder_legacy]] — вариант `multidisorder`, полностью совместимый с алгоритмом nfqws1
- [[fakedsplit]] · [[fakeddisorder]] — разрез по одной позиции с подмешиванием фейковых сегментов
- [[hostfakesplit]] — разрез по границам имени хоста с подмешиванием фейкового имени
- [[tcpseg]] — отправка произвольного диапазона данных, ограниченного двумя маркерами
- [[oob]] — сегментация с urgent-байтом вместо разреза
- [[fake]] — отдельный фейковый пакет, с которым `multisplit` чаще всего комбинируют
- [[blob]] — как объявлять и передавать данные для аргументов `blob` и `seqovl_pattern`
- [[payload]] — распознавание типов протоколов, от которого зависит работа относительных маркеров
- [[ts-and-fooling]] — опции fooling и почему в `multisplit` большинство из них вредны
- [[verify-strategy]] — как честно проверить, работает ли подобранная комбинация
- [[последовательность аргументов]] — как выстраивается цепочка инстансов в профиле
- [[profile]] · [[preset]] — где `--lua-desync` живёт среди остальных настроек
- [[DPI/DPI|DPI и ТСПУ]] — как устроена инспекция трафика, против которой работает `multisplit`
---
> **Источники:** `lua/zapret-antidpi.lua:483-539` (тело `multisplit`), `lua/zapret-lib.lua` (`blob_or_def`, `pattern`, `delete_pos_1`, `rawsend_payload_segmented`, `direction_check`, `replay_first`/`replay_drop`), `nfq2/lua.c` и `nfq2/protocol.c` (`resolve_pos`/`resolve_multi_pos`/`ResolveMultiPos`), `docs/manual.md`, `docs/readme.md` из репозитория zapret2.
---
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/desync/multisplit.md) · [скачать весь репозиторий одним zip-архивом](https://git.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).