todo/Zapret2/desync/multisplit.md
loop-uh 08b4491545
Some checks failed
Published content check / validate (push) Failing after 3s
Завершить переезд базы знаний на Forgejo
Обновить правила репозитория и подписи исходников в 99 заметках, не затрагивая пользовательские незакоммиченные файлы. Сделать Forgejo Actions содержательным: проверять опубликованный commit, а не пустое рабочее дерево после checkout.
2026-08-07 08:27:28 +03:00

913 lines
72 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
date: 2026-02-17
tags:
- zapret
- zapret2
- nfqws2
- lua
- lua-desync
- antidpi
- tcp
- multisplit
- seqovl
aliases:
- multisplit
---
# `multisplit` — последовательная TCP-сегментация (zapret2 / nfqws2)
**Файл:** `lua/zapret-antidpi.lua:483`
**nfqws1 эквивалент:** `--dpi-desync=multisplit`
**Сигнатура:** `function multisplit(ctx, desync)`
`multisplit` — базовая функция TCP-сегментации в zapret2. Она берёт текущий payload (или reasm, или blob), разрезает его на несколько TCP-сегментов по заданным позициям и отправляет их **последовательно** (от первого к последнему). После успешной отправки выносит `VERDICT_DROP`, чтобы оригинальный пакет не ушёл.
Родственные функции: [[multidisorder]] (обратный порядок), [[fakedsplit]] (с фейками), [[fakeddisorder]] (фейки + обратный порядок), [[hostfakesplit]] (по hostname), [[tcpseg]] (диапазон), [[oob]] (urgent byte).
> [!info] О чём заметка
> `multisplit` — функция дурения DPI в [[Zapret2/Zapret2|Zapret 2]] (движок `nfqws2`), которая режет исходящий TCP-пакет на несколько кусков, чтобы цензор не увидел в нём имя запрашиваемого сайта. Здесь разобраны все аргументы функции, маркеры позиций разреза, техника `seqovl` и готовые примеры команд. Общий контракт `--lua-desync`-функций (кто и когда их вызывает) описан в [[desync]], общий для всех техник порядок работы — в [[жизненный цикл desync-функции]], устройство входной таблицы `desync` — в [[структура desync и диссекта]].
## 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)
- [Порядок отправки сегментов](#порядок-отправки-сегментов)
- [Поведение при replay / reasm](#поведение-при-replay--reasm)
- [Автосегментация по MSS](#автосегментация-по-mss)
- [Нюансы и подводные камни](#нюансы-и-подводные-камни)
- [Отличия от других функций сегментации](#отличия-от-других-функций-сегментации)
- [Миграция с nfqws1](#миграция-с-nfqws1)
- [Практические примеры](#практические-примеры)
- [Как это устроено внутри](#как-это-устроено-внутри)
- [Место в общем скелете](#место-в-общем-скелете)
- [Стадия 7: нарезка и отправка](#стадия-7-нарезка-и-отправка)
- [Псевдокод алгоритма](#псевдокод-алгоритма)
- [📚 См. также](#-см-также)
---
## Зачем нужен multisplit
DPI анализирует TCP-поток, пытаясь собрать полный payload и найти в нём сигнатуры (hostname в HTTP, SNI в TLS). Если мы разрежем пакет на несколько TCP-сегментов, DPI может:
1. **Не собрать данные:** некоторые DPI работают попакетно и не реассемблируют TCP
2. **Не найти сигнатуру:** если разрез проходит через `Host:` или SNI, ни в одном отдельном сегменте полного hostname нет
3. **Принять фейк за реальные данные:** с помощью `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]].
---
## Быстрый старт
Минимально (дефолты: `pos=2` — первые 2 байта отдельным сегментом, `payload=known`, `dir=out`):
```bash
--lua-desync=multisplit
```
Типовой TLS-разрез:
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld
```
TLS с seqovl:
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1:seqovl=5:seqovl_pattern=0x1603030000
```
HTTP с разрезом по hostname:
```bash
--payload=http_req --lua-desync=multisplit:pos=host,midsld,endhost
```
---
## Откуда берутся данные для нарезки
Прежде чем что-то резать, `multisplit` должна выбрать, *какие именно* данные она режет. Это делает всего одна строка, но за ней стоит цепочка приоритетов:
```lua
local data = blob_or_def(desync, desync.arg.blob) or desync.reasm_data or desync.dis.payload
```
Lua-оператор `or` возвращает первый истинный (не `nil` и не `false`) операнд, поэтому строка читается как «сверху вниз, берём первое, что есть». Разберём три источника по порядку.
**Первый приоритет — явный blob.** Если в аргументах задан `blob=имя`, функция `blob_or_def` достаёт содержимое этого blob и режет именно его, полностью игнорируя реальный payload пакета. Это способ отправить *произвольные* данные вместо того, что реально шлёт приложение: заранее заготовленный фейковый ClientHello, модифицированный запрос, содержимое файла. Что такое blob и как их объявлять (inline-hex `0x...`, `@файл`, именованные переменные) — см. отдельную заметку [[blob]]. Если `blob=` не задан, `blob_or_def` возвращает `nil`, и `or` идёт дальше.
**Второй приоритет — реассемблированные данные.** Если blob нет, но движок собрал многопакетный payload в `desync.reasm_data` (например, TLS ClientHello, растянувшийся на несколько TCP-сегментов), режется этот собранный буфер целиком. Благодаря этому разрез по `midsld` попадёт в реальную середину доменного имени, даже если само имя физически лежало во втором или третьем пакете исходного потока. Механику сборки и однократной отправки reasm разбирает раздел [Поведение при replay / reasm](#поведение-при-replay--reasm).
**Третий приоритет — payload текущего пакета.** Если ни blob, ни reasm нет (обычный одиночный пакет, всё поместилось), берётся `desync.dis.payload` — тело именно этого перехваченного TCP-сегмента. Это дефолтный путь для большинства простых случаев.
**Почему порядок именно такой и что из этого следует.** Все дальнейшие аргументы — маркеры `pos`, длина `seqovl`, проверки — применяются к тем данным, которые реально выбраны на этом шаге, а не к «пакету вообще». Практическое следствие: если вы задали `blob=myblob`, то относительные маркеры вроде `midsld` или `host` сработают только при условии, что содержимое `myblob` — это валидный TLS- или HTTP-payload, который движок сумеет распознать (иначе `desync.l7payload` для blob окажется `unknown` и такие маркеры молча отвалятся, см. [Важные нюансы pos](#важные-нюансы-pos)).
> [!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` прибавляет единицу), но в командной строке считать нужно от нуля.
### Типы маркеров
| Тип | Описание | Пример |
|:----|:---------|:-------|
| **Абсолютный положительный** | Смещение от начала payload, считая с 0. `pos=N` → первые N байт становятся отдельным сегментом | `1`, `5`, `100` |
| **Абсолютный отрицательный** | Смещение от конца payload. `-1` = последний байт (он уйдёт отдельным сегментом) | `-1`, `-10`, `-50` |
| **Относительный** | Логическая позиция внутри распознанного payload. Привязана к структуре протокола | `midsld`, `host`, `sniext` |
### Относительные маркеры
| Маркер | Описание | Для каких payload |
|:-------|:---------|:------------------|
| `method` | Начало HTTP-метода (`GET`, `POST`, `HEAD`, `PUT` и т.д.). Обычно позиция 0, но может стать 1-2 при использовании `http_methodeol` | `http_req` |
| `host` | Первый байт имени хоста (`Host:` в HTTP, SNI в TLS) | `http_req`, `tls_client_hello` |
| `endhost` | Байт, **следующий** за последним байтом имени хоста. Т.е. `host..endhost-1` = полный hostname | `http_req`, `tls_client_hello` |
| `sld` | Первый байт домена второго уровня (SLD). Для `www.example.com` — это `e` в `example` | `http_req`, `tls_client_hello` |
| `endsld` | Байт, следующий за последним байтом SLD. Для `example.com` — это `.` после `example` | `http_req`, `tls_client_hello` |
| `midsld` | Середина SLD (самый популярный маркер). Для `example` (7 символов) — позиция 3-го или 4-го символа | `http_req`, `tls_client_hello` |
| `sniext` | Начало поля данных SNI extension в TLS ClientHello. 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 частей (если все маркеры успешно разрешатся и дадут различные позиции).
### Как маркеры разрешаются в коде
Внутри `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`
- **Реальный случай, когда маркер выпадает по этой причине — `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
---
## seqovl — скрытый фейк внутри сегмента
**seqovl** (Sequence Overlap) — техника скрытого замешивания фейковых данных в реальный TCP-сегмент через манипуляцию TCP sequence number. В `multisplit` 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. Байты с номерами 990999 (те самые 10 байт паттерна) оказываются левее границы окна — сервер отбрасывает их автоматически, как повтор/устаревшие данные. А байты, начиная с 1000 (реальная первая часть), попадают в окно и принимаются. В итоге сервер собирает ровно исходный поток, будто никакого паттерна и не было.
Наглядно расклад по одному sequence-байту такой:
| Диапазон seq | Содержимое | DPI | Сервер (TCP-стек) |
|:-------------|:-----------|:----|:------------------|
| 990999 | `PATTERN` (10 байт фейка) | анализирует вместе со всем сегментом | **отбрасывает** — левее границы окна |
| с 1000 | реальная первая часть | анализирует | **принимает** |
Ключевая идея: одни и те же 10 байт для DPI — «полноценные данные для анализа», а для сервера — «мусор за левым краем окна». Расхождение в трактовке и есть рабочий механизм обхода.
### Зачем seqovl лучше обычного fooling
| Критерий | Обычный 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`.
```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` отсутствует — используется нулевой паттерн (операция не отменяется).
**Важно в multisplit:** `seqovl` — только **число**, маркеры не поддерживаются (в отличие от `multidisorder` и `fakeddisorder`, где seqovl может быть маркером).
---
## Полный список аргументов
Формат вызова:
```
--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=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` уменьшается на N. Сервер отбросит левую часть, DPI — может не отбросить
- **Примеры:**
- `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-фейк
- `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` обычно нежелателен — оригинал ещё раз уйдёт, что создаст дублирование и может ухудшить обход
---
### B) Standard direction
| Параметр | Значения | По умолчанию |
|:---------|:---------|:-------------|
| `dir` | `in`, `out`, `any` | `out` |
Фильтр по направлению пакета. `multisplit` по умолчанию работает только с исходящими (`out`).
- `dir=out` — только исходящие (от клиента к серверу)
- `dir=in` — только входящие (от сервера к клиенту)
- `dir=any`оба направления
При первом вызове с указанным `dir` функция делает `direction_cutoff_opposite` — отсекает себя от противоположного направления.
---
### C) Standard payload
| Параметр | Значения | По умолчанию |
|:---------|:---------|:-------------|
| `payload` | список типов через запятую | `known` |
Фильтр по типу payload на уровне Lua. Это **дополнительный** фильтр к `--payload=...` на уровне профиля.
- `payload=known` — только распознанные протоколы (`http_req`, `tls_client_hello`, `quic_initial` и т.д.)
- `payload=all` — любой payload, включая `unknown`
- `payload=tls_client_hello,http_req` — конкретные типы
- `payload=~unknown` — инверсия: всё кроме unknown
**Важно:** лучше ставить `--payload=...` на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.
---
### D) Standard fooling
Модификации L3/L4 заголовков. В `multisplit` применяются **ко всем** отправляемым сегментам (в отличие от fakedsplit, где 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=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-сегментации).
---
### F) Standard ipfrag
IP-фрагментация **поверх** TCP-сегментации. Каждый TCP-сегмент дополнительно фрагментируется на уровне IP.
| Параметр | Описание | По умолчанию |
|:---------|:---------|:-------------|
| `ipfrag[=func]` | Включить IP-фрагментацию. Если без значения → `ipfrag2` | — |
| `ipfrag_disorder` | Отправить IP-фрагменты в обратном порядке | — |
| `ipfrag_pos_tcp=N` | Позиция фрагментации TCP (кратно 8) | `32` |
| `ipfrag_pos_udp=N` | Позиция фрагментации UDP (кратно 8). Для multisplit бесполезно — он только TCP | `8` |
| `ipfrag_next=N` | IPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы) | — |
---
### G) Standard reconstruct
| Параметр | Описание |
|:---------|:---------|
| `badsum` | Испортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет |
---
### H) Standard rawsend
| Параметр | Описание |
|:---------|:---------|
| `repeats=N` | Отправить каждый сегмент N раз (идентичные повторы) |
| `ifout=<iface>` | Интерфейс для отправки (по умолчанию определяется автоматически) |
| `fwmark=N` | Firewall mark (только Linux, nftables/iptables) |
---
## Порядок отправки сегментов
`multisplit` всегда отправляет сегменты **последовательно** — от первого к последнему (в порядке возрастания TCP sequence).
### Пример с 3 позициями разреза
Возьмём payload длиной 600 байт и три позиции разреза: `pos=100,300,450`. Они делят данные на четыре куска (099, 100299, 300449, 450599), и `multisplit` отправляет их четырьмя TCP-сегментами по возрастанию sequence number. Sequence каждого сегмента равен смещению его первого байта в исходном payload, поэтому сервер собирает поток обратно без пропусков:
| Сегмент | Байты payload | Длина | Смещение (seq) |
|:--------|:--------------|:------|:---------------|
| 1 | 099 | 100 | 0 |
| 2 | 100299 | 200 | 100 |
| 3 | 300449 | 150 | 300 |
| 4 | 450599 | 150 | 450 |
### Пример с seqovl=10
Тот же payload 600 байт, но `pos=100` (один разрез, два сегмента) и `seqovl=10`. К первому сегменту слева дописываются 10 байт паттерна, а его sequence сдвигается на те же 10 в минус — поэтому паттерн уходит за левую границу окна и сервер его отбрасывает, приняв только реальную часть с seq 0:
| Сегмент | Содержимое | Длина | Смещение (seq) | Что с ним на сервере |
|:--------|:-----------|:------|:---------------|:---------------------|
| 1 | 10 байт паттерна + байты 099 | 110 | 10 | паттерн отброшен, реальная часть принята |
| 2 | байты 100599 | 500 | 100 | принят целиком |
---
## Поведение при replay / reasm
Многопакетный запрос (например, большой TLS ClientHello с post-quantum ключами, не влезающий в один TCP-сегмент) `nfqws2` придерживает, собирает в буфер `reasm_data` и затем перепроигрывает через desync-функции. `multisplit` использует штатную развилку перепроигрывания из общего скелета desync-функций ([[жизненный цикл desync-функции]], стадия 6) без изменений: режет и отправляет **весь** собранный буфер на первой части, а остальные части дропает, потому что их содержимое уже ушло в составе нарезки.
Специфика именно `multisplit` тут одна: маркеры разрешаются по всему `reasm_data`, а не по отдельному пакету. Поэтому разрез по `midsld` попадёт в реальную середину домена, даже если имя хоста физически лежало во втором или третьем пакете исходного потока.
Если отправка сорвалась (`rawsend` вернул `false`), флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть — данные не теряются.
---
## Автосегментация по MSS
О размерах 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; механика описана в [[жизненный цикл desync-функции]]).
Ограничение не техническая недоработка, а следствие сути приёма — он опирается на два свойства, которые есть у 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" и ничего не сделает.
### 4. nodrop создаёт дублирование
С `nodrop` multisplit отправляет нарезанные сегменты И пропускает оригинальный пакет. Сервер получит данные дважды. Используйте `nodrop` только для отладки или когда это осознанно нужно.
### 5. seqovl=10000 не вызовет ошибку
В отличие от nfqws1, где большие значения seqovl вызывали ошибку, nfqws2 автоматически сегментирует по MSS. Большой seqovl просто создаст много под-сегментов.
### 6. Fooling применяется ко ВСЕМ сегментам
В `multisplit` все сегменты — а других, кроме реальных, функция и не шлёт — получают [fooling](#d-standard-fooling) целиком. У [[fakedsplit]] и [[fakeddisorder]] иначе: там fooling в полном объёме достаётся только фейкам, а к реальным частям применяется единственная опция `tcp_ts_up`.
Практическое следствие: портящие пакет опции в `multisplit` вредны. Если задать `tcp_ack=-66000`, **все** сегменты получат инвалидный ack, сервер их отбросит — и соединение просто не состоится. Осмысленны здесь лишь те опции, которые не мешают серверу принять данные: `tcp_ts_up`, политика `ip_id`, IPv6 extension headers. Подробнее про сами опции — в [[ts-and-fooling]].
### 7. Порядок инстансов важен
Если перед `multisplit` стоит инстанс, модифицирующий диссект (например `pktmod`), то `multisplit` порежет уже изменённые данные — порядок `--lua-desync` в профиле определяет порядок применения. Разбор того, как аргументы и инстансы выстраиваются в цепочку, — в [[последовательность аргументов]] и [[desync]].
Частое заблуждение: `VERDICT_DROP` якобы обрывает цепочку. Это не так. Движок проходит **все** инстансы профиля и агрегирует их вердикты (`VERDICT_MODIFY` перебивает `VERDICT_PASS`, `VERDICT_DROP` перебивает оба). Инстансы, стоящие после `multisplit`, будут вызваны как обычно и увидят тот же самый диссект; дроп означает лишь, что оригинальный пакет в итоге не будет отправлен. Оборвать выполнение может только явный вызов `execution_plan_cancel`, которого в `multisplit` нет.
---
## Отличия от других функций сегментации
| Аспект | `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 | Да | Да | **Нет** | **Нет** |
---
## Миграция с nfqws1
### Соответствие параметров
| 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` в инстансе |
### Пример полной миграции
```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
```
```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
```
---
## Практические примеры
### Минимальный (дефолт: 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 не увидит полный метод.
### 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).
### Произвольный blob вместо payload
```bash
--blob=mydata:@custom_payload.bin \
--lua-desync=multisplit:blob=mydata:pos=10,100,-20
```
Режет и отправляет произвольные данные из файла вместо реального payload.
### Защита от отсутствующего 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
```
Отправляет нарезанные сегменты И пропускает оригинальный пакет (для экспериментов).
### С TCP timestamp + IP ID
```bash
--payload=tls_client_hello --lua-desync=multisplit:pos=1:tcp_ts_up:ip_id=seq:ip_id_conn
```
### Повторы отправки
```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-уровне в обратном порядке.
### Комбинация: 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.
### Боевой пример для 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 части.
---
## Как это устроено внутри
Дальше идёт разбор кода функции. Для настройки обхода он не нужен — практическая часть закончилась выше. Читать имеет смысл, если вы правите 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` |
Из общего скелета полезно помнить две вещи, о которых спотыкаются чаще всего. Первая: работу функция делает **не через `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]] — разрез по одной позиции с подмешиванием фейковых сегментов
- [[tcpseg]] — отправка произвольного диапазона данных, ограниченного двумя маркерами
- [[oob]] — сегментация с urgent-байтом вместо разреза
- [[blob]] — как объявлять и передавать данные для аргументов `blob` и `seqovl_pattern`
- [[payload]] — распознавание типов протоколов, от которого зависит работа относительных маркеров
- [[ts-and-fooling]] — опции fooling и почему в `multisplit` большинство из них вредны
- [[последовательность аргументов]] — как выстраивается цепочка инстансов в профиле
- [[profile]] · [[preset]] — где `--lua-desync` живёт среди остальных настроек
---
> **Источники:** `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] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/desync/multisplit.md) · [весь репозиторий](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main).