♻️ Жизненный цикл 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 и каким способом выпускают пакеты в сеть. Всё остальное у них общее.
Оглавление
- Почему у всех функций одинаковый скелет
- Восемь стадий целиком
- Стадия 1. Отсев чужого транспорта и instance cutoff
- Стадия 2. Отсечение противоположного направления
- Стадия 3. Ранняя проверка аргументов
- Стадия 4. Выбор данных: blob → reasm → payload
- Стадия 5. Три гварда
- Стадия 6. Развилка перепроигрывания (replay)
- Стадия 7. Собственно техника
- Стадия 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 не прошёл фильтр, маркер не разрешился. Зная стадии, вы читаете отладочный лог не как поток сообщений, а как маршрут: видно, до какой стадии функция дошла и где остановилась.
Проще говоря
Скелет — это не требование движка, а сложившаяся общая часть. Все функции сперва делают одну и ту же «бухгалтерию» (мой ли это пакет, мои ли данные, не обрабатывал ли я это уже), и лишь потом занимаются своим приёмом. Поэтому осваивать 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 и pktmod — any, потому что они осмысленны в обе стороны:
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 ... -- дропаем остальные части: их содержимое уже отправлено
endreplay_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 | не-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.
🤖 Эти статьи открыты — можно обучать на них ИИ
При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.