todo/Zapret2/жизненный цикл desync-функции.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

490 lines
50 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-07-24
tags:
- zapret
- zapret2
- nfqws2
- lua
- lua-desync
- antidpi
- архитектура
aliases:
- жизненный цикл desync-функции
- общий скелет desync-функций
- конвейер desync-функции
- стадии desync-функции
---
# ♻️ Жизненный цикл desync-функции: общий скелет всех техник дурения
> [!info] О чём заметка
> Все функции дурения DPI в [[Zapret2/Zapret2|Zapret 2]] (`nfqws2`) — [[fake]], [[multisplit]], [[multidisorder]], [[fakedsplit]] и остальные — написаны по одному и тому же шаблону: одинаковые проверки в начале, одинаковый выбор данных, одинаковая работа с многопакетными запросами и одинаковый набор вердиктов в конце. Здесь этот общий скелет разобран один раз и целиком, стадия за стадией. Заметки отдельных функций опираются на него и описывают только то, чем конкретная техника от скелета отличается. Устройство входной таблицы `desync` и диссекта пакета — в [[структура desync и диссекта]], каталог самих функций и синтаксис флага — в [[desync]].
## TL;DR
- Функция дурения — это Lua-функция вида `function имя(ctx, desync)`, которую C-ядро `nfqws2` вызывает на каждом перехваченном пакете, попавшем в её инстанс. Тело почти любой такой функции проходит **восемь стадий** в фиксированном порядке.
- Стадии 13 — отсев: не тот транспорт (TCP/UDP), не то направление, нет обязательного или необязательного blob. Здесь же функция может навсегда отключиться от потока через `instance_cutoff`, чтобы не тратить CPU впустую.
- Стадия 4 — выбор данных по цепочке `blob → reasm → payload текущего пакета`. Всё, что функция делает дальше, применяется к тому источнику, который выиграл этот выбор.
- Стадии 56 — гварды (`#data>0`, направление, тип протокола) и развилка перепроигрывания: многопакетный запрос обрабатывается один раз на первой части, остальные части дропаются.
- Стадия 7 — собственно техника: разрешение маркеров, формирование частей и фейков, отправка сырых пакетов помимо очереди.
- Стадия 8 — вердикт перехваченному пакету: `VERDICT_DROP`, `VERDICT_PASS`, `VERDICT_MODIFY` или ничего. Вердикты всех инстансов профиля **агрегируются**, а не обрывают цепочку.
- Отличия между функциями сводятся к трём вещам: что они отсеивают на стадии 1, что делают на стадии 7 и каким способом выпускают пакеты в сеть. Всё остальное у них общее.
---
## Оглавление
- [Почему у всех функций одинаковый скелет](#почему-у-всех-функций-одинаковый-скелет)
- [Восемь стадий целиком](#восемь-стадий-целиком)
- [Стадия 1. Отсев чужого транспорта и instance cutoff](#стадия-1-отсев-чужого-транспорта-и-instance-cutoff)
- [Стадия 2. Отсечение противоположного направления](#стадия-2-отсечение-противоположного-направления)
- [Стадия 3. Ранняя проверка аргументов](#стадия-3-ранняя-проверка-аргументов)
- [Стадия 4. Выбор данных: blob → reasm → payload](#стадия-4-выбор-данных-blob--reasm--payload)
- [Стадия 5. Три гварда](#стадия-5-три-гварда)
- [Стадия 6. Развилка перепроигрывания (replay)](#стадия-6-развилка-перепроигрывания-replay)
- [Стадия 7. Собственно техника](#стадия-7-собственно-техника)
- [Стадия 8. Вердикт и его агрегация](#стадия-8-вердикт-и-его-агрегация)
- [Четыре способа выпустить пакет](#четыре-способа-выпустить-пакет)
- [Наборы опций: кто их применяет и к чему](#наборы-опций-кто-их-применяет-и-к-чему)
- [Три типа функций](#три-типа-функций)
- [Карта отличий по всем функциям](#карта-отличий-по-всем-функциям)
- [Как читать скелет в логах](#как-читать-скелет-в-логах)
- [📚 См. также](#-см-также)
---
## Почему у всех функций одинаковый скелет
Функции дурения живут в `lua/zapret-antidpi.lua` и представляют собой обычные глобальные функции Lua. Никакого фреймворка, базового класса или обязательного интерфейса у них нет: C-ядро просто ищет глобальную функцию с указанным в `--lua-desync` именем и вызывает её с двумя аргументами. Формально функция может делать что угодно.
Одинаковыми они получились по другой причине — все они решают одну и ту же организационную задачу перед тем, как заняться своей техникой. Каждой нужно: убедиться, что пакет вообще относится к её транспорту; не тратить процессор на направление, которое ей не интересно; понять, какие данные считать «запросом»; не сработать дважды на многопакетном запросе; и корректно сообщить ядру, что делать с оригинальным пакетом. Ответы на эти вопросы вынесены в общие хелперы `lua/zapret-lib.lua` (`direction_check`, `payload_check`, `replay_first`, `blob_or_def`, `rawsend_payload_segmented` и другие), и функции просто вызывают их в одном и том же порядке.
Практическая польза от знания скелета такая. Когда техника «не работает», отказ почти всегда происходит не в самой технике, а на одной из ранних стадий: пакет не того транспорта, инстанс отключился от потока, payload не прошёл фильтр, маркер не разрешился. Зная стадии, вы читаете отладочный лог не как поток сообщений, а как маршрут: видно, до какой стадии функция дошла и где остановилась.
> [!note] Проще говоря
> Скелет — это не требование движка, а сложившаяся общая часть. Все функции сперва делают одну и ту же «бухгалтерию» (мой ли это пакет, мои ли данные, не обрабатывал ли я это уже), и лишь потом занимаются своим приёмом. Поэтому осваивать zapret2 удобнее один раз по скелету, а дальше читать только различия.
---
## Восемь стадий целиком
Общая форма тела функции выглядит так. Это не абстракция, а буквальный порядок вызовов, который вы увидите почти в любой функции файла `zapret-antidpi.lua`:
```lua
function имя_функции(ctx, desync)
-- 1. мой ли это транспорт? если нет — отключиться от потока навсегда
if not desync.dis.tcp then
if not desync.dis.icmp then instance_cutoff_shim(ctx, desync) end
return
end
-- 2. отключиться от противоположного направления
direction_cutoff_opposite(ctx, desync)
-- 3. ранняя проверка аргументов (обязательные / optional)
if desync.arg.optional and desync.arg.blob and not blob_exist(desync, desync.arg.blob) then return end
-- 4. выбор данных
local data = blob_or_def(desync, desync.arg.blob) or desync.reasm_data or desync.dis.payload
-- 5. три гварда
if #data>0 and direction_check(desync) and payload_check(desync) then
-- 6. развилка перепроигрывания
if replay_first(desync) then
-- 7. собственно техника: маркеры, части, фейки, отправка
...
replay_drop_set(desync)
-- 8. вердикт
return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP
end
if replay_drop(desync) then
return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP
end
end
end
```
Функции отличаются тем, какие стадии они пропускают и что делают на седьмой. Например, [[fake]] не отсекает не-TCP (он умеет и UDP), [[oob]] вместо направления смотрит позицию пакета в соединении, а `drop` состоит из стадий 2, 5 и 8 и больше ни из чего. Полная карта отличий — в разделе [Карта отличий по всем функциям](#карта-отличий-по-всем-функциям).
---
## Стадия 1. Отсев чужого транспорта и instance cutoff
Первое, что делает функция — проверяет, её ли это протокол. Техники TCP-сегментации бессмысленны для UDP, `udplen` бессмысленна для TCP, а SYN-техники интересны только на этапе рукопожатия.
```lua
if not desync.dis.tcp then
-- do not cutoff on related icmp
if not desync.dis.icmp then instance_cutoff_shim(ctx, desync) end
return
end
```
Важна не сама проверка, а то, что происходит при несовпадении: вызывается `instance_cutoff_shim`, который через C-функцию `instance_cutoff` **отключает этот инстанс функции от этого потока навсегда**. Дальше движок не будет тратить время на вызов Lua для пакетов данного соединения — он уже знает, что функция всё равно ничего не сделает. На нагруженном соединении это заметная экономия: Lua-вызов на каждый пакет стоит дорого.
Отдельно оговаривается ICMP. Связанный ICMP-пакет (например «destination unreachable», внутри которого лежит копия исходного заголовка) относится к тому же потоку, но не является ни TCP, ни UDP. Отключаться из-за него нельзя — соединение живое, и следующий обычный пакет функции ещё понадобится. Поэтому проверка `if not desync.dis.icmp`.
Вариации по функциям:
- **TCP-техники** ([[multisplit]], [[multidisorder]], [[fakedsplit]], [[fakeddisorder]], [[hostfakesplit]], [[tcpseg]], `rst`, HTTP-модификаторы) проверяют `desync.dis.tcp`.
- **UDP-техники** (`udplen`, `dht_dn`) проверяют `desync.dis.udp` — код тот же с точностью до поля.
- **[[fake]]** работает с обоими транспортами и потому не отключается вовсе: у него просто условие `if (desync.dis.tcp or desync.dis.udp) and ...`.
- **Техники этапа рукопожатия** (`synack`, `synack_split`, [[syndata]], `wsize`) проверяют не только транспорт, но и флаги TCP. Когда нужная фаза прошла, они делают cutoff с комментарием «mission complete» — их работа на этом соединении закончена навсегда:
```lua
if bitand(desync.dis.tcp.th_flags, TH_SYN + TH_ACK)==TH_SYN then
... -- работаем
else
instance_cutoff_shim(ctx, desync) -- mission complete
end
```
- **[[oob]]** дополнительно требует наличия `desync.track` (без conntrack он не работает) и требует, чтобы первый увиденный пакет был SYN — иначе тоже отключается.
- **`wssize`** делает cutoff не по транспорту, а по событию: как только в соединении прошёл непустой payload нужного типа, «forced cutoff».
---
## Стадия 2. Отсечение противоположного направления
```lua
direction_cutoff_opposite(ctx, desync)
```
Почти всем функциям интересны только исходящие пакеты — от клиента к серверу. Это направление по умолчанию (`dir=out`), и `direction_cutoff_opposite` при первом же вызове отключает инстанс от входящего направления. Механика та же, что на первой стадии: движок перестаёт дёргать Lua там, где это заведомо бесполезно.
Функция читает аргумент `dir` и работает так:
| `dir` | Что отключается |
|:------|:----------------|
| `out` (по умолчанию) | входящее направление |
| `in` | исходящее направление |
| `any` | ничего не отключается |
Значение по умолчанию у функций разное. У большинства техник дурения это `out`, а у служебных `drop`, `send` и `pktmod` — `any`, потому что они осмысленны в обе стороны:
```lua
direction_cutoff_opposite(ctx, desync, "any")
```
Обратите внимание: отключение и проверка — разные вещи, разнесённые по разным стадиям. Здесь инстанс лишь перестаёт получать пакеты не своего направления; фактическая проверка текущего пакета (`direction_check`) произойдёт позже, на стадии 5. Так сделано потому, что cutoff — это оптимизация на будущее, а гвард — решение по текущему пакету.
---
## Стадия 3. Ранняя проверка аргументов
До того, как трогать данные, функция проверяет, что её вообще корректно вызвали. Здесь два разных механизма с принципиально разным поведением.
**Обязательные аргументы — `error()`.** Если без аргумента техника не имеет смысла, функция бросает ошибку Lua. [[fake]] требует `blob` (без фейкового содержимого фейк невозможен), [[tcpseg]] требует `pos` (без диапазона нечего отправлять), `tls_client_hello_clone` требует `blob`, куда класть клон:
```lua
if not desync.arg.blob then
error("fake: 'blob' arg required")
end
```
Ошибка Lua не роняет `nfqws2`: C-ядро ловит её через `lua_pcall`, пишет в лог и прекращает обработку пакета. Но профиль при этом фактически не работает, поэтому такие ошибки надо ловить при первом же запуске с включённым отладочным выводом (см. [[log-analyzer]]).
**Мягкий режим — `optional`.** Если blob задан, но его нет, поведение зависит от флага `optional`. Без флага дальнейший вызов `blob()` бросит ошибку; с флагом функция тихо выходит, ничего не сделав:
```lua
if desync.arg.optional and desync.arg.blob and not blob_exist(desync, desync.arg.blob) then
DLOG("multisplit: blob '"..desync.arg.blob.."' not found. skipped")
return
end
```
Это нужно не столько от опечаток, сколько для blob, которые создаются **другой функцией того же профиля**. Классический пример — `tls_client_hello_clone` кладёт клонированный ClientHello в переменную `desync`, а стоящий следом [[fake]] его использует. Если клонировать не удалось (пакет оказался не TLS), фейк с `optional` просто пропустит ход вместо того, чтобы сломать обработку. Подробнее про источники и синтаксис blob — в [[blob]].
У `optional` есть второе, менее очевидное значение: для аргумента `seqovl_pattern` он означает не отказ от операции, а замену отсутствующего blob нулевым паттерном. То есть один флаг управляет двумя разными реакциями в зависимости от того, чего именно не хватает.
---
## Стадия 4. Выбор данных: blob → reasm → payload
Одна строка, определяющая всю дальнейшую работу:
```lua
local data = blob_or_def(desync, desync.arg.blob) or desync.reasm_data or desync.dis.payload
```
Оператор `or` в Lua возвращает первый не-`nil` операнд, поэтому строка читается сверху вниз как «берём первое, что есть».
**Первый приоритет — явный blob.** Если задан `blob=имя`, функция работает с его содержимым и полностью игнорирует реальный payload пакета. Так отправляют произвольные данные: заготовленный фейковый ClientHello, модифицированный запрос, содержимое файла.
**Второй приоритет — реассемблированные данные.** Если прикладные данные не поместились в один TCP-сегмент (типичный случай — большой TLS ClientHello с post-quantum ключами), движок собирает все части в единый буфер `desync.reasm_data`. Работа идёт с ним целиком, поэтому маркер вроде `midsld` попадает в реальную середину домена, даже если само имя лежало во втором пакете.
**Третий приоритет — payload текущего пакета.** Обычный одиночный пакет: берётся `desync.dis.payload`, тело именно этого TCP-сегмента.
Практическое следствие, о котором легко забыть: **маркеры позиций разрешаются по выбранным данным, а не «по пакету»**. Если вы подсунули свой blob, относительные маркеры (`midsld`, `host`, `sniext`) сработают только при условии, что содержимое blob — валидный HTTP- или TLS-payload, который движок распознает. Иначе тип окажется `unknown`, и такие маркеры молча отвалятся.
Исключения из общего правила:
- **[[fake]]** цепочку не использует: он всегда шлёт содержимое своего `blob`, а `reasm_data` ему нужен лишь как образец для `tls_mod` (скопировать session id из настоящего ClientHello).
- **[[multidisorder_legacy]]** намеренно работает иначе: `data = desync.dis.payload`, а `reasm_data` используется отдельно, только для разрешения маркеров. Это и есть причина, по которой legacy-вариант сохраняет исходную сегментацию — он режет каждую часть reasm по отдельности, как это делал nfqws1.
- **[[oob]]** берёт `desync.reasm_data or desync.dis.payload` без поддержки blob.
- **HTTP-модификаторы** (`http_domcase`, `http_hostcase`, `http_methodeol`, `http_unixeol`) работают строго с `desync.dis.payload`, потому что они меняют текущий пакет на месте, а не отправляют новый.
---
## Стадия 5. Три гварда
```lua
if #data>0 and direction_check(desync) and payload_check(desync) then
```
Три условия проверяются подряд и решают, работать ли с этим конкретным пакетом.
**`#data>0` — есть ли что обрабатывать.** Пустой payload бывает у чистых ACK, у которых нет прикладных данных. Резать или подменять там нечего.
**`direction_check(desync)` — то ли направление.** Здесь читается тот же аргумент `dir`, что и на стадии 2, но теперь проверяется текущий пакет: `desync.outgoing` сопоставляется с `dir`. Проверка нужна отдельно от cutoff, потому что cutoff может ещё не сработать (например, при отсутствии conntrack), а пропускать чужое направление всё равно нельзя.
**`payload_check(desync)` — тот ли протокол.** Фильтр по типу прикладного протокола, распознанного C-ядром. Значение по умолчанию для большинства техник — `known`, то есть «любой распознанный протокол, кроме `unknown` и `empty`». Это ровно то поведение, которое в nfqws1 включалось отсутствием флага `--dpi-desync-any-protocol`.
Синтаксис фильтра:
| Запись | Что означает |
|:-------|:-------------|
| `payload=known` | любой распознанный протокол (по умолчанию) |
| `payload=all` | любой payload, включая нераспознанный |
| `payload=tls_client_hello,http_req` | перечисленные типы |
| `payload=~unknown` | инверсия: всё, кроме указанного |
Тот же фильтр можно задать флагом профиля `--payload=...`. Разница в том, что флаг профиля обрабатывается в C-коде до вызова Lua и потому дешевле; Lua-аргумент `payload` работает как дополнительное уточнение внутри инстанса. Про распознавание типов — в [[payload]], про фильтры уровня профиля — в [[filter]].
---
## Стадия 6. Развилка перепроигрывания (replay)
Самая неочевидная стадия, и именно она чаще всего вызывает вопросы «почему функция сработала один раз, а пакетов было три».
Когда прикладной запрос не помещается в один пакет, `nfqws2` его **задерживает**: он придерживает части, собирает их в `reasm_data`, а затем «перепроигрывает» (replay) через desync-функции. Механика сборки описана в [[структура desync и диссекта]]; здесь важно её следствие для скелета. При перепроигрывании функция вызывается на каждой части, но осмысленно сработать должна ровно один раз — иначе сервер получит данные несколько раз.
Отсюда пара хелперов:
```lua
if replay_first(desync) then
... -- работаем: reasm_data уже собран целиком
replay_drop_set(desync)
return ...
else
DLOG("не действуем на последующих частях replay")
end
if replay_drop(desync) then
return ... -- дропаем остальные части: их содержимое уже отправлено
end
```
- **`replay_first(desync)`** — истина, если это не перепроигрывание вообще либо это его первая часть. Реализация буквально в одну строку: `return not desync.replay or desync.replay_piece==1`.
- **`replay_drop_set(desync)`** — ставит в состоянии соединения (`track.lua_state`) флаг «реасм уже отправлен этим инстансом». Флаг ставится только при успешной отправке.
- **`replay_drop(desync)`** — на последующих частях проверяет флаг и говорит, надо ли вернуть `VERDICT_DROP`. Заодно сам сбрасывает флаг на последней части, чтобы состояние не протекло на следующий запрос в том же соединении.
Логика целиком: первая часть берёт весь собранный `reasm_data`, обрабатывает и отправляет; остальные части дропаются, потому что их содержимое уже ушло в составе обработанного реасма. Если отправка не удалась, флаг не ставится — и оставшиеся части пройдут как есть, чтобы данные не пропали совсем.
Отдельный нюанс: `replay_drop_set` работает только при наличии conntrack (`desync.track`). Без отслеживания соединений хранить флаг негде, и защита от повторной отправки не работает.
> [!warning] Не путайте replay с retransmit
> «Перепроигрывание» здесь — внутренний механизм `nfqws2`, повторная подача уже задержанных им же пакетов в цепочку desync-функций. Это не ретрансмиссия TCP и не повтор со стороны приложения. Со стороны сети никакого дублирования не видно: части, которые функция дропает, наружу не выходят.
---
## Стадия 7. Собственно техника
Единственная стадия, которая у всех функций разная — здесь живёт то, ради чего функция написана. Тем не менее и она собрана из повторяющихся кирпичей:
**Разрешение маркеров в позиции.** `resolve_pos` (один маркер), `resolve_multi_pos` (список), `resolve_range` (диапазон из ровно двух маркеров). Все три переводят логические маркеры вроде `midsld` в конкретные смещения по выбранным на стадии 4 данным. Маркеры считаются от нуля, а возвращаются как 1-based индексы Lua; неразрешимые маркеры молча выпадают из списка.
**Формирование частей.** `string.sub` по разрешённым позициям — так получаются реальные сегменты. Для фейков используется `pattern(pat, offset, len)`, размножающая паттерн до нужной длины.
**Приклеивание seqovl.** У сегментирующих функций — дописать паттерн слева и увести sequence в минус.
**Отправка.** Один из четырёх способов, описанных ниже.
**Порядок отправки.** Прямой у `*split`, обратный у `*disorder`, с вкраплением фейков у `faked*` и `hostfakesplit`.
Что именно делает конкретная функция на этой стадии — тема её собственной заметки: [[multisplit]], [[multidisorder]], [[multidisorder_legacy]], [[fakedsplit]], [[fakeddisorder]], [[hostfakesplit]], [[tcpseg]], [[fake]], [[syndata]], [[oob]].
---
## Стадия 8. Вердикт и его агрегация
Функция возвращает ядру одно из четырёх решений по перехваченному пакету:
| Вердикт | Что делает ядро |
|:--------|:----------------|
| `VERDICT_PASS` | пропустить пакет как есть, изменения диссекта игнорируются |
| `VERDICT_MODIFY` | собрать пакет заново из изменённого диссекта и отправить его |
| `VERDICT_DROP` | выбросить пакет |
| ничего (`nil`) | приравнивается к `VERDICT_PASS` |
Плюс отдельный бит `VERDICT_PRESERVE_NEXT`, который прибавляется к основному вердикту и означает «не пересчитывать поля next protocol в заголовках IPv6, использовать значения из диссекта». Он нужен при ручном крафтинге цепочки extension headers.
Типичная концовка сегментирующей функции выглядит так:
```lua
replay_drop_set(desync)
return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP
```
Смысл: раз данные уже отправлены отдельными сырыми пакетами, оригинал надо выбросить, иначе сервер получит их дважды. Аргумент `nodrop` отменяет это поведение — он полезен при отладке, но в боевом профиле обычно вреден именно из-за дублирования.
**Агрегация — важнейшая часть, которую легко понять неправильно.** Дроп не обрывает выполнение: движок проходит **все** `--lua-desync`-инстансы профиля по очереди и складывает их вердикты по правилу «`VERDICT_MODIFY` перебивает `VERDICT_PASS`, `VERDICT_DROP` перебивает оба». Инстансы, стоящие после сработавшего дропа, будут вызваны как обычно и увидят тот же диссект.
```
инстанс 1: fake -> nil (PASS)
инстанс 2: multisplit -> VERDICT_DROP <- пакет уже обречён,
инстанс 3: pktmod -> VERDICT_MODIFY но инстанс 3 всё равно вызывается
итог: DROP
```
Единственный способ прервать цепочку — явный вызов `execution_plan_cancel`, которого в штатных функциях дурения нет. Порядок инстансов при этом полностью определяется порядком флагов в командной строке, см. [[последовательность аргументов]].
---
## Четыре способа выпустить пакет
Функция может воздействовать на трафик четырьмя разными способами, и выбор способа — одно из главных отличий между техниками.
**1. Изменить текущий диссект и вернуть `VERDICT_MODIFY`.** Функция ничего не отправляет сама: она правит поля `desync.dis` (payload, флаги, заголовки), а ядро собирает из диссекта пакет и отправляет вместо оригинала. Так работают все HTTP-модификаторы, `udplen`, `dht_dn`, `wsize`, `wssize`, `pktmod`. Ограничение: пакет не может вырасти в размере, потому что используется тот же буфер.
**2. `rawsend_dissect_ipfrag(dis, opts)` — отправить готовый диссект.** Функция делает копию диссекта, правит её как нужно и выпускает в сеть сырым пакетом помимо очереди. Так работают [[fake]] на SYN-пакетах, `rst`, `send`, [[syndata]], `synack`, `synack_split`. Сегментация по MSS здесь не производится — отправляется ровно то, что собрали.
**3. `rawsend_payload_segmented(desync, payload, seq, opts)` — подменить payload у копии текущего диссекта.** Самый ходовой способ у техник сегментации. Функция передаёт кусок данных и сдвиг sequence number, а хелпер сам делает копию диссекта, подставляет payload, правит `th_seq` и отправляет. Если кусок не влезает в MSS, он автоматически дорезается на несколько сегментов с корректными sequence.
**4. `rawsend_dissect_segmented(desync, dis, mss, opts)` — то же для произвольного диссекта.** Нижний уровень предыдущего способа; используется, когда нужно отправить не просто другой payload, а диссект с изменёнными заголовками — например [[oob]] выставляет флаг URG и указатель `th_urp`.
Про автосегментацию стоит помнить одну деталь, которая влияет на результат: [fooling](#наборы-опций-кто-их-применяет-и-к-чему) применяется **один раз к исходному диссекту**, то есть все под-сегменты получают его одинаково, а политика `ip_id` применяется **к каждому под-сегменту отдельно**.
---
## Наборы опций: кто их применяет и к чему
Стандартные наборы аргументов (`fooling`, `ip_id`, `ipfrag`, `reconstruct`, `rawsend`) описаны в общем виде в [[desync]] и подробно в [[ts-and-fooling]]. Со стороны скелета важно другое — как функция решает, к каким именно пакетам их применить.
Большинство функций собирают один общий набор:
```lua
desync_opts(desync) -- rawsend + reconstruct + ipfrag + ipid + fooling, всё из desync.arg
```
и применяют его ко всему, что отправляют. Так устроены [[multisplit]], [[multidisorder]], [[tcpseg]], `rst`, `send`, [[fake]].
Функции с фейковыми сегментами собирают **два разных набора** — и это принципиально:
```lua
-- к оригиналам: только tcp_ts_up из fooling, без reconstruct и ipfrag, но с ip_id
local opts_orig = {rawsend = rawsend_opts_base(desync), reconstruct = {}, ipfrag = {}, ipid = desync.arg, fooling = {tcp_ts_up = desync.arg.tcp_ts_up}}
-- к фейкам: всё в полном объёме
local opts_fake = {rawsend = rawsend_opts(desync), reconstruct = reconstruct_opts(desync), ipfrag = {}, ipid = desync.arg, fooling = desync.arg}
```
Логика очевидна, если вспомнить назначение фейков: фейк **должен** быть отвергнут сервером, поэтому ему нужны портящие пакет опции (невалидный ack, badsum, малый TTL). Реальные части, наоборот, сервер обязан принять — им ничего портить нельзя. Единственное исключение `tcp_ts_up` пропускается к оригиналам потому, что он не портит пакет, а лишь двигает TCP-опцию timestamp в начало заголовка.
Так устроены [[fakedsplit]], [[fakeddisorder]] и [[hostfakesplit]]. Отсюда же следует практическое правило: у чисто сегментирующих функций ([[multisplit]], [[multidisorder]]) портящие fooling-опции **вредны** — они достанутся реальным данным, и сервер их отбросит.
Ещё одна деталь: `rawsend_opts_base` отличается от `rawsend_opts` отсутствием `repeats`. Повторы отправляются только для фейков — дублировать реальные сегменты смысла нет.
---
## Три типа функций
Если смотреть на скелет целиком, все функции проекта делятся на три группы по способу воздействия.
**Модификаторы** правят текущий пакет и возвращают `VERDICT_MODIFY`. Ничего не отправляют, состояние соединения обычно не трогают: `http_domcase`, `http_hostcase`, `http_methodeol`, `http_unixeol`, `udplen`, `dht_dn`, `wsize`, `wssize`, `pktmod`, а также входящая ветка [[oob]].
**Отправители** выпускают в сеть один или несколько сырых пакетов и решают судьбу оригинала: [[fake]], [[multisplit]], [[multidisorder]], [[multidisorder_legacy]], [[fakedsplit]], [[fakeddisorder]], [[hostfakesplit]], [[tcpseg]], [[syndata]], `rst`, `send`, `synack`, `synack_split`. Именно у них скелет проявляется полностью — со всеми восемью стадиями.
**Фильтры и служебные** не отправляют и не модифицируют, а только выносят вердикт или готовят данные для других инстансов: `drop` (вердикт `VERDICT_DROP` по фильтру) и `tls_client_hello_clone` (кладёт клон ClientHello в переменную `desync` для последующего [[fake]]).
Деление помогает при отладке: если функция-модификатор «не сработала», ищите причину в фильтрах и распознавании протокола; если не сработал отправитель — смотрите ещё и на маркеры, conntrack и replay.
---
## Карта отличий по всем функциям
Таблица показывает, чем каждая функция отклоняется от общего скелета. Колонка «Данные» — источник на стадии 4, «replay» — использует ли функция развилку перепроигрывания, «Отправка» — способ из раздела [Четыре способа выпустить пакет](#четыре-способа-выпустить-пакет).
### Техники сегментации TCP
| Функция | Стадия 1 отсекает | `dir` | Данные | replay | Отправка | Вердикт |
|:--------|:------------------|:------|:-------|:-------|:---------|:--------|
| [[multisplit]] | не-TCP | out | blob → reasm → payload | да | payload_segmented | DROP |
| [[multidisorder]] | не-TCP | out | blob → reasm → payload | да | payload_segmented | DROP |
| [[multidisorder_legacy]] | не-TCP | out | payload (reasm — только для маркеров) | нет | payload_segmented | DROP |
| [[tcpseg]] | не-TCP | out | blob → reasm → payload | да | payload_segmented | нет (`nil`) |
| [[oob]] | не-TCP, отсутствие conntrack, старт не с SYN | — (по позиции в потоке) | reasm → payload | особый | dissect_segmented | DROP / MODIFY |
### Техники с фейковыми сегментами
| Функция | Стадия 1 отсекает | `dir` | Данные | replay | Отправка | Вердикт |
|:--------|:------------------|:------|:-------|:-------|:---------|:--------|
| [[fakedsplit]] | не-TCP | out | blob → reasm → payload | да | payload_segmented, два набора опций | DROP |
| [[fakeddisorder]] | не-TCP | out | blob → reasm → payload | да | payload_segmented, два набора опций | DROP |
| [[hostfakesplit]] | не-TCP | out | blob → reasm → payload | да | payload_segmented, два набора опций | DROP |
| [[fake]] | ни TCP, ни UDP (без cutoff) | out | только свой blob | да | payload_segmented | нет (`nil`) |
### Этап рукопожатия
| Функция | Стадия 1 отсекает | `dir` | Данные | replay | Отправка | Вердикт |
|:--------|:------------------|:------|:-------|:-------|:---------|:--------|
| [[syndata]] | не-TCP, не SYN («mission complete») | — | blob (по умолчанию 16 нулей) | нет | dissect_ipfrag | DROP |
| `synack` | не-TCP, не SYN | — | — | нет | dissect_ipfrag | нет |
| `synack_split` | не-TCP, не SYN+ACK | — | — | нет | dissect_ipfrag | DROP |
| `wsize` | не-TCP, не SYN+ACK | — | — | нет | — | MODIFY |
| `wssize` | не-TCP; форс-cutoff после непустого payload | out | — | нет | — | MODIFY |
### Модификаторы и служебные
| Функция | Стадия 1 отсекает | `dir` | Данные | replay | Отправка | Вердикт |
|:--------|:------------------|:------|:-------|:-------|:---------|:--------|
| `http_domcase` · `http_hostcase` · `http_methodeol` · `http_unixeol` | не-TCP, не `http_req` | out | payload | нет | — | MODIFY |
| `udplen` | не-UDP | out | payload | нет | — | MODIFY |
| `dht_dn` | не-UDP, не `dht` | out | payload | нет | — | MODIFY |
| `pktmod` | — | any | payload | нет | — | MODIFY |
| `send` | — | any | текущий диссект | нет | dissect_ipfrag | нет |
| `drop` | — | any | — | нет | — | DROP |
| `rst` | не-TCP | any при проверке, но инстанс отключается от входящего | — (пустой payload) | да | dissect_ipfrag | нет |
| `tls_client_hello_clone` | не-TCP | out | reasm → payload | нет | — | нет |
---
## Как читать скелет в логах
Отладочный вывод (`--debug`, разбор которого есть в [[log-analyzer]]) построен так, что по нему видно, на какой стадии остановилась функция. Сообщения общего скелета одинаковы для всех техник — меняется только префикс с именем инстанса:
| Сообщение | Стадия | Что произошло |
|:----------|:-------|:--------------|
| `voluntary cutoff` | 12 | инстанс сам отключился от потока и больше не вызывается |
| `pos … is beyond range` | до 1 | инстанс не вызван из-за внутрипрофильного фильтра `--out-range` |
| `payload_type '…' does not satisfy filter` | 5 | тип протокола не прошёл фильтр `payload` |
| `blob '…' not found. skipped` | 3 | сработал мягкий режим `optional` |
| `not acting on further replay pieces` | 6 | это не первая часть многопакетного запроса |
| `dropping replay packet because reasm was already sent` | 6 | часть дропнута, реасм уже отправлен нарезанным |
| `no valid split positions` | 7 | ни один маркер не разрешился (специфично для сегментации) |
| `desync` | 7 | функция реально вызвана и начала работу |
Практический порядок разбора «почему техника не сработала»: сначала убедиться, что вообще есть строка `desync` для нужного инстанса (иначе проблема в фильтрах или cutoff), затем искать сообщения про payload и replay, и только потом разбираться с маркерами и содержимым пакетов.
---
## 📚 См. также
- [[desync]] — каталог всех функций `--lua-desync`, синтаксис флага и соответствие флагам nfqws1
- [[структура desync и диссекта]] — прототип функции, поля таблицы `desync`, устройство диссекта и механика reasm
- [[схема обработки трафика]] — где вызов Lua-функций стоит в общем конвейере обработки пакета
- [[последовательность аргументов]] — как порядок флагов превращается в порядок инстансов
- [[payload]] — распознавание типов протоколов, от которого зависит гвард стадии 5
- [[blob]] — источники данных для аргументов `blob`, `pattern`, `seqovl_pattern`
- [[ts-and-fooling]] — что делают опции fooling и почему их нельзя лить на реальные сегменты
- [[filter]] — фильтры уровня профиля, отсекающие пакеты до вызова Lua
- [[log-analyzer]] — чтение отладочного лога `nfqws2`
- [[multisplit]] · [[multidisorder]] · [[multidisorder_legacy]] · [[tcpseg]] · [[oob]] — техники сегментации
- [[fake]] · [[fakedsplit]] · [[fakeddisorder]] · [[hostfakesplit]] · [[syndata]] — техники с фейками
---
> **Источники:** `lua/zapret-antidpi.lua` (тела всех desync-функций), `lua/zapret-lib.lua` (`instance_cutoff_shim`, `direction_check`, `direction_cutoff_opposite`, `payload_check`, `blob_or_def`, `replay_first`/`replay_drop`/`replay_drop_set`, `desync_opts`, `rawsend_opts`/`rawsend_opts_base`, `rawsend_payload_segmented`, `rawsend_dissect_segmented`), `nfq2/desync.c` (цикл вызова инстансов и агрегация вердиктов), `nfq2/lua.c` (`execution_plan_cancel`, `resolve_pos`/`resolve_multi_pos`), `docs/manual.md` из репозитория zapret2.
---
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/%D0%B6%D0%B8%D0%B7%D0%BD%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9%20%D1%86%D0%B8%D0%BA%D0%BB%20desync-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8.md) · [весь репозиторий](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main).