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

О чём заметка

Подробный справочник по данным, с которыми работает Lua desync-функция в Zapret 2: как объявляется функция, что она получает в таблице 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 — таблица, содержащая все передаваемые в функцию значения: аргументы инстанса, диссект текущего пакета, данные потока и многое другое. Это главный вход функции.

Проще говоря

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 как есть. Без него эти поля генерируются автоматически по содержимому диссекта. Нужен, когда вы вручную выстраиваете цепочку заголовков и не хотите, чтобы ядро её пересчитало.

Агрегация вердиктов по цепочке инстансов

Результат всех --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).

ПолеТипСодержаниеПримечание
funcstringимя desync-функции
func_nnumberномер инстанса внутри профиля
func_instancestringназвание инстансапроизводная от имени функции, номера инстанса и номера профиля
profile_nnumberномер профиля
profile_namestringназвание профиляможет отсутствовать
cookiestringзначение параметра --cookie для профиляможет отсутствовать
outgoingbooltrue, если направление исходящее
ifinstringимя входящего интерфейсаможет отсутствовать
ifoutstringимя исходящего интерфейсаможет отсутствовать
fwmarknumberfwmark текущего пакетатолько в Linux
targettableip-адрес и порт, по которым проверяются ipset-ы и фильтры по портам
replayboolидёт проигрывание задержанного пакета (replay)
replay_piecenumberномер проигрываемой частинумерация с 1
replay_piece_countnumberколичество проигрываемых частей
replay_piece_lastboolпоследняя проигрываемая часть
l7payloadstringтип пейлоада текущего пакета или группы пакетовесли неизвестно — unknown
l7protostringтип протокола потокаесли неизвестно — unknown
reasm_datastringрезультат сборки многопакетного сообщения, либо сам пейлоад, если сборки не былопока только для TCP
reasm_offsetnumberсмещение текущего перепроигрываемого пакета в сборкепока только для TCP
decrypt_datastringрезультат сборки и дешифровки пейлоадов нескольких пакетовприменяется для QUIC
tcp_mssnumberMSS противоположного конца TCP-соединенияприсутствует всегда, только для TCP
tracktableданные, привязанные к записи conntrackтолько если есть conntrack, может не быть
argtableвсе аргументы инстанса и их значенияподстановки % и # уже замещены
distableдиссект текущего пакета

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, см. раздел ниже.

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

ПолеТипОписание
iptableзаголовок IPv4
ip6tableзаголовок IPv6
frag_offnumberсмещение IP-фрагмента; присутствует только в IP-фрагментах
tcptableзаголовок TCP
udptableзаголовок UDP
icmptableзаголовок ICMP
l4protonumberIPPROTO_TCP или IPPROTO_UDP
transport_lennumberдлина пакета без L3-заголовков
l3_lennumberдлина L3-заголовков, включая ip options и IPv6 extension headers
l4_lennumberдлина L4-заголовка, включая tcp options
payloadstringL4-пейлоад (или содержимое после L3-заголовков для raw IP)

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

ПолеОписание
ip_vверсия IP — 4
ip_hlдлина IP-заголовка в блоках по 4 байта (5 без ip options)
ip_tostype of service; содержит DSCP
ip_lenполная длина IP-пакета со всеми заголовками и пейлоадом
ip_idидентификатор пакета для сборки из фрагментов
ip_offoffset фрагмента, флаги MF (more fragments) и DF (don’t fragment)
ip_ttltime to live — максимальное число хопов
ip_pномер IP-протокола (как правило IPPROTO_TCP или IPPROTO_UDP)
ip_sumконтрольная сумма IP-заголовка
ip_srcIP источника
ip_dstIP назначения
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_hlimhop limit (тот же смысл, что TTL в IPv4)
ip6_srcIPv6-адрес источника
ip6_dstIPv6-адрес приёмника
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_flagsTCP-флаги: TH_FIN, TH_SYN, TH_RST, TH_PUSH, TH_ACK, TH_URG, TH_ECE, TH_CWR
th_seqsequence number
th_ackacknowledgement number
th_winразмер TCP-окна
th_sumконтрольная сумма TCP
th_urpurgent 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_data32-битное поле данных со смещения 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.)

Проще говоря

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


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

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

Всегда проверяйте наличие track

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

Поля track

ПолеТипОписаниеПримечание
incoming_ttlnumberTTL/hop limit первого входящего пакета потокаможет не быть, если не определено
l7protostringпротокол потокаесть всегда; если неизвестно — unknown
hostnamestringимя хоста (по анализу L6/L7-протоколов)появляется только после определения
hostname_is_ipboolявляется ли hostname IP-адресомтолько если есть hostname
lua_statetableхранилище состояния, привязанного к потокуесть всегда, передаётся с каждым пакетом потока
lua_in_cutoffboolотсечение Lua от входящего направлениятолько для чтения
lua_out_cutoffboolотсечение Lua от исходящего направлениятолько для чтения
t_startnumberunix-время первого пакета потокас дробной частью высокой точности
postableсчётчики по направлениямсодержит таблицы client, server, direct, reverse

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.seqsequence текущего пакета
tcp.rseqrelative sequence текущего пакетавычисляется как seq - seq0
tcp.rseq_over_2Gбыл переход rseq за границу 2 ГБs- и p-позиции больше учитывать нельзя
tcp.posrelative sequence верхней границы текущего пакетавычисляется как rseq + payload_size
tcp.upposмаксимальный pos в соединении
tcp.uppos_prevuppos в предыдущем пакете с даннымиполезно для определения ретрансмиссий
tcp.winsizeпоследнее поле th_winбез коррекции по scale
tcp.scaleпоследнее значение TCP-опции scale
tcp.winsize_calcwinsize с учётом scaleэффективный размер TCP-окна
tcp.mssпоследнее значение TCP-опции MSS

Не перепутайте сторону: 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 не может служить надёжным счётчиком.

Для счёта пакетов/байт берите p*counter

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


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

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

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

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 всегда отсутствует.


📚 См. также


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

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