Обновить правила репозитория и подписи исходников в 99 заметках, не затрагивая пользовательские незакоммиченные файлы. Сделать Forgejo Actions содержательным: проверять опубликованный commit, а не пустое рабочее дерево после checkout.
35 KiB
| date | tags | link | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-07-17 |
|
https://github.com/bol-van/zapret2/blob/master/docs/manual.md |
|
🧬 Прототип 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]
l7payloadvsl7protol7proto— тип протокола всего потока (ставится один раз и держится до конца).l7payload— тип содержимого конкретного пакета внутри этого потока; у разных пакетов одного потока он может отличаться, а нераспознанный помечается какunknown. Подробнее про распознавание — payload.
Структура диссекта (desync.dis)
Диссект — это разобранный на поля текущий пакет. Прежде чем читать таблицы, важно понять три общих правила устройства диссекта:
- Версия IP и L4-протокол определяются по наличию полей. Есть
dis.ip→ это IPv4, естьdis.ip6→ IPv6. Естьdis.tcp→ транспорт TCP, естьdis.udp→ UDP. Не «читайте номер протокола», а проверяйте наличие подтаблицы. - Имена полей повторяют C-структуры. Таблицы заголовков копируют названия полей из системных заголовков
netinet/{ip,ip6,tcp,udp}.h. IP-адреса и IPv4 options передаются как «сырая» строка (raw string) — для перевода raw IP в текст есть C-функцияntop(она сама определяет версию по размеру). IPv6 extension headers и TCP options представлены таблицами. - Порядок байт уже машинный. Все многобайтовые числовые значения автоматически переведены из сетевого порядка байт (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 нет (одиночный пакет), для TCPdesync.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.pos— 64-битные, у них проблемы заворота нет. Если нужен именно счётчик, используйте их, а не арифметику 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 всегда отсутствует.
📚 См. также
- desync — как и зачем вызываются desync-функции (эта заметка — про их входные данные)
- жизненный цикл desync-функции — что функция делает с этими входными данными: восемь стадий, общих для всех техник дурения
- схема обработки трафика — где в конвейере пакета появляются
dis,track, reasm и replay - структура проекта — почему логика в Lua, а разбор пакета и conntrack — в C-ядре
- multisplit — практический пример: как одна функция читает
desyncи возвращает вердикт - payload — что означают
l7payloadиl7proto - blob — заготовки, на которые ссылаются аргументы
blob=… - 🔗 Официальная документация (docs/manual.md) — первоисточник
[!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: исходник этой заметки · весь репозиторий.