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

35 KiB
Raw Permalink Blame History

date tags link aliases
2026-07-17
zapret
zapret2
nfqws2
lua
lua-desync
desync
dissect
conntrack
reference
https://github.com/bol-van/zapret2/blob/master/docs/manual.md
Структура таблицы desync
Прототип desync-функции
Диссект nfqws2
desync API
Структура track
Поля desync

🧬 Прототип desync-функции и структура таблицы desync

[!info] О чём заметка Подробный справочник по данным, с которыми работает Lua desync-функция в Zapret2/Zapret2: как объявляется функция, что она получает в таблице desync, из чего состоит диссект пакета (dis), запись потока (track), как приходят собранные из нескольких пакетов пейлоады (reasm/decrypt), и в чём особенности ICMP и raw IP. Сам механизм вызова --lua-desync (как и зачем) описан в desync; где вызов стоит в общем конвейере — в схема обработки трафика; из чего состоит проект — в структура проекта. Эта заметка — про устройство входных данных, то есть то, что нужно, чтобы писать или читать desync-функции.

TL;DR

  • Прототип: function desync_f(ctx, desync). ctx — ручка для вызова C-функций (отправка, cutoff), desync — таблица со всеми данными пакета.
  • Функция возвращает вердикт: VERDICT_PASS (не трогать), VERDICT_MODIFY (отправить изменённый диссект), VERDICT_DROP (выбросить); ничего не вернуть = PASS. Отдельный бит VERDICT_PRESERVE_NEXT сохраняет поля «next protocol» в IPv6.
  • Вердикты инстансов агрегируются: MODIFY перебивает PASS, DROP перебивает оба; PRESERVE_NEXT — если его вернул хоть один инстанс.
  • Изучать содержимое desync удобнее всего готовым инстансом pktdebug.
  • desync.dis — разобранный пакет (поля повторяют C-структуры из netinet/*.h; числа уже в machine byte order). desync.track — данные потока из conntrack (может отсутствовать — всегда проверяйте!).
  • Многобайтовые числа переведены в порядок машины автоматически, но sequence numbers 32-битные беззнаковые — для арифметики используйте u32add/bitand, а не обычное сложение.

Прототип Lua desync-функции

Любая desync-функция объявляется с двумя параметрами:

function desync_f(ctx, desync)
    -- ... тело ...
end
  • ctx — контекст для вызова некоторых C-функций. Сам по себе он не предназначен для чтения: его передают обратно в C (при отправке пакетов, при cutoff), чтобы ядро понимало, к какому пакету и очереди относится вызов.
  • desync — таблица, содержащая все передаваемые в функцию значения: аргументы инстанса, диссект текущего пакета, данные потока и многое другое. Это главный вход функции.

[!note] Проще говоря desync — это «вот пакет и всё, что мы про него знаем». ctx — «вот линия связи обратно в C-ядро, чтобы что-то отправить или отключиться». Первое читают, второе используют как ручку для команд.

Вердикты — что функция возвращает

Функция возвращает вердикт по текущему пакету. Можно не возвращать ничего — тогда результат приравнивается к VERDICT_PASS.

Вердикт Что делает
VERDICT_PASS передать пакет как есть, без учёта изменений диссекта
VERDICT_MODIFY выполнить реконструкцию и отправку текущего (изменённого) диссекта
VERDICT_DROP дропнуть (выбросить) текущий пакет
VERDICT_PRESERVE_NEXT отдельный бит, который прибавляется к основному вердикту

Про VERDICT_PRESERVE_NEXT. Это не самостоятельный вердикт, а флаг, добавляемый к основному (например, VERDICT_MODIFY + VERDICT_PRESERVE_NEXT). Он велит использовать поля «next protocol» в IPv6-заголовке и IPv6 extension headers как есть. Без него эти поля генерируются автоматически по содержимому диссекта. Нужен, когда вы вручную выстраиваете цепочку заголовков и не хотите, чтобы ядро её пересчитало.

[!important] Агрегация вердиктов по цепочке инстансов Результат всех --lua-desync-инстансов профиля объединяется по приоритету: VERDICT_MODIFY замещает VERDICT_PASS, а VERDICT_DROP замещает их обоих. VERDICT_PRESERVE_NEXT применяется, если его вернул хотя бы один инстанс. То есть достаточно одному инстансу вернуть DROP — оригинал будет выброшен, что бы ни вернули остальные.


Как изучать desync вживую

Структуру desync лучше всего изучать не по таблицам, а по её реальному содержимому на конкретном пакете. Для этого в zapret-lib.lua есть готовая тестовая функция-инстанс pktdebug — она выводит всё содержимое desync в лог. Достаточно добавить её в профиль:

--lua-desync=pktdebug

Дальнейшие таблицы полей — это как раз то, что вы увидите в выводе pktdebug (примеры ниже сняты, в частности, на HTTP-запросе по IPv6 к http://one.one.one.one).


Структура таблицы desync

Верхний уровень desync — это «паспорт» пакета: кто его обрабатывает (какой инстанс/профиль), куда он идёт, что за протокол, и вложенные таблицы с самим пакетом (dis) и потоком (track).

Поле Тип Содержание Примечание
func string имя desync-функции
func_n number номер инстанса внутри профиля
func_instance string название инстанса производная от имени функции, номера инстанса и номера профиля
profile_n number номер профиля
profile_name string название профиля может отсутствовать
cookie string значение параметра --cookie для профиля может отсутствовать
outgoing bool true, если направление исходящее
ifin string имя входящего интерфейса может отсутствовать
ifout string имя исходящего интерфейса может отсутствовать
fwmark number fwmark текущего пакета только в Linux
target table ip-адрес и порт, по которым проверяются ipset-ы и фильтры по портам
replay bool идёт проигрывание задержанного пакета (replay)
replay_piece number номер проигрываемой части нумерация с 1
replay_piece_count number количество проигрываемых частей
replay_piece_last bool последняя проигрываемая часть
l7payload string тип пейлоада текущего пакета или группы пакетов если неизвестно — unknown
l7proto string тип протокола потока если неизвестно — unknown
reasm_data string результат сборки многопакетного сообщения, либо сам пейлоад, если сборки не было пока только для TCP
reasm_offset number смещение текущего перепроигрываемого пакета в сборке пока только для TCP
decrypt_data string результат сборки и дешифровки пейлоадов нескольких пакетов применяется для QUIC
tcp_mss number MSS противоположного конца TCP-соединения присутствует всегда, только для TCP
track table данные, привязанные к записи conntrack только если есть conntrack, может не быть
arg table все аргументы инстанса и их значения подстановки % и # уже замещены
dis table диссект текущего пакета

[!note] l7payload vs l7proto l7proto — тип протокола всего потока (ставится один раз и держится до конца). l7payload — тип содержимого конкретного пакета внутри этого потока; у разных пакетов одного потока он может отличаться, а нераспознанный помечается как unknown. Подробнее про распознавание — payload.


Структура диссекта (desync.dis)

Диссект — это разобранный на поля текущий пакет. Прежде чем читать таблицы, важно понять три общих правила устройства диссекта:

  1. Версия IP и L4-протокол определяются по наличию полей. Есть dis.ip → это IPv4, есть dis.ip6 → IPv6. Есть dis.tcp → транспорт TCP, есть dis.udp → UDP. Не «читайте номер протокола», а проверяйте наличие подтаблицы.
  2. Имена полей повторяют C-структуры. Таблицы заголовков копируют названия полей из системных заголовков netinet/{ip,ip6,tcp,udp}.h. IP-адреса и IPv4 options передаются как «сырая» строка (raw string) — для перевода raw IP в текст есть C-функция ntop (она сама определяет версию по размеру). IPv6 extension headers и TCP options представлены таблицами.
  3. Порядок байт уже машинный. Все многобайтовые числовые значения автоматически переведены из сетевого порядка байт (network byte order) в порядок машины (machine byte order) — читать и сравнивать их можно как обычные числа. Исключение по осторожности — sequence numbers, см. раздел ниже.

Верхний уровень диссекта

Поле Тип Описание
ip table заголовок IPv4
ip6 table заголовок IPv6
frag_off number смещение IP-фрагмента; присутствует только в IP-фрагментах
tcp table заголовок TCP
udp table заголовок UDP
icmp table заголовок ICMP
l4proto number IPPROTO_TCP или IPPROTO_UDP
transport_len number длина пакета без L3-заголовков
l3_len number длина L3-заголовков, включая ip options и IPv6 extension headers
l4_len number длина L4-заголовка, включая tcp options
payload string L4-пейлоад (или содержимое после L3-заголовков для raw IP)

Заголовок IPv4 (dis.ip)

Поле Описание
ip_v версия IP — 4
ip_hl длина IP-заголовка в блоках по 4 байта (5 без ip options)
ip_tos type of service; содержит DSCP
ip_len полная длина IP-пакета со всеми заголовками и пейлоадом
ip_id идентификатор пакета для сборки из фрагментов
ip_off offset фрагмента, флаги MF (more fragments) и DF (don't fragment)
ip_ttl time to live — максимальное число хопов
ip_p номер IP-протокола (как правило IPPROTO_TCP или IPPROTO_UDP)
ip_sum контрольная сумма IP-заголовка
ip_src IP источника
ip_dst IP назначения
options бинарный блок ip options (почти не используется, режется всеми)

Заголовок IPv6 (dis.ip6)

Поле Описание
ip6_flow первые 4 байта IPv6-заголовка: version (6), traffic class, flow label
ip6_plen длина пакета за вычетом базового заголовка IPv6 — IP6_BASE_LEN (40 байт)
ip6_nxt следующий протокол; если нет exthdr — IPPROTO_TCP (6) или IPPROTO_UDP (17)
ip6_hlim hop limit (тот же смысл, что TTL в IPv4)
ip6_src IPv6-адрес источника
ip6_dst IPv6-адрес приёмника
exthdr массив таблиц расширенных заголовков (индекс с 1)

IPv6 extension header (dis.ip6.exthdr[i])

Поле Описание
type тип заголовка: IPPROTO_HOPOPTS, IPPROTO_ROUTING, IPPROTO_DSTOPTS, IPPROTO_MH, IPPROTO_HIP, IPPROTO_SHIM6, IPPROTO_FRAGMENT, IPPROTO_AH
next тип следующего заголовка (аналогично type); для последнего может быть IPPROTO_TCP/IPPROTO_UDP
data данные без первых двух байт (типа и длины)

Заголовок UDP (dis.udp)

Поле Описание
uh_sport порт источника
uh_dport порт приёмника
uh_ulen длина UDP — UDP_BASE_LEN (8) + длина пейлоада
uh_sum контрольная сумма UDP

Заголовок TCP (dis.tcp)

Поле Описание
th_sport порт источника
th_dport порт приёмника
th_x2 зарезервированное поле; используется для расширенных TCP-флагов
th_off размер TCP-заголовка в блоках по 4 байта
th_flags TCP-флаги: TH_FIN, TH_SYN, TH_RST, TH_PUSH, TH_ACK, TH_URG, TH_ECE, TH_CWR
th_seq sequence number
th_ack acknowledgement number
th_win размер TCP-окна
th_sum контрольная сумма TCP
th_urp urgent pointer
options массив таблиц TCP-опций (индекс с 1)

TCP-опция (dis.tcp.options[i])

Поле Описание
kind тип опции: TCP_KIND_END, TCP_KIND_NOOP, TCP_KIND_MSS, TCP_KIND_SCALE, TCP_KIND_SACK_PERM, TCP_KIND_SACK, TCP_KIND_TS, TCP_KIND_MD5, TCP_KIND_AO, TCP_KIND_FASTOPEN
data данные опции без kind и длины; отсутствует для TCP_KIND_END и TCP_KIND_NOOP

Заголовок ICMP (dis.icmp)

Заголовком ICMP считается его постоянная часть — первые 8 байт. Они универсальны и для IPv4-, и для IPv6-версии ICMP. Всё остальное содержимое зависит от типа ICMP и лежит в payload — включая возможные специальные поля заголовка или обрезанный исходный пакет, на который был сгенерирован ICMP.

Поле Описание
icmp_type тип ICMP
icmp_code код ICMP
icmp_cksum контрольная сумма ICMP
icmp_data 32-битное поле данных со смещения 4

Приём многопакетных пейлоадов (reasm / decrypt / replay)

Иногда одно логическое сообщение не помещается в один пакет. Его сборка называется reasm (reassemble). Она выполняется автоматически C-кодом, если встречен пейлоад, требующий сборки, доступен conntrack и сборка не запрещена флагом --reasm-disable. На данный момент таких пейлоадов два — tls_client_hello и quic_initial: оба могут содержать постквантовую криптографию Kyber, которая не влезает в один пакет.

Сборка идёт по-разному:

  • tls_client_hello — обычная сборка: пейлоады последовательных TCP-сегментов объединяются в единый блок reasm_data.
  • quic_initial — отдельные пакеты накапливаются во внутреннем буфере, затем происходит их дешифровка, объединение и дефрагментация частей пейлоада, разбросанных по пакетам и разным смещениям (так делает Chrome — намеренно, чтобы все реализации следовали стандарту и умели корректно собирать пейлоад по частям).

Как это видно в Lua (replay)

Пока сборка не финализирована, пакеты копятся во внутреннем буфере без вызовов Lua. Как только сборка завершена, начинается перепроигрывание отдельных частей (replay): в Lua-инстансы приходит диссект каждого задержанного пакета, но при этом выставлены поля desync.replay=true, desync.replay_piece, desync.replay_piece_count и desync.replay_piece_last.

Различие между TCP и QUIC в том, какое поле несёт собранный результат:

  • TCP-сборка: выставляется desync.reasm_data — полный блок собранных данных. При этом desync.dis.payload по-прежнему содержит пейлоад отдельного перепроигрываемого пакета. Если replay нет (одиночный пакет), для TCP desync.reasm_data содержит копию desync.dis.payload.
  • QUIC-сборка: desync.reasm_data отсутствует — вместо него передаётся desync.decrypt_data с результатом дешифровки и дефрагментации всех пейлоадов сборки. (Для QUIC этот decrypt_data содержит tls_client_hello без record layer.)

[!note] Проще говоря reasm_data/decrypt_data — это «целое собранное сообщение», а dis.payload — «кусок из конкретного пакета». Стратегии, которым нужна полная картина (например, найти домен в SNI и разрезать по нему), работают с собранным блоком; отправка же по-прежнему идёт по отдельным пакетам-частям. Как это использует конкретная функция — см. multisplit.


Структура track (данные потока из conntrack)

Таблица track присутствует в desync только если для текущего пакета нашлась запись в conntrack (системе схема обработки трафика). Её может не быть, если nfqws2 не видел пакет SYN или SYN,ACK: соединение установили до запуска nfqws2, вы не перехватили SYN/SYN,ACK из ядра, либо conntrack принудительно выключен через --ctrack-disable.

[!danger] Всегда проверяйте наличие track Ваш код обязан проверять наличие desync.track (и опциональных полей внутри него) перед обращением — иначе он упадёт с ошибкой на пакетах без conntrack. Проверяйте свой код с --ctrack-disable и на разных протоколах — TCP и UDP.

Поля track

Поле Тип Описание Примечание
incoming_ttl number TTL/hop limit первого входящего пакета потока может не быть, если не определено
l7proto string протокол потока есть всегда; если неизвестно — unknown
hostname string имя хоста (по анализу L6/L7-протоколов) появляется только после определения
hostname_is_ip bool является ли hostname IP-адресом только если есть hostname
lua_state table хранилище состояния, привязанного к потоку есть всегда, передаётся с каждым пакетом потока
lua_in_cutoff bool отсечение Lua от входящего направления только для чтения
lua_out_cutoff bool отсечение Lua от исходящего направления только для чтения
t_start number unix-время первого пакета потока с дробной частью высокой точности
pos table счётчики по направлениям содержит таблицы client, server, direct, reverse

[!tip] lua_state — долгая память потока track.lua_state есть всегда (когда есть track) и выдаётся одна и та же таблица на каждый пакет потока. В ней хранят состояние между пакетами: счётчики попыток, флаги «уже отправлено» и т. п. Для временных данных в пределах одного пакета используйте саму таблицу desync (см. desync про два уровня памяти).

Счётчики track.pos (client / server / direct / reverse)

track.pos содержит подтаблицы счётчиков по двум сторонам соединения: client — пакеты от клиента, server — пакеты от сервера. Ещё две подтаблицы, direct и reverse, — это просто ссылки на client/server. Куда они указывают, зависит от текущего направления (desync.outgoing) и серверного режима (b_server): direct всегда указывает на текущее направление, reverse — на противоположное.

Кроме того, в track.pos есть поле dt — время получения пакета в секундах от t_start (с дробной частью высокой точности).

Набор счётчиков в каждой подтаблице (подтаблица tcp присутствует только для TCP):

Поле Описание Примечание
pcounter счётчик пакетов
pdcounter счётчик пакетов с данными у которых размер L4-пейлоада не равен 0
pbcounter счётчик переданных байт считается только размер L4-пейлоада, без заголовков
ip6_flow последнее поле ip6.ip6_flow отсутствует, если неизвестно или протокол не IPv6
tcp.seq0 начальный sequence соединения
tcp.seq sequence текущего пакета
tcp.rseq relative sequence текущего пакета вычисляется как seq - seq0
tcp.rseq_over_2G был переход rseq за границу 2 ГБ s- и p-позиции больше учитывать нельзя
tcp.pos relative sequence верхней границы текущего пакета вычисляется как rseq + payload_size
tcp.uppos максимальный pos в соединении
tcp.uppos_prev uppos в предыдущем пакете с данными полезно для определения ретрансмиссий
tcp.winsize последнее поле th_win без коррекции по scale
tcp.scale последнее значение TCP-опции scale
tcp.winsize_calc winsize с учётом scale эффективный размер TCP-окна
tcp.mss последнее значение TCP-опции MSS

[!warning] Не перепутайте сторону: MSS / winsize / scale Значения MSS, winsize и scale одна сторона соединения передаёт другой, чтобы та знала допустимые параметры партнёра. Если вам нужно узнать, какого размера пакеты можно отсылать, смотрите противоположную сторону — то, что она может принять. MSS дополнительно дублируется в desync.tcp_mss независимо от наличия conntrack, и там значение уже рассчитано под вычисление размера отправляемого пакета. Если conntrack нет или MSS не был согласован, используется значение по умолчанию DEFAULT_MSS (1220).


Работа с sequence numbers

Общее правило «числа уже в машинном порядке байт» верно, но у TCP sequence/ack есть отдельная ловушка: они 32-битные беззнаковые и переполняются по кругу. Если к 4294967280 (0xFFFFFFF0) прибавить 100, по правилам TCP получится не 4294967380, а 84 (0x54) — счётчик «завернулся».

Проблема в том, что Lua считает числа в более широком, знаковом представлении: обычное seq1 + seq2 даст «неправильные» с точки зрения TCP 4294967380, без заворота. Поэтому для арифметики и сравнения sequence используйте C-функции u32add и bitand. Пример — проверка seq1 >= seq2:

-- истина, если seq1 >= seq2 (при расхождении не более 2 ГБ)
0 == bitand(u32add(seq1, -seq2), 0x80000000)

Наивный вариант через обычное сложение работать корректно не будет, а этот — будет, если seq1 ушёл от seq2 не более чем на 2 ГБ. Большее расстояние через sequence в принципе не отследить, поэтому при передаче больших объёмов данных sequence не может служить надёжным счётчиком.

[!tip] Для счёта пакетов/байт берите p*counter Счётчики pcounter/pdcounter/pbcounter в track.pos64-битные, у них проблемы заворота нет. Если нужен именно счётчик, используйте их, а не арифметику sequence.


Особенности обработки ICMP

Некоторые типы ICMP несут прикреплённый исходный пакет, на который был сгенерирован ICMP, — их называют «related». nfqws2 распознаёт такие пейлоады и ищет по прикреплённому пакету исходную запись conntrack. Если она находится:

  • выбирается кэшированный профиль (тот, к которому относится прикреплённый пакет);
  • направление выбирается как обратное относительно найденной записи;
  • тип пейлоада ставится как ipv4 или ipv6, тип протокола сеанса берётся из профиля исходного пакета;
  • дальше ICMP проходит по профилю обычным образом.

[!warning] ICMP без TCP/UDP, но с track В функцию может прийти диссект с icmp, без tcp или udp, но при этом с desync.track (взятым от исходной записи). Учитывайте это в коде — не считайте, что раз есть track, то есть и tcp.

Если ICMP не содержит прикреплённого пакета, он невалиден или запись conntrack не найдена — ICMP проходит самостоятельно, без track. Важно: conntrack ведёт учёт только для TCP и UDP; пинги и прочие ICMP он не отслеживает, и при проходе ICMP по записи conntrack никакие счётчики не меняются.


Особенности обработки raw IP

Если IP-протокол не распознан как TCP, UDP, ICMP или ICMPv6, пакет считается raw IP. В диссекте тогда присутствуют поля ip/ip6 и payload, где payload — это всё содержимое пакета после L3-заголовков. desync.track для raw IP всегда отсутствует.


📚 См. также


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