---
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` вызывает на каждом перехваченном пакете, попавшем в её инстанс. Тело почти любой такой функции проходит **восемь стадий** в фиксированном порядке.
- Стадии 1–3 — отсев: не тот транспорт (TCP/UDP), не то направление, нет обязательного или необязательного blob. Здесь же функция может навсегда отключиться от потока через `instance_cutoff`, чтобы не тратить CPU впустую.
- Стадия 4 — выбор данных по цепочке `blob → reasm → payload текущего пакета`. Всё, что функция делает дальше, применяется к тому источнику, который выиграл этот выбор.
- Стадии 5–6 — гварды (`#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` | 1–2 | инстанс сам отключился от потока и больше не вызывается |
| `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).
