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

О чём заметка

Все функции дурения DPI в 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 и каким способом выпускают пакеты в сеть. Всё остальное у них общее.

Оглавление


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

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

Проще говоря

Скелет — это не требование движка, а сложившаяся общая часть. Все функции сперва делают одну и ту же «бухгалтерию» (мой ли это пакет, мои ли данные, не обрабатывал ли я это уже), и лишь потом занимаются своим приёмом. Поэтому осваивать 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). Без отслеживания соединений хранить флаг негде, и защита от повторной отправки не работает.

Не путайте 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не-TCPoutblob → reasm → payloadдаpayload_segmentedDROP
multidisorderне-TCPoutblob → reasm → payloadдаpayload_segmentedDROP
multidisorder_legacyне-TCPoutpayload (reasm — только для маркеров)нетpayload_segmentedDROP
tcpsegне-TCPoutblob → reasm → payloadдаpayload_segmentedнет (nil)
oobне-TCP, отсутствие conntrack, старт не с SYN— (по позиции в потоке)reasm → payloadособыйdissect_segmentedDROP / MODIFY

Техники с фейковыми сегментами

ФункцияСтадия 1 отсекаетdirДанныеreplayОтправкаВердикт
fakedsplitне-TCPoutblob → reasm → payloadдаpayload_segmented, два набора опцийDROP
fakeddisorderне-TCPoutblob → reasm → payloadдаpayload_segmented, два набора опцийDROP
hostfakesplitне-TCPoutblob → 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_ipfragDROP
synackне-TCP, не SYNнетdissect_ipfragнет
synack_splitне-TCP, не SYN+ACKнетdissect_ipfragDROP
wsizeне-TCP, не SYN+ACKнетMODIFY
wssizeне-TCP; форс-cutoff после непустого payloadoutнетMODIFY

Модификаторы и служебные

ФункцияСтадия 1 отсекаетdirДанныеreplayОтправкаВердикт
http_domcase · http_hostcase · http_methodeol · http_unixeolне-TCP, не http_reqoutpayloadнетMODIFY
udplenне-UDPoutpayloadнетMODIFY
dht_dnне-UDP, не dhtoutpayloadнетMODIFY
pktmodanypayloadнетMODIFY
sendanyтекущий диссектнетdissect_ipfragнет
dropanyнетDROP
rstне-TCPany при проверке, но инстанс отключается от входящего— (пустой payload)даdissect_ipfragнет
tls_client_hello_cloneне-TCPoutreasm → payloadнетнет

Как читать скелет в логах

Отладочный вывод (--debug, разбор которого есть в log-analyzer) построен так, что по нему видно, на какой стадии остановилась функция. Сообщения общего скелета одинаковы для всех техник — меняется только префикс с именем инстанса:

СообщениеСтадияЧто произошло
voluntary cutoff1–2инстанс сам отключился от потока и больше не вызывается
pos … is beyond rangeдо 1инстанс не вызван из-за внутрипрофильного фильтра --out-range
payload_type '…' does not satisfy filter5тип протокола не прошёл фильтр payload
blob '…' not found. skipped3сработал мягкий режим optional
not acting on further replay pieces6это не первая часть многопакетного запроса
dropping replay packet because reasm was already sent6часть дропнута, реасм уже отправлен нарезанным
no valid split positions7ни один маркер не разрешился (специфично для сегментации)
desync7функция реально вызвана и начала работу

Практический порядок разбора «почему техника не сработала»: сначала убедиться, что вообще есть строка 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.


🤖 Эти статьи открыты — можно обучать на них ИИ

При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.