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

50 KiB
Raw Permalink Blame History

date tags aliases
2026-07-24
zapret
zapret2
nfqws2
lua
lua-desync
antidpi
архитектура
жизненный цикл desync-функции
общий скелет desync-функций
конвейер desync-функции
стадии desync-функции

♻️ Жизненный цикл desync-функции: общий скелет всех техник дурения

[!info] О чём заметка Все функции дурения DPI в Zapret2/Zapret2 (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 и каким способом выпускают пакеты в сеть. Всё остальное у них общее.

Оглавление


Почему у всех функций одинаковый скелет

Функции дурения живут в 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:

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-техники интересны только на этапе рукопожатия.

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» — их работа на этом соединении закончена навсегда:

    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. Отсечение противоположного направления

direction_cutoff_opposite(ctx, desync)

Почти всем функциям интересны только исходящие пакеты — от клиента к серверу. Это направление по умолчанию (dir=out), и direction_cutoff_opposite при первом же вызове отключает инстанс от входящего направления. Механика та же, что на первой стадии: движок перестаёт дёргать Lua там, где это заведомо бесполезно.

Функция читает аргумент dir и работает так:

dir Что отключается
out (по умолчанию) входящее направление
in исходящее направление
any ничего не отключается

Значение по умолчанию у функций разное. У большинства техник дурения это out, а у служебных drop, send и pktmodany, потому что они осмысленны в обе стороны:

direction_cutoff_opposite(ctx, desync, "any")

Обратите внимание: отключение и проверка — разные вещи, разнесённые по разным стадиям. Здесь инстанс лишь перестаёт получать пакеты не своего направления; фактическая проверка текущего пакета (direction_check) произойдёт позже, на стадии 5. Так сделано потому, что cutoff — это оптимизация на будущее, а гвард — решение по текущему пакету.


Стадия 3. Ранняя проверка аргументов

До того, как трогать данные, функция проверяет, что её вообще корректно вызвали. Здесь два разных механизма с принципиально разным поведением.

Обязательные аргументы — error(). Если без аргумента техника не имеет смысла, функция бросает ошибку Lua. fake требует blob (без фейкового содержимого фейк невозможен), tcpseg требует pos (без диапазона нечего отправлять), tls_client_hello_clone требует blob, куда класть клон:

if not desync.arg.blob then
    error("fake: 'blob' arg required")
end

Ошибка Lua не роняет nfqws2: C-ядро ловит её через lua_pcall, пишет в лог и прекращает обработку пакета. Но профиль при этом фактически не работает, поэтому такие ошибки надо ловить при первом же запуске с включённым отладочным выводом (см. log-analyzer).

Мягкий режим — optional. Если blob задан, но его нет, поведение зависит от флага optional. Без флага дальнейший вызов blob() бросит ошибку; с флагом функция тихо выходит, ничего не сделав:

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

Одна строка, определяющая всю дальнейшую работу:

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. Три гварда

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 и диссекта; здесь важно её следствие для скелета. При перепроигрывании функция вызывается на каждой части, но осмысленно сработать должна ровно один раз — иначе сервер получит данные несколько раз.

Отсюда пара хелперов:

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.

Типичная концовка сегментирующей функции выглядит так:

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. Со стороны скелета важно другое — как функция решает, к каким именно пакетам их применить.

Большинство функций собирают один общий набор:

desync_opts(desync)  -- rawsend + reconstruct + ipfrag + ipid + fooling, всё из desync.arg

и применяют его ко всему, что отправляют. Так устроены multisplit, multidisorder, tcpseg, rst, send, fake.

Функции с фейковыми сегментами собирают два разных набора — и это принципиально:

-- к оригиналам: только 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, и только потом разбираться с маркерами и содержимым пакетов.


📚 См. также


Источники: 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: исходник этой заметки · весь репозиторий.