О чём заметка

Это полный текст видео номер 16 из серии о том, как работает Zapret 2, разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.

Как читать пометки

«В нашем тесте» значит настоящий запуск в лаборатории проекта: клиент, роутер, сервер. «По коду» значит, что мы прочитали исходники, но не запускали. «По общим сведениям» значит, что это вне нашего кода и лаборатории. О настоящем оборудовании ТСПУ мы ничего не утверждаем: наш «инспектор» это модель.

Повтор и вопрос части

Добро пожаловать в третий выпуск. Короткий повтор, чтобы часть можно было смотреть отдельно.

Номера выпусков в видео

В озвучке выпуски названы по номерам из первоначального плана серии. Сейчас порядок другой, поэтому читайте номера по таблице: «первый выпуск» это «Письмо в сортировочном центре», «второй» это «Что видит наблюдатель», «третий» это видео, «Внутри программы». «Четвёртый» это «От байтов к таблице», «пятый» это «Память о соединении», «шестой» это «Тип нагрузки и имя сайта», «седьмой» это «Профили: как программа выбирает», «восьмой» это «Lua внутри программы», «десятый» это «multisplit: резать на части». «Следующий выпуск» в конце видео это «От байтов к таблице».

В первом выпуске мы представили сортировочный центр. Письма едут по ленте, а сортировщик не вскрывает конверты: он смотрит в окошко с адресом и решает, пропустить письмо или бросить в корзину. Так мы описали наблюдателя на пути пакетов, которого называем ТСПУ. Это только модель. Настоящее оборудование мы не видели и не знаем, как оно устроено, поэтому говорим осторожно.

В нашей лаборатории наблюдателя играет игрушечный инспектор: простое правило в роутере, которое ищет имя сайта в каждом пакете по отдельности. Схема такая: клиент, роутер и сервер. Программа zapret2 и правила ядра стоят на клиенте.

Во втором выпуске мы вскрыли конверт. Пакет это вложенные слои: Ethernet, четырнадцать байт; IP, двадцать; TCP, тридцать два; и данные, у нашего запроса двести пятьдесят. Вместе триста шестнадцать байт.

Внутри запроса есть имя сайта, www.example.com, пятнадцать байт. Если запрос разрезан на две части, сто четыре и сто сорок шесть байт, получатель склеивает их по номерам, а игрушечный инспектор, читающий пакеты по одному, целого имени не находит.

Но один вопрос мы обходили. Обычные пакеты создаёт не zapret2, а сетевая часть операционной системы. Как же пакет, уже собранный и готовый уйти в сеть, попадает в отдельную программу? Как она возвращает его обратно: целым, изменённым или выброшенным? И как сделать, чтобы она не перехватывала вообще всё? Это три вопроса выпуска.

Три вопроса выпуска Клиент с zapret2: слева ядро, сетевая часть операционной системы, справа программа nfqws2 в пользовательском пространстве. Три вопроса выпуска: кто создаёт обычные пакеты (ядро, а не zapret2); как пакет попадает в программу и возвращается (целым, изменённым или выброшенным); как не перехватывать всё подряд (правило в ядре отбирает только нужные пакеты). Клиент с zapret2: ядро и программа ЯДРОсетевая часть ОСПРОГРАММЫпользовательское пространство создаёт и принимаетобычные пакеты браузерредакторnfqws2 не в ядре: обычные процессы 1 пакет2 ответ целымизменённымвыброшенным 3 правило: только нужные 1Кто создаёт обычные пакеты?ответ: ядро ОС, не zapret2 2Как пакет попадает в программу и возвращается?разбор: части 1 и 2 3Как не перехватывать всё подряд?разбор: часть 1 Разберём в два слоя: сначала картинка, потом настоящие правила ядра, код и один настоящий пакет.

Мы разберём их в два слоя. Сначала картинка на сортировочном центре, потом настоящие правила ядра, настоящий код и один настоящий пакет, наш запрос на двести пятьдесят байт. Исходные тексты мы открывали и проверяли. Там, где мы измерили что-то сами, мы говорим «в нашем тесте». Там, где речь об устройстве ядра Linux и мы этого не проверяли, мы говорим осторожно или молчим.

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

План выпуска из трёх частей План выпуска из трёх частей. Часть 1, общая картина: где в ядре пункт досмотра, правила, выбирающие пакеты, почему выбираются только первые пакеты. Часть 2, сама программа: очередь и главный цикл, приёмщик пакетов, три ответа ядру, один пакет по журналу. Часть 3, обратная сторона: как программа сама отправляет пакеты, защита от возврата по кругу, другие системы, первый пакет соединения. Внизу путь пакета: правило в ядре, очередь, программа, ответ; от программы отдельно идёт отправка своих пакетов в сеть. План выпуска: три части и путь пакета ЧАСТЬ 1Общая картина· где в ядре пункт досмотра· правила, выбирающие пакеты· почему только первые пакеты ЧАСТЬ 2Сама программа· очередь и главный цикл· приёмщик пакетов· три ответа ядру· один пакет по журналу ЧАСТЬ 3Обратная сторона· программа сама шлёт пакеты· защита от возврата по кругу· другие системы· первый пакет соединения вы здесь Путь одного пакета правило в ядре очередь программа nfqws2 три ответа ядру своя отправка пакетовв сеть не должна идти по кругу

Итак, цель этой части: понять, где в ядре стоит пункт досмотра и как правила выбирают пакеты. Начинаем с картинки.

Ядро, окошко, очередь и три штампа

Первый вопрос: где программа zapret2 находится по отношению к пакетам, и как пакет попадает к ней и возвращается?

Представим выдуманный пример. Большая почта, внутри неё лента: письма едут одно за другим, там их нумеруют и отправляют дальше. Рядом отдельная контора, где сидит умный клерк. К ленте он не допущен, двери между зданиями нет. Чтобы он увидел письмо, письмо кто-то должен вынести и принести ему. Для почты это лишняя работа.

Теперь с настоящими названиями. У компьютера с Linux есть ядро, главная часть операционной системы. По общим сведениям, ядро само работает с сетевой картой, принимает и отправляет пакеты и держит протокол TCP из второго выпуска. Мы этого в лаборатории отдельно не проверяли. В нашей картинке ядро это здание с лентой.

Остальные программы живут снаружи: браузер, редактор и основная часть zapret2, программа nfqws2. Эту область называют user space, пользовательское пространство. В документации сказано прямо: nfqws2 работает не в режиме ядра, это пользовательский процесс, а передача пакетов между ядром и пользовательским пространством связана со значительными накладными расходами. Чем больше лишнего трафика отсеяно, тем ниже нагрузка на процессор. Вывод: в контору надо выносить не все письма, а только нужные, и выбирать их лучше прямо в ядре.

Как это выглядит. На ленте есть пункт досмотра, окошко в стене. К окошку ядро подвозит только письма, выбранные правилом, остальные едут мимо. Оригинал выбранного письма ждёт у окошка, а его копию протягивают клерку. Клерк пишет на листке своё решение, и ядро поступает с письмом по этому решению. Пока листка нет, письмо стоит. Что ядро делает с очередью внутри, мы не открывали, и опыта с длинной очередью ожидающих пакетов у нас не было, так что это упрощение.

Окошко в ядре и программа за стеной Выдуманная картинка: слева ядро с лентой, на которой едут пакеты, справа отдельные программы (браузер, редактор, nfqws2). Двери между зданиями нет. У ленты пункт досмотра, окошко: пакеты, не выбранные правилом, едут мимо; выбранный пакет подъезжает к окошку, оригинал ждёт, копию уносят программе nfqws2, программа возвращает листок с решением, и ядро поступает с оригиналом по этому решению; следующий выбранный пакет ждёт очереди (упрощение, внутри ядра очередь не открывали). В документации сказано, что nfqws2 работает не в ядре, а передача пакетов из ядра в программу отнимает много ресурсов, поэтому лучше отфильтровать как можно больше прямо в ядре. Как пакет попадает из ядра в программу и обратно выдуманная картинка ЯДРОсетевая часть ОС, лента пакетовПРОГРАММЫснаружи ядра, user space браузерредакторnfqws2 пункт досмотра нетдвери оригинал ждёт копия решение следующий ждёт 1Выбранный пакет подъезжает к окошку, остальные едут мимо. 2Оригинал ждёт у окошка, копию уносят программе. 3Программа пишет листок «решение»; пока его нет, пакет стоит. 4Ядро поступает с оригиналом по листку; следующий выбранный ждёт очереди. Что ядро делает с очередью внутри, мы не открывали: это упрощение. Из документации zapret2«Процесс перенаправления трафика из ядра отнимает достаточно много ресурсов, поэтому лучше всегоотфильтровать как можно больше прямо в нём.» docs/readme.md:74«nfqws2 does not run in kernel mode; it is a user-mode process.» docs/manual.en.md:324

Этот механизм в Linux называется NFQUEUE, «очередь пакетов для программы». Приставка NF, по общим сведениям, от слова netfilter: так называется часть ядра, которая работает с пакетами.

У очереди есть номер, любой от нуля до шестидесяти пяти тысяч пятисот тридцати пяти. Это самое большое число, которое помещается в шестнадцать бит. В наших примерах номер двести. Правило в ядре отправляет пакеты в очередь двести, а программа подписывается на очередь двести. Номера должны совпасть, иначе письма будут направлены в одну комнату, а клерк будет ждать в другой; что при этом случится, разберём на слове bypass.

Есть и второй номер, его не надо путать с первым: у каждого пакета в очереди своя бирка. По общим сведениям, её выдаёт ядро, и по ней программа говорит, к какому пакету относится её решение. В нашем тесте бирки в каждом прогоне идут с единицы подряд, в девяноста двух журналах без пропусков. Это не номер из заголовка IP и не порядковый номер TCP.

Решение клерка называется вердиктом, и на листке он пишет одно из трёх. Пропустить как есть: письмо едет дальше тем же, в программе это PASS. Пропустить, но с заменой: письмо едет с другими байтами, это MODIFY. Выбросить: это DROP. PASS совсем не значит «подозрительно», это самый обычный ответ: в нашем опыте t01, «тэ-ноль-один», где стратегия pass ничего не делает, все десять пакетов очереди получили именно его. Будем называть их тремя штампами.

Номер очереди, бирка и вердикт Три вещи у пакета в очереди. Номер очереди: число от 0 до 65535, то есть шестнадцать бит, все единицы; в наших примерах 200, правило ядра queue num 200 и параметр программы --qnum=200 должны совпасть, а число больше 65535 программа отвергает сообщением bad qnum (nfq2/nfqws.c:2392-2396). Бирка пакета: номер, который в очереди выдаёт ядро (в нашем тесте с единицы подряд, например id 1–11); это не номер очереди и не идентификатор из заголовка IP (в том же прогоне 37470–37475). Ответ программы, вердикт, одно из трёх слов: PASS (0), MODIFY (1), DROP (2) (nfq2/darkmagic.h:101-103). PASS отпускает пакет как есть, MODIFY отпускает с другими байтами, DROP выбрасывает (nfq2/nfqws.c:330-338). В журналах нашей лаборатории из 1062 пакетов 990 получили PASS, 22 MODIFY, 50 DROP. Номер очереди, бирка пакета и три ответа Номер очередиот 0 до 65535, шестнадцать бит 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 шестнадцать единиц подряд = 65535 = 2¹⁶ − 1 065535200 — номер в наших примерах правило ядраqueue num 200=программа nfqws2--qnum=200 Бирка пакета в очередиу каждого пакета своя один пакет в очереди queue 200 id = 4 id из шапки IP queue 200— куда везти пакет id = 4— бирка, её выдаёт ядро по ней программа называет пакет,к которому относится её решение id из шапки IP— другое поле и не порядковый номер TCP В нашем тесте бирки идут с 1 подряд(92 журнала, пропусков нет). В одном прогоне:бирки 1…11, а id в шапке IP 37470…37475. Вердикт: один из трёх ответовnfq2/darkmagic.h:101-103 PASS= 0пропустить как естьNF_ACCEPTnfqws.c:337-338 MODIFY= 1пропустить с другими байтамиNF_ACCEPT + новые байтыnfqws.c:330-331 DROP= 2выброситьNF_DROPnfqws.c:333-334 В журналах нашей лаборатории (92 журнала) из 1062 пакетов: 990 PASS, 22 MODIFY, 50 DROP.PASS не значит «подозрительно»: в опыте со стратегией pass все 10 пакетов очереди получили именно его. Проверь себя: что при --qnum=65536?2392params.qnum = atoi(optarg);2393if (params.qnum < 0 || params.qnum>65535)2395DLOG_ERR("bad qnum\n");2396exit_clean(1);

Проверь себя. Что будет, если запустить программу с номером очереди шестьдесят пять тысяч пятьсот тридцать шесть? В коде при разборе параметров стоит проверка: если число меньше нуля или больше шестидесяти пяти тысяч пятисот тридцати пяти, программа пишет в журнал bad qnum, «плохой номер очереди», и завершается.

Итак, nfqws2 сидит в соседней конторе, и ядро выносит ему только выбранные пакеты через очередь с номером. У каждого пакета своя бирка, а ответ один из трёх: PASS, MODIFY или DROP.

Проверка в лаборатории: путь пакета и цена захода

Как проверить в нашей лаборатории, что программа стоит на пути пакетов, и сколько стоит один заход к ней?

Выдуманная картинка: у здания две камеры, одна на входной двери, другая на выходной, окошко клерка посередине. Входящее письмо камера у входа снимет раньше клерка, а исходящее камера у выхода снимет позже.

В лаборатории камера это запись пакетов на сетевом интерфейсе клиента, он называется c0, «цэ-ноль». По общим сведениям, она видит входящие пакеты до правил ядра, а исходящие после. Сама программа тоже записывает в файл каждый пакет, который до неё дошёл, и ставит время. Значит, у пакета два времени, и их можно сравнить.

Возьмём опыт t10, «тэ-один-ноль», где запрос режется на две части. Входящий SYN-ACK сервера интерфейс записал раньше программы: пакет сначала пришёл на интерфейс, потом попал к программе. Исходящий SYN клиента наоборот: программа записала его раньше, интерфейс позже.

Так мы сравнили все пакеты, которые удалось сопоставить. В t01 это десять пакетов. В t10 программа получила одиннадцать, но один, наш запрос, выброшен и сам на проводе не появляется, поэтому сопоставимых десять. Десять и десять будет двадцать, и в двадцати случаях из двадцати порядок такой, как должен быть. Он согласуется с тем, что программа стоит на пути пакетов, а прямо это показывает выброшенный запрос, о нём дальше.

Теперь выброшенный запрос. В t10 программа получила шесть исходящих. Один, запрос, выброшен, остаётся пять. Программа вставила две части запроса, сто четыре и сто сорок шесть байт: пять и два будет семь. На интерфейсе клиента ровно семь исходящих, сходится. Входящих программа получила пять, и на интерфейсе пять: их она не выбрасывала и не добавляла. Как так получается, что данные не теряются, разберём во второй и третьей частях.

Порядок записей и счёт пакетов Как в нашей лаборатории проверено, что программа стоит на пути пакетов. Запись на интерфейсе клиента c0 видит входящий пакет раньше программы, а исходящий позже: в опыте t10 входящий SYN-ACK сначала записан интерфейсом, потом программой, исходящий SYN сначала программой, потом интерфейсом. Так во всех сопоставимых пакетах: 10 из 10 в опыте pass (t01) и 10 из 10 в t10 (11 пакетов минус один выброшенный запрос), всего 20 из 20. Счёт в t10: в очереди 6 исходящих, минус 1 выброшенный запрос, плюс 2 вставленные части (104 и 146 байт), получается 7 исходящих на интерфейсе; входящих 5 в очереди и 5 на интерфейсе. В опыте pass в очереди и на интерфейсе по 6 исходящих и 4 входящих. Лаборатория: где записан пакет и сколько пакетов прошло Входящий: SYN-ACK сервера (опыт t10) запись интерфейса c01 запись программы nfqws22 на интерфейс пришёл раньше,чем к программе Исходящий: SYN клиента (опыт t10) запись программы nfqws21 запись интерфейса c02 программа записала раньше,интерфейс позже 20 из 20 опыт pass (t01): 10 пакетов из 10 опыт t10: 11 пакетов − 1 выброшенный запрос = 10 из 10 10 + 10 = 20, исключений нет Порядок согласуется с тем, что программа стоит на пути; прямо это показывает выброшенный запрос. Счёт пакетов в t10 (в нашем тесте) ИСХОДЯЩИЕ 6исходящих в очереди − 1выброшен: наш запрос + 2вставлены: 104 и 146 байт = 7исходящих на c0 ВХОДЯЩИЕ 5входящих в очереди = 5входящих на c0 их программа не выбрасывала и не добавляла Опыт pass (t01): в очереди 6 исходящих и 4 входящих, на интерфейсе c0 те же 6 и 4,ничего не вычитали и не добавляли. Выброшенный запрос на провод как есть не вышел, вышли две его части.

Теперь цена. В первых прогонах задержки получились слишком большими из-за медленного диска, на который писались журнал и файл с пакетами; какой из двух файлов виноват, не разделяли. Тех чисел мы не называем.

Мы смотрели на пару пакетов: входящий SYN-ACK и исходящее подтверждение. Без программы, опыт t00, «тэ-ноль-ноль», в трёх сохранённых прогонах от прихода SYN-ACK до ухода подтверждения проходит от двенадцати до пятнадцати тысячных миллисекунды; в новых шести повторах было от одиннадцати до двадцати. С программой, опыт t01: в нашем тесте шесть повторов на локальном диске дали от двухсот шестидесяти четырёх до пятисот пятидесяти пяти тысячных. В сохранённом прогоне та же пара занимала в сотню и более раз дольше, чем в повторах, а менялась только папка вывода.

Вычтем по медианам. Около двухсот восьмидесяти тысячных с программой, около тринадцати без неё: разность около двухсот семидесяти, в пяти типичных прогонах от двухсот пятидесяти до трёхсот, в худшем около пятисот сорока. В паре два захода к клерку. Если считать их равными, а это наше допущение, получится порядка ста тридцати микросекунд на заход, в типичных прогонах от ста двадцати пяти до ста пятидесяти. Заход здесь это весь крюк: вынос пакета, работа программы с записью журнала и файла пакетов, возврат вердикта. Наша оценка, с включённым журналом.

Цена одного захода к программе Сколько стоит один заход пакета к программе, замер на нашем компьютере (WSL2, шесть повторов на локальном диске). Время от прихода SYN-ACK до ухода подтверждения ACK на интерфейсе клиента: без программы (опыт t00) от 0,011 до 0,020 мс, медиана 0,013; с программой (опыт t01) 0,278, 0,283, 0,314, 0,555, 0,269 и 0,264 мс, медиана около 0,28, выброс 0,555. Разность медиан около 0,27 мс на пару пакетов, в паре два захода; если они равны (допущение), около 0,13 мс, то есть порядка 130 микросекунд на заход, в типичных прогонах от 126 до 150, в худшем около 270. Это весь крюк с включённым журналом. Первые замеры на медленном диске дали слишком большие задержки и не используются. Один компьютер, ориентир, настоящее железо не измеряли. Цена одного захода к программе (в нашем тесте) От прихода SYN-ACK до ухода ACK на интерфейсе c0, мс без программы (t00, шесть повторов): 0,011–0,020, медиана 0,013 с программой (t01, шесть повторов): 0,264–0,555, медиана около 0,28 00,10,20,30,40,50,6 выброс 0,555 0,28 − 0,013 ≈ 0,27 0,27 мс добавляет программа на пару пакетов: входящий SYN-ACK и исходящий ACK. В паре два захода. Если они равны (наше допущение): 0,27 ÷ 2 ≈ 0,13 мс на заход. Пять типичных прогонов: 0,25–0,30 на пару, то есть 0,126–0,150 мс на заход. Худший: 0,54 и около 0,27. ≈ 130 мкс на заходнаша оценка: весь крюк с журналом,вынос пакета, работа программы, записьфайлов и возврат вердикта Первые прогонызадержки слишком большие: файлы писалисьна медленный диск. Виноват диск, не программа.Те числа мы не используем. Повтор на локальном дискешесть прогонов с программой и шестьбез неё: числа на этой картинке отсюда. WSL2 (Linux внутри Windows), один компьютер, шесть повторов: ориентир, а не закон.Настоящее железо, реальную сеть и большой поток пакетов мы не измеряли.

И осторожность: это один компьютер, WSL2, Linux внутри Windows, шесть прогонов повтора. Ориентир, а не закон. Настоящее железо, реальную сеть и большой поток пакетов мы не измеряли.

Проверь себя. Опыт t01, стратегия pass: программа получила шесть исходящих пакетов, ничего не выбросила и не вставила. Сколько исходящих на интерфейсе? Шесть: ничего не вычитается и не добавляется.

Итак, в нашем тесте входящий пакет интерфейс записывает раньше программы, а исходящий позже, и так для двадцати пакетов из двадцати. Выброшенный запрос на провод не вышел, вышли две его части. Один заход к программе, весь крюк с включённым журналом, стоит, по нашей оценке, порядка ста тридцати микросекунд, если входящий и исходящий заходы равны, на одном компьютере, это ориентир.

Правило nftables: таблица, цепочка, точка на пути

Теперь настоящие правила, которые стоят в ядре и выбирают письма для окошка. Мы берём те, что использует наша лаборатория. Они взяты из документации zapret2 с небольшими отличиями в имени таблицы, порядке строк и записи порта UDP, смысл тот же.

Сначала выдуманная картотека: папка с ярлыком, в папке листы, на листах строки. Одна строка это одна инструкция: если письмо такое-то, сделать с ним то-то.

Правила задаются в nftables, это современный способ настраивать фильтрацию пакетов в Linux. По общим сведениям, в ней папка называется таблицей, лист цепочкой, а строка правилом.

Цепочка это список правил, которые ядро проходит в одной определённой точке пути пакета.

Первая строка нашего набора создаёт таблицу с именем z, «зэт». По общим сведениям, слово inet значит, что она работает и для IPv4, и для IPv6, а имя произвольное.

На каком месте пути висит лист? Выдуманная картинка: пакет проходит несколько дверей, у первой он только что вошёл с улицы, у последней вот-вот выйдет. Лист вешают на любую. В netfilter такая дверь называется hook, «крючок», точка перехвата. Нам нужны две: prerouting, куда пакет попадает только что принятым, и postrouting, где исходящий пакет уже выбрал дорогу и вот-вот уйдёт в интерфейс. Для самого клиента исходящие идут через output к postrouting, входящие через prerouting к input. Это общие сведения о netfilter.

Вторая строка создаёт цепочку post на точке postrouting: исходящие пакеты, на выходе.

В этой же строке слово priority, приоритет. Выдуманный пример: на одной двери висят два листа, на одном написано «сто», на другом «сто один». Кто смотрит на письмо первым? Тот, у кого число меньше. Приоритет это число, по которому ядро упорядочивает цепочки на одной точке, чем меньше, тем раньше. Наше число сто один значит «чуть позже перевода адресов, у которого сто»; в лаборатории перевода адресов нет, число нужно лишь для порядка. Это общие сведения.

Следующая цепочка, pre, висит на точке prerouting с приоритетом минус сто один. По общим сведениям, это после отслеживания соединений, которое нам скоро понадобится, но до перевода адресов назначения. Главное, что эта цепочка смотрит на только что пришедшие пакеты.

Набор правил: таблица и две цепочки Набор правил nftables нашей лаборатории, девять строк (пять правил в трёх цепочках), по смыслу как в docs/readme.md zapret2. Строка 1, nft add table inet z, создаёт таблицу z (inet: IPv4 и IPv6, по общим сведениям; имя произвольное). Строка 2 создаёт цепочку post на точке postrouting с приоритетом 101: исходящие пакеты. Строка 4 создаёт цепочку pre на точке prerouting с приоритетом −101: только что пришедшие пакеты. Справа дерево: таблица z, в ней цепочки post и pre, в цепочках строки-правила; цепочка predefrag разбирается позже. Набор правил нашей лаборатории (nftables) 1nft add table inet z2nft add chain inet z post '{type filter hook postrouting priority 101;}'3nft add rule inet z post … tcp dport '{80,443}' … queue num 200 bypass4nft add chain inet z pre '{type filter hook prerouting priority -101;}'5nft add rule inet z pre … tcp sport '{80,443}' … queue num 200 bypass6nft add rule inet z post … udp dport 443 … queue num 200 bypass7nft add rule inet z pre … udp sport 443 … queue num 200 bypass8nft add chain inet z predefrag '{type filter hook output priority -401;}'9nft add rule inet z predefrag 'mark & 0x40000000 != 0x00000000 notrack' 1nft add table inet z 2nft add chain inet z post '{type filter hook postrouting priority 101;}' 4nft add chain inet z pre '{type filter hook prerouting priority -101;}' … середина правил пропущена; строки 8–9 (predefrag) разберём позже таблица zinet: IPv4 и IPv6 цепочка postpostrouting, 101правилаtcp, udp цепочка preprerouting, −101правилаtcp, udp 1строка 1: создаёт таблицу z. inet значит IPv4 и IPv6 (по общим сведениям), имя произвольное. 2строка 2: создаёт цепочку post на точке postrouting: исходящие пакеты, на выходе. 4строка 4: цепочка pre на точке prerouting, приоритет −101: только что пришедшие пакеты.

Согласуется с этим журнал программы: у исходящих пакетов указан выходной интерфейс c0 и не указан входной, а у входящих наоборот.

Две цепочки на пути пакета Где на пути пакета висят две цепочки. Слева сеть, дверь prerouting (здесь висит цепочка pre с приоритетом −101: пакет только что принят), посередине выбор дороги, дверь postrouting (цепочка post с приоритетом 101: исходящий пакет вот-вот уйдёт в интерфейс c0). Для самого клиента исходящие идут от программы через output к postrouting и в c0, входящие из c0 через prerouting к input. Приоритет, по общим сведениям: чем меньше число, тем раньше; −101 после отслеживания соединений (−200) и до перевода адресов назначения (−100), 101 чуть позже перевода адресов источника (100); в лаборатории перевода адресов нет. Журнал программы не называет цепочку, её выводят по интерфейсам: у исходящего пакета id=1 указан только выходной интерфейс ifout=c0(3), у входящего id=2 только входной ifin=c0(3). Где висят цепочки на пути пакета схема, не масштаб сетьвыбор дорогив интерфейс preroutingпакет только что пришёл postroutingпакет вот-вот уйдёт pre · приоритет −101 post · приоритет 101 Для самого клиентаисходящиевходящие программаoutputpostroutingc0 c0preroutinginputпрограмма NAT в лабораториинет Приоритет: у кого число меньше, тот раньше (по общим сведениям) −200отслеживание соединений −101pre −100перевод адресов назначения 100перевод адресов источника 101post меньше — раньше В лаборатории перевода адресов нет: число 101 нужно только для порядка. packet: id=1 … ifin=(0) ifout=c0(3)исходящий: только выходной интерфейс c0 → цепочка post packet: id=2 … ifin=c0(3) ifout=(0)входящий: только входной интерфейс c0 → цепочка pre Журнал не пишет имя цепочки: её выводят по интерфейсам.

Проверь себя. Пакет только что пришёл снаружи: какую цепочку он проходит? Цепочку pre, потому что она на точке prerouting, и в журнале у него указан входной интерфейс c0.

Итак, правила лежат в таблице, таблица состоит из цепочек, цепочка висит на точке пути пакета, и у неё есть приоритет. У нас две цепочки: post для исходящих и pre для входящих.

Правило слева направо: метка и порты

Теперь само правило, слева направо. Строка длинная, поэтому в этой сцене только первые две части, остальные дальше. Первая часть: meta mark and ноль-икс четыре и семь нулей равно нулю.

Сначала зачем это нужно, на выдуманном примере. Клерк тоже отправляет письма, и мы клеим на них красную наклейку «своё». Правило говорит: письма с красной наклейкой к окошку не возить. Иначе, по документации, клерк получит собственное письмо, отправит снова, получит снова, и так по кругу. О петле подробно в третьей части, сейчас важно, что наклейка выбирает письма.

Как читать такие числа. Во втором выпуске мы говорили о шестнадцатеричной записи, но приставку не объясняли. Приставка «ноль-икс» значит, что дальше идёт шестнадцатеричная запись, и цифры мы читаем по одной: ноль-икс, четыре и семь нулей.

Что это за число? В нём восемь цифр, каждая это четыре бита, всего тридцать два бита.

Старшая цифра, четвёрка, в двоичной записи ноль, один, ноль, ноль, и единица стоит на месте номер тридцать, если считать справа налево от нуля. Остальные цифры нули. Значит, единица стоит в одном-единственном месте: тридцатый бит.

Слово mark, метка. По общим сведениям, это число, которое ядро хранит рядом с пакетом, в памяти. В самих байтах пакета, которые уходят в сеть, его нет.

Слово and, побитовое «и». Представим трафарет, лист бумаги с одним окошком: он лежит на ленте из тридцати двух клеток, и видна только клетка номер тридцать. «Метка and ноль-икс четыре и семь нулей» значит «оставить только тридцатый бит», а потом правило проверяет, равен ли результат нулю.

Два примера. Метка ноль, в нашем журнале восемь нулей: оставили тридцатый бит, получили ноль, правило подходит, идём дальше. Метка ноль-икс четыре и семь нулей: оставили тридцатый бит, получили то же число, не ноль, правило не подходит, пакет идёт мимо окошка. Это пакеты самой программы, у них этот бит стоит.

Проверь себя. Метка равна единице, стоит только самый младший бит. Подойдёт ли пакет под эту часть правила? Тридцатый бит не стоит, трафарет его не видит, результат ноль, значит, подходит.

Метка пакета и бит 30 Метка пакета и трафарет: число 0x40000000 это 8 шестнадцатеричных цифр по 4 бита, 32 бита; старшая цифра 4 это 0100, единица стоит на месте 30, если считать справа налево с нуля; «mark and 0x40000000» оставляет только бит 30, и правило подходит, если результат равен нулю: метка 0x00000000 даёт 0 (идём дальше), метка 0x40000000 даёт 0x40000000 (не подходит, это пакет самой программы), метка 0x00000001 даёт 0 (идём дальше). метка пакета и трафарет: оставить только бит 30 0x40000000приставка 0x: дальше шестнадцатеричная запись, 8 цифр по 4 бита = 32 бита 40000000 01000000000000000000000000000000 313029282726252423222120191817161514131211109876543210 номера битов справа налево от нуля до тридцати одного старшая цифра 4 в двоичной записи: 0100 1единица стоит ровно на одном месте: бит 30. Остальные 31 бит нули. mark and 0x40000000: трафарет с одним окошком над лентой, видна только клетка 30.Потом правило сравнивает результат с нулём. меткав окошкерезультат and== 0 ?правило 0x00000000бит 30 = 00x00000000даидём дальше 0x40000000бит 30 = 10x40000000нетне подходит: пакет мимо окошка 0x00000001бит 30 = 00x00000000даидём дальше (проверь себя) метка лежит в памяти ядра рядом с пакетом, в байтах пакета её нет; бит 30 стоит у пакетов самой программы документация: docs/readme.md:83; значение метки по умолчанию: nfq2/desync.h:15

Вторая часть: tcp dport, фигурные скобки, восемьдесят и четыреста сорок три. Это пакеты TCP, у которых порт назначения восемьдесят или четыреста сорок три. Порты мы разбирали во втором выпуске, но порт восемьдесят там не объяснялся: это стандартный порт обычного HTTP, где запрос идёт открытым текстом. А четыреста сорок три порт защищённых соединений. По общим сведениям, фигурные скобки значат «любой из списка». Наш клиент ходит на четыреста сорок три в опытах с TLS и на восемьдесят в опытах с HTTP, а порт восемь тысяч восемьдесят не подойдёт: его нет в списке.

Правило ядра: метка и порты Правило ядра nftables из docs/readme.md:83, первые две части слева направо: «meta mark and 0x40000000 == 0» оставляет пакеты, у которых в метке не стоит бит 30 (игрушка: письма с красной наклейкой «своё» к окошку не возят, иначе клерк получил бы собственное письмо), «tcp dport {80,443}» оставляет пакеты TCP с портом назначения 80 или 443, а порт 8080 не подходит; остальные части правила разобраны дальше. правило ядра слева направо: первые две части строка из документации zapret2, docs/readme.md:83 (в лаборатории то же правило) meta mark and 0x40000000 == 0 tcp dport {80,443} ct original packets 1-12 queue num 200 bypass эти две части разберём в следующих сценах 1 · метка часть 1: метка игрушка (выдумана): клерк клеит наклейку «своё» обычное письмо окошковезём письмо с наклейкой «своё» к окошку не везём Иначе, по документации, клерк получил бы своё письмо,отправил снова, и так по кругу (подробно в части 3). 2 · порт часть 2: порт назначения порт назначения; фигурные скобки: любой из списка порт 443в списке: подходит порт 80в списке: подходит порт 8080нет в списке: мимо 80: обычный HTTP, запрос идёт открытым текстом 443: защищённые соединения (TLS) наш клиент: 443 в опытах с TLS, 80 в опытах с HTTP Итог двух частей: пакет без нашего бита в метке, TCP, порт назначения 80 или 443.

Итак, две первые части правила. Метка: оставляем тридцатый бит, и если получился ноль, идём дальше. Порт: восемьдесят или четыреста сорок три.

Правило слева направо: очередь, bypass и весь набор

В правиле остались слова queue num двести и bypass; ограничитель ct original packets оставим на следующую сцену.

Queue num двести: это само действие, отправить пакет в очередь с номером двести и ждать вердикта. Программа запускается с параметром qnum равно двести, и числа обязаны совпасть. В журнале программы это строка binding this socket to queue двести: «привязываю этот сокет к очереди двести».

Теперь bypass. Выдуманная картинка: что будет, если клерк не пришёл и в комнате у окошка никого нет? Можно выбросить письма, если ждать некому, а можно пропустить их дальше, как будто окошка нет. Табличка на двери, bypass, включает второе. Здесь это слово значит не обход блокировок, а «пропусти, если некому»: по общим сведениям о netfilter, пакет идёт дальше.

В нашем тесте это видно по опыту t00: правила стоят, nfqws2 не запущен, и соединение работает, десять пакетов TCP проходят, клиент соединился за три десятых миллисекунды. А что без bypass пакеты выбрасываются, показал наш тест на UDP, где очередь никто не слушает: с bypass два пакета из двух дошли, без него ни один.

У bypass есть обратная сторона. Если в правиле написано двести, а программа запущена с номером триста, ядро кладёт пакеты в очередь двести, где никого нет, и bypass пропускает их дальше. Всё работает, программа не получает ничего, и сообщения об ошибке нет. Это наш вывод из смысла слова bypass, опыта с неверным номером у нас не было.

Очередь 200 и bypass Действие «queue num 200 bypass»: правило отправляет пакет в очередь 200, и программа должна быть запущена с --qnum=200 (в журнале: binding this socket to queue '200', nfq2/nfqws.c:391); если очередь никто не слушает, bypass пропускает пакет дальше (здесь это не обход блокировок, а «пропусти, если некому»), без bypass пакет выбрасывается: в опыте с остановленной программой десять пакетов TCP прошли и клиент соединился за 0,3 мс, а в тесте на UDP с bypass дошло 2 из 2, без него 0 из 2; если в правиле 200, а программа запущена с 300, пакеты идут мимо неё без сообщения об ошибке (вывод из смысла bypass, опыта с неверным номером нет). действие правила: queue num 200 bypass правило: queue num 200=программа: nfqws2 --qnum=200 журнал программы:binding this socket to queue '200'(nfq2/nfqws.c:391) bypass: не обход блокировок,а «пропусти, если некому» что ядро делает с пакетом, который попал в очередь 200 1 · клерк на местепакеткомнатаnfqws2вердиктпакет едет в комнату,программа выдаёт вердикт 2 · пусто, табличка bypassпакеткомнатапустотабличка bypass: пакет едетдальше, будто окошка нет 3 · пусто, таблички нетпакеткомнатапустобез таблички пакет падаетв корзину: выброшен опыт t00: правила стоят, nfqws2 не запущен10 пакетов TCP проходят, клиент соединилсяза 0,3 мс: bypass пропускает пакеты наш тест на UDP: очередь без слушателяс bypass дошло 2 из 2, без bypass 0 из 2TCP так отдельно не проверяли обратная сторона bypass: номера обязаны совпасть правило: 200≠программа: 300 пакеты уходят в пустую очередь 200,bypass пропускает их дальше всё работает, программа ничего не получает, об ошибке никто не сообщит это наш вывод из смысла bypass, опыта с неверным номером у нас не было

Теперь весь набор: пять правил в трёх цепочках. В цепочке post два правила: TCP на порты восемьдесят и четыреста сорок три, то, что мы разобрали, и UDP на порт четыреста сорок три, это протокол QUIC, о нём в шестом выпуске. В цепочке pre такие же два для входящих: порт источника вместо порта назначения, потому что ответы идут с порта сервера, и ct reply вместо ct original, тот же ограничитель для ответного направления.

Пятое правило стоит в третьей цепочке, predefrag, её имя читается «до сборки фрагментов», на точке output с приоритетом минус четыреста один. Output, по общим сведениям, это место, где пакет только что создан на этом самом компьютере. Правило одно: если на пакете стоит наш бит, то notrack, «не отслеживать». По общим сведениям, такой пакет в таблицу отслеживания соединений ядра не попадёт. Это вторая защита от петли, о ней в третьей части.

В нашем тесте на UDP хватало одной проверки метки, и одного notrack тоже хватало. Какая защита действовала в основных опытах, по журналам сказать нельзя.

И последняя строка набора включает «либеральную проверку» в системе отслеживания соединений, это параметр ядра nf_conntrack_tcp_be_liberal, его ставят в единицу. В комментарии к скриптам автор пишет, что российский DPI присылает сброс соединения, пакет с флагами RST и ACK, с неправильным номером подтверждения, ядро иногда считает такой пакет недопустимым, и тогда правило с ограничителем не передаёт его программе; либеральная проверка нужна, чтобы такие входящие сбросы всё же доходили. Это утверждение автора о настоящем оборудовании. Мы его не проверяли и как факт не повторяем.

Пять правил в трёх цепочках Набор из пяти правил в трёх цепочках (по docs/readme.md:82-92): в post (postrouting, приоритет 101) правила на исходящие TCP с портом назначения 80 и 443 и UDP с портом 443; в pre (prerouting, приоритет -101) такие же на входящие с портом источника; в predefrag (output, приоритет -401) одно правило: пакет с битом метки 0x40000000 не отслеживать (notrack), это вторая защита от петли; последняя строка набора включает nf_conntrack_tcp_be_liberal, по объяснению автора скриптов ради входящих сбросов RST,ACK с неправильным номером подтверждения (common/linux_fw.sh:21-25), мы это не проверяли. набор: пять правил в трёх цепочках по docs/readme.md:82-92; лаборатория ставит те же правила (порт UDP записан без скобок) цепочкаправило после «meta mark and 0x40000000 == 0»кого ловитстр. posttcp dport {80,443} ct original packets 1-12 queue num 200 bypassисходящие TCP83 pretcp sport {80,443} ct reply packets 1-12 queue num 200 bypassвходящие TCP88 postudp dport {443} ct original packets 1-12 queue num 200 bypassисходящие UDP (QUIC)84 preudp sport {443} ct reply packets 1-12 queue num 200 bypassвходящие UDP89 predefragmark & 0x40000000 != 0x00000000 notrackсобственные пакеты92 во входящих: порт источника вместо назначения (ответы идут с порта сервера) и ct reply вместо ct original где стоит каждая цепочка на пути пакета prepreroutingприоритет −101пакет только что пришёл снаружи postpostroutingприоритет 101пакет уходит в интерфейс predefragoutputприоритет −401пакет создан на этом компьютере меньше приоритет, раньше срабатывает цепочка; predefrag читается как «до сборки фрагментов» (по названию) защита 1: проверка метки в правилахmark and 0x40000000 == 0: пакет с нашимбитом правило пропускает мимо окошка защита 2: notrack в predefragтакой пакет, по общим сведениям, не попадётв таблицу отслеживания соединений ядра в нашем тесте на UDP каждой защиты хватало и по отдельности;какая действовала в основных опытах, по журналам сказать нельзя последняя строка набора: nf_conntrack_tcp_be_liberal = 1 (docs/readme.md:86)автор скриптов пишет (common/linux_fw.sh:21-25): DPI присылает RST,ACKс неправильным номером подтверждения; ядро иногда считает такой пакет недопустимым,а либеральная проверка нужна, чтобы такие сбросы доходили до программы.это утверждение автора о настоящем оборудовании: мы его не проверяли и как факт не повторяем проверь себя: правил пять, цепочек три; notrack стоит в цепочке predefrag

Проверь себя. Сколько правил в нашем наборе и сколько цепочек? Пять правил, три цепочки. Где стоит notrack? В цепочке predefrag.

Итак, queue num двести отправляет пакет в очередь, а bypass говорит: если никто не слушает, пропусти. Номер в правиле и в параметре программы обязан совпасть, иначе, по нашему выводу, программа ничего не получит, а соединение продолжит работать; опыта с неверным номером не было. Набор: пять правил в трёх цепочках, исходящая сторона, входящая и цепочка для собственных пакетов программы.

Счётчик первых пакетов: что считает ядро и зачем предел

В правиле осталась одна непрочитанная часть: ct original packets, от одного до двенадцати. Что она считает и зачем нужна?

Сначала выдуманный клуб. У входа турникет со счётчиком, а охранник говорит: к стойке регистрации идут только первые трое гостей, остальные проходят мимо. Первый гость это номер один, четвёртый идёт мимо. Считается и сам гость: первого тоже посчитали.

Теперь без картинки. По общим сведениям, ct значит conntrack, «отслеживание соединений в ядре»: таблица, где ядро хранит известные ему соединения и счётчики пакетов по каждому, одна строка на соединение.

У соединения два направления. Исходное, original, это то, в котором ушёл первый пакет соединения, а ответное, reply, обратное. У нас первым уходит SYN клиента, значит, исходное направление от клиента к серверу, а для каждого направления ядро ведёт свой счётчик.

Исходное и ответное значат «по первому пакету», а не «наружу» и «внутрь». Для клиента исходное исходящее, а для сервера то же направление входящее, поэтому в лаборатории правила на сервере зеркальные.

Условие «ct original packets от одного до двенадцати» значит: счётчик исходного направления сейчас в пределах от одного до двенадцати. Считаются пакеты, а не байты: пустое подтверждение и пакет с двумястами пятьюдесятью байтами данных равны, по одному.

Пройдём по началу опыта t10. Клиент отправляет SYN: до него клиент не отправлял ничего, это первый пакет исходного направления, счётчик единица. Проверяемый пакет уже входит в счёт, и единица подходит под «от одного до двенадцати». Так устроено по общим сведениям, и наш опыт дальше это подтверждает. В очереди это номер один.

Сервер отвечает SYN-ACK. Это ответное направление, там счётчик был нулём и стал единицей, а исходный остался единицей: направления считаются независимо. В очереди это номер два.

Клиент отправляет ACK. Он идёт в исходном направлении, счётчик становится двойкой, в очереди номер три.

Четвёртый пакет, наш запрос с двумястами пятьюдесятью байтами. До него клиент отправил два пакета, SYN и ACK, значит, запрос третий в исходном направлении, счётчик три. Но в очереди у него номер четыре: номера в очереди общие для обоих направлений, и между SYN и ACK клиента в неё попал входящий SYN-ACK.

Целиком t10: в очередь прошло шесть исходящих и пять входящих, шесть и пять будет одиннадцать, каждое направление ниже двенадцати, и в опытах t01 и t10 в очередь попало всё соединение, кроме пакетов, которые программа вставила сама.

И предупреждение. Мы говорим о таблице в ядре. У самой программы тоже есть память о соединениях, о ней в пятом выпуске, и считает она отдельно и по-своему.

Счётчики conntrack на примере t10 Условие «ct original packets 1-12»: ядро (conntrack) хранит для каждого соединения два счётчика пакетов, исходного направления (в котором ушёл первый пакет, у клиента это исходящее) и ответного, и считает пакеты, а не байты, включая сам проверяемый пакет; в игрушке с турникетом (предел три) первых трёх гостей ведут к стойке, четвёртый идёт мимо; в начале опыта t10 SYN клиента даёт исходное 1 (в очереди номер 1), SYN-ACK сервера ответное 1 (номер 2), ACK исходное 2 (номер 3), запрос в 250 байт исходное 3, но номер в очереди 4, потому что номера общие для обоих направлений; весь опыт: 6 исходящих и 5 входящих в очереди, 6 + 5 = 11. что считает ядро: ct original packets 1-12 игрушка (выдумана): охранник у турникета пускает к стойке только первых трёх гостей (предел три) 1к стойке 2к стойке 3к стойке 4мимо 5мимо считается и сам гость: первого тоже посчитали,первый это номер один, а четвёртый идёт мимо таблица conntrack в ядре (по общим сведениям): у каждого соединения два счётчика клиентсервер исходное (original): так ушёл первый пакет, SYN ответное (reply): в обратную сторону исходное: 0 исходное: 1 исходное: 2 исходное: 3 ответное: 0 ответное: 1 считаются пакеты, а не байты: пустой ACK и пакет с 250 байтами данных равны, по одному клиент: исходное = исходящее; сервер: исходное = входящее (поэтому правила на сервере зеркальные) номер в очередипакетнаправлениеисходноеответноеитог 1SYNклиент → сервер10в очередь 2SYN-ACKсервер → клиент11в очередь 3ACKклиент → сервер21в очередь 4запрос, 250 байт данныхклиент → сервер31в очередь запрос это третий исходящий пакет клиента, но в очереди он четвёртый:номера общие для обоих направлений, и между ними пришёл SYN-ACK весь опыт t10: в очередь прошло 6 исходящих и 5 входящих, 6 + 5 = 11, каждое направление ниже 12 таблица ядра и память самой программы это разные таблицы: у программы свой счёт

Зачем нужен предел? Причин три. Первая, стоимость: документация говорит о значительных накладных расходах на перенос пакетов, а в руководстве сказано, что число перехваченных начальных пакетов подбирается под стратегию, и избыточный перехват создаёт лишнюю нагрузку.

Вторая, что нужно программе: для нашего игрушечного инспектора и наших стратегий всё нужное, имя сайта и рукопожатие, лежит в самом начале соединения, а страница, картинки и видео ей ни к чему.

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

Проверь себя. Стратегии нужен пятнадцатый исходящий пакет, а предел двенадцать. Увидит ли программа этот пакет? Нет: пятнадцатый не подойдёт под «от одного до двенадцати», ядро пропустит его мимо очереди, и программа не получит его вообще.

Три причины предела Зачем нужен предел в двенадцать пакетов: выносить пакеты из ядра в программу дорого (руководство: избыточный перехват создаёт лишнюю нагрузку на процессор, число подбирается под стратегию, docs/manual.en.md:393); нужное стратегиям, имя сайта и рукопожатие, лежит в начале соединения; программа увеличивает свои счётчики только на пакеты, которые получила (nfq2/conntrack.c:174-188), поэтому пакеты сверх предела для неё не существуют: в условном примере стратегии нужен 15-й исходящий пакет, а ядро пускает в очередь только первые 12, остальные идут мимо программы. зачем предел: три причины 1 · это дорогоядропрограммакаждый пакетруководство: лишний перехватсоздаёт дополнительную нагрузкуна процессор; число подбираютпод стратегиюdocs/manual.en.md:393 2 · нужное лежит в началеначаловсё остальноеигрушечному инспектору и нашимстратегиям нужны имя сайта ирукопожатие, они идут первымистраница и видео им не нужны 3 · программа видит своё123456полученымимосчётчики программы растут толькона пакеты, которые она получила(ConntrackFeedPacket)nfq2/conntrack.c:174-188 после двенадцатого пакета стратегия слепа (условный пример, не опыт) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ядро пускает в очередь: пакеты 1-12 идут мимо программы исходящие пакеты клиента по порядку; предел двенадцать проверь себя: стратегии нужен пятнадцатый исходящий пакет,предел двенадцать. Увидит ли программа этот пакет?Нет: пятнадцатый не подходит под «от одного до двенадцати», ядро пропускает егомимо очереди, и программа не получает его вообще.

Итак, ядро считает пакеты каждого направления отдельно, считая и сам проверяемый пакет, и пускает в очередь, пока счётчик в пределах от одного до двенадцати. Предел нужен, потому что вынос дорог, нужное лежит в начале соединения, а программа видит только то, что ей дали.

Опыт t20: где предел кончается, обгон и откуда число двенадцать

Теперь опыт, где предел действительно кончается и это видно в записи.

Он называется t20, «тэ-два-ноль». Программа вставляет поддельный пакет без защиты, то есть с ненастоящими данными, которые сервер в нашем опыте принимает за настоящие. Поэтому ответа клиент так и не получает, повторяет отправку снова и снова, и поток получается длинным, как раз чтобы дойти до предела.

Что показывает запись. На интерфейс клиента пришло двадцать четыре входящих пакета, а программа получила двенадцать, и не какие-нибудь, а ровно первые двенадцать. Двадцать четыре минус двенадцать будет двенадцать пакетов мимо, и двенадцатым в очереди оказалось подтверждение, ACK.

Исходящие. На интерфейсе клиента девятнадцать исходящих, но шесть из них вставила сама программа. Девятнадцать минус шесть будет тринадцать собственных пакетов клиента. В очередь попало двенадцать, значит, по порядку создания, это видно по идентификаторам IP, тринадцатым был ACK, он и прошёл мимо.

Почему последним в очередь попал именно двенадцатый пакет, а не тринадцатый? Правило говорит «от одного», а SYN, первый пакет, в очередь попал. Значит, в тот момент счёт был единицей, а не нулём: пакет считает сам себя.

Опыт t20: кадры и очередь Опыт t20: программа вставляет поддельные пакеты без защиты, сервер принимает их как данные, ответа нет (reply: TIMEOUT), клиент повторяет отправку, и поток доходит до предела; на интерфейс клиента пришло 24 входящих кадра, в очередь попали ровно первые 12 (24 − 12 = 12 мимо, двенадцатым был ACK), а из 19 исходящих 6 вставила сама программа (19 − 6 = 13 собственных), в очередь попало 12, тринадцатый по порядку создания (по IP id), ACK с IP id 6842, прошёл мимо (13 − 12 = 1); SYN попал в очередь, значит пакет считает сам себя. опыт t20: сколько кадров попало в очередь клиентокошко, клерк (nfqws2)роутерсервер fake fake программа вставляет поддельные пакеты без защиты (оранжевые): сервер принимает их как данные,ответа нет (reply: TIMEOUT), клиент повторяет отправку снова и снова, и поток получается длинным входящие кадры на интерфейсе клиента: 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 в очереди: первые 12мимо: 24 − 12 = 12 двенадцатый входящий в очереди это ACK; следующие входящие кадры в очередь уже не идут исходящие кадры на интерфейсе клиента: 19, из них 6 вставила программа свои кадры клиента, в порядке создания (по IP id): 13 1 2 3 4 5 6 7 8 9 10 11 12 13 вставлены программой: 6 1 2 3 4 5 6 в очереди: 12 красным: 13-й по порядку создания, ACK с IP id 6842, мимо очереди 19 − 6 = 13 собственных13 − 12 = 1 мимо очереди по времени на проводе этот ACK двенадцатый, «тринадцатый» он только по порядку создания; это вывод по IP id почему ровно двенадцать, а не одиннадцать и не тринадцатьправило говорит «от одного», и SYN, самый первый пакет, в очередь попал: в этот момент счёт был единицей,а не нулём. Пакет считает сам себя, поэтому предел двенадцать пустил в очередь ровно двенадцать. в журнале опыта 24 номера очереди; счётчики двух направлений независимы

Теперь два независимых счётчика, по номерам очереди. Входящий предел набрался раньше: двенадцатым входящим в очередь попал пакет с номером девятнадцать. Но после него в очередь попали ещё пять пакетов, с двадцатого по двадцать четвёртый, и все исходящие. Исходящий счётчик добрался до своих двенадцати только на номере двадцать четыре.

Это не случайность одного прогона. Те же двенадцать и двенадцать мы видели ещё в пяти опытах: t29 и t29c, «тэ-два-девять» и «тэ-два-девять-цэ», и трёх опытах с IPv6, «вэ-один-три», «вэ-один-пять» и «вэ-один-шесть». А во всех девяноста двух журналах нет ни одного, где в одну сторону было бы больше двенадцати.

Есть следствие. Пакеты сверх предела идут мимо программы целиком, поэтому могут обогнать пакеты, которые она ещё держит. Выдуманный пример: гость, которому регистрация не нужна, проходит мимо стойки и входит в зал раньше того, кто стоит у клерка.

В t20 так и случилось: подтверждение, не попавшее в очередь, ушло в сеть раньше повторного запроса, который клиент создал перед ним и который ждал в очереди. У подтверждения идентификатор в заголовке IP шесть тысяч восемьсот сорок два, у запроса шесть тысяч восемьсот сорок один. По общим сведениям, идентификаторы выдаются в порядке отправки, так что запрос создан раньше, а ушёл позже. Это наш вывод по идентификаторам, а сколько прошло времени между ними, мы не называем: прогон со старым медленным диском.

Теперь о числе. Двенадцать это число из документации и нашей лаборатории, но не значение по умолчанию. В файле настроек по умолчанию, config.default, которым пользуются готовые скрипты установки, для TCP двадцать исходящих и десять входящих, для UDP пять и три.

В примере из руководства пятнадцать и пятнадцать. Это разные назначения, и единственно верного числа в коде нет. В руководстве есть и отдельные правила без ограничителя для пакетов FIN и RST, а в нашем лабораторном наборе их нет.

Опыт t20: номера очереди и обгон Очередь опыта t20: номера 1-24 общие для обоих направлений; входящие заняли номера 2, 5, 6, 7, 9, 11, 12, 13, 15, 16, 17, 19, исходящие 1, 3, 4, 8, 10, 14, 18 и 20-24; входящий предел набрался на номере 19, после него пять исходящих подряд (24 − 19 = 5), исходящий добрался до двенадцати на номере 24; то же 12 и 12 в пяти других опытах, в 92 журналах больше 12 в одну сторону не было. Пакет сверх предела идёт мимо программы и может обогнать тот, что она держит: ACK с IP id 6842 не был в очереди и ушёл раньше повторного запроса с IP id 6841 (вывод по идентификаторам). Число 12 берётся из документации и лаборатории, config.default (config.default:74-77) даёт TCP 20 исходящих и 10 входящих, UDP 5 и 3, пример руководства 15 и 15 (docs/manual.en.md:404-405): единственно верного числа в коде нет. опыт t20: общие номера очереди 1-24 и два независимых счётчика каждому пакету, попавшему в очередь, ядро даёт общий номер; закрашены номера, которые заняло направление вх.исх. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 рамка у 19: 12-й входящий (ACK), входящий предел исчерпанрамка у 24: 12-й исходящий, предел исчерпан только здесь после 19 пять исходящих подряд:24 − 19 = 5 то же 12 и 12 ещё в пяти опытах: t29, t29c и трёх опытах с IPv6 (v13, v15, v16);во всех 92 журналах больше двенадцати в одну сторону не было пакет сверх предела идёт мимо программы и может обогнать тот, что она держит игрушка (выдумана): гость, которому регистрация не нужна, проходит мимо стойки и входит в зал раньше создан клиентом(порядок по IP id)ушёл в сеть(порядок на интерфейсе) повторный запросIP id 6841, стоит в очередиACKIP id 6842, не в очереди потом обгон ACKIP id 6842повторный запросIP id 6841 потом ACK создан позже, но не ждал программу и ушёл раньше. Это наш вывод по идентификаторам IP:по общим сведениям, они выдаются в порядке отправки. Сколько времени прошло между ними, не называем. откуда число двенадцать: у источников оно разное документация и лаборатория12 исходящих / 12 входящихdocs/readme.md:83, 88 файл настроек по умолчаниюTCP 20 / 10, UDP 5 / 3config.default:74-77 пример в руководстве15 / 15docs/manual.en.md:404-405 единственно верного числа в коде нет; в руководстве есть отдельные правила для FIN и RST без ограничителя (docs/manual.en.md:416, 422), в нашем лабораторном наборе их нет;для UDP предел в основных опытах не проверяли

Чего мы не проверяли. В основных опытах для UDP ограничитель не тестировали: QUIC давал в очереди по одному-два пакета. Отдельный тест на UDP в третьей части. И что стало бы с повторным запросом уже после предела, мы не видели: запись заканчивается раньше.

Проверь себя. В опыте t20 какое направление исчерпало предел первым и что попадало в очередь после этого? Первым входящее, его последний пакет в очереди номер девятнадцать. После него в очередь шли только исходящие.

Итак, предел считает проверяемый пакет вместе с предыдущими, поэтому в очередь попадает ровно двенадцать. У направлений свои счётчики, и во всех девяноста двух журналах больше двенадцати в одну сторону не было. Пакеты сверх предела идут мимо программы и могут обогнать те, что она держит. А число двенадцать не значение по умолчанию: у разных источников оно разное.

Итог части: что мы знаем и вопросы к следующей части

Подведём итог первой части.

Карточка маршрута пакета: итог первой части Растущая карточка маршрута пакета, итог первой части: ядро, окошко очереди и программа nfqws2 в соседней конторе; номер очереди от 0 до 65535 (в правиле queue num 200, в программе --qnum=200); три вердикта PASS, MODIFY и DROP; в нашем тесте 20 пакетов из 20 прошли через программу; цена захода порядка 130 мкс (наша оценка, один компьютер, журнал включён); цепочки post (postrouting, 101), pre (prerouting, −101) и predefrag (output, −401); метка бит 30 (0x40000000) у пакетов самой программы; порты 80 и 443; слово bypass; пять правил в трёх цепочках; предел первых 12 пакетов каждого направления, число 12 не значение по умолчанию. Внизу стрелка к следующей части: настройщик очереди, главный цикл, приёмщик, вердикт ядру. итог первой части: карточка маршрута пакета Где стоит пункт досмотраядро (здание) → окошко очереди → программа nfqws2 (соседняя контора) Номер очередиот 0 до 65535; в правиле queue num 200, в программе --qnum=200 ВердиктPASSMODIFYDROPпропустить · заменить · выбросить Проверено20 из 20 пакетов: программа стоит на пути пакета (наш тест) Цена заходапорядка 130 мкс: наша оценка, один компьютер, журнал включён Точка и приоритетpost: postrouting, 101 · pre: prerouting, −101 · predefrag: output, −401 Меткабит 30 (0x40000000): так помечены пакеты самой программы Порт80 и 443 Очередь и bypassqueue num 200 bypass: если очередь никто не слушает, пакет проходит Набор5 правил в 3 цепочках: post, pre и predefrag Пределпервые 12 пакетов каждого направления; 12 не значение по умолчанию следующая часть: что происходит внутри программы настройщик очередиглавный циклприёмщиквердикт ядру

Где всё стоит. Ядро это здание с лентой, в нём работают сетевая карта, протокол TCP и правила. А nfqws2 это программа в соседней конторе, в пользовательском пространстве. Чтобы она увидела пакет, ядро должно его вынести, и по нашей оценке это порядка ста тридцати микросекунд на заход, вместе с работой программы и записью журнала, если входящий и исходящий заходы равны, на одном компьютере.

Как пакет попадает в программу. Через очередь NFQUEUE: у неё номер, у нас двести, и он должен совпасть в правиле и в параметре программы. У каждого выбранного пакета своя бирка, а программа отвечает штампом: PASS, MODIFY или DROP. В нашем тесте программа стоит на пути пакета: двадцать из двадцати.

Как ядро выбирает пакеты. Правила лежат в таблице зэт, в цепочках: post на точке postrouting с приоритетом сто один, pre на точке prerouting с приоритетом минус сто один и predefrag на точке output, она защищает от петли. Правило состоит из частей: метка, порт, ограничитель первых пакетов, номер очереди и слово bypass. Всего пять правил в трёх цепочках.

И почему первые пакеты. Ядро считает пакеты каждого направления отдельно, вместе с проверяемым, и пускает до двенадцатого. В t20 на интерфейс пришло двадцать четыре входящих, а программа получила двенадцать. Пакеты сверх предела идут мимо и могут обогнать те, что она держит. И двенадцать не значение по умолчанию: в других источниках числа другие.

Предел: из 24 входящих до программы дошли 12 Предел первых двенадцати пакетов в долгом соединении нашего опыта. Входящих кадров пришло 24: первые 12 попали в очередь и к программе, остальные 12 прошли мимо. Исходящих кадров у интерфейса 13: в очереди 12, тринадцатый мимо. Счётчики направлений независимы, SYN считается первым. Пакет сверх предела идёт мимо программы и может обогнать пакет, который она ещё держит: в нашем опыте ACK мимо очереди ушёл в +4757,5 мс, а ClientHello из очереди в +4824,2 мс. предел «первые 12» на долгом соединении (наш опыт) входящие (ответы сервера): на интерфейс пришло 24 кадра 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 в очереди, у программы: 12 мимо программы: 12 исходящие (запросы клиента): у интерфейса 13 кадров своего стека 1 2 3 4 5 6 7 8 9 10 11 12 13 в очереди, у программы: 12 мимо: 1 Счётчики направлений независимы, проверяемый пакет считается: SYN это номер один. пакет сверх предела идёт мимо программы и может обогнать тот, который она ещё держит ACKClientHello +4757,5 мс: мимо очереди, ушёл сразу +4824,2 мс: из очереди, программа держала обогнал времена от начала записи в нашем опыте; число 12 здесь из правила лаборатории, не значение по умолчанию

Проверим себя, три вопроса. Зачем в правиле bypass? Чтобы пакеты проходили, если в очереди никого нет. Что считает ct original packets? Пакеты исходного направления, по одному на пакет, считая проверяемый. Сколько из двадцати четырёх входящих пакетов t20 дошло до программы? Двенадцать, ровно первые.

В следующей части мы войдём внутрь программы: как она настраивает очередь и подписывается на номер двести, как работает главный цикл, как приёмщик получает от проходной один из трёх штампов и передаёт его ядру, и как выглядит один настоящий пакет, наш запрос на двести пятьдесят байт, на всём пути.

Итак, мы знаем, где в ядре стоит пункт досмотра, как правила выбирают пакеты и почему выбираются только первые пакеты каждого направления, у нас двенадцать.

Повтор, карта программы и порядок запуска

Во второй части мы входим в программу. Сначала короткий повтор, чтобы часть можно было смотреть отдельно. Вопрос такой: что стоит за окошком пункта досмотра, какая цепочка кода берёт пакет и как возвращает ответ?

Вспомним сортировочный центр. Письма едут по ленте внутри большого здания. Здание это ядро, главная часть операционной системы, которая принимает пакеты от сетевой карты и отправляет их обратно в сеть. В стене есть окошко, а за окошком маленькая отдельная комната, в ней сидит программа nfqws2 из набора zapret2. На ленте её нет, она живёт снаружи ядра.

К окошку подъезжают не все письма: перед ним правило ядра. В нашей лаборатории оно выбирает первые двенадцать пакетов каждого направления соединения на порты восемьдесят и четыреста сорок три, и только те, на которых нет метки самой программы. В нашем тесте с долгим соединением в очередь попало ровно двенадцать исходящих и двенадцать входящих, и самый первый, SYN, считался первым. Зачем нужна метка, разберём в третьей части.

Выбранный пакет попадает в очередь с номером, у нас это двести, как номер окошка, и получает ещё собственный номер внутри очереди. Ядро держит пакет и ждёт ответа программы. Ответ называется вердиктом, у программы их три: пропустить как есть, пропустить с заменой байтов или выбросить. Мы зовём их тремя штампами, а в программе они называются PASS, MODIFY и DROP. По нашей оценке, с включённым журналом один заход к окошку стоит порядка ста тридцати микросекунд. Это оценка на одном компьютере, мы предположили, что входящий и исходящий заходы стоят поровну. Для настоящего оборудования цифр у нас нет.

Откуда у нас строки программы? Подробный журнал она пишет, только если запущена с параметром debug, а отдельные строки, например quit requested, дублируются на консоль и без него. Наша лаборатория запускает nfqws2 с этим параметром и сохраняет по журналу на опыт. Всего у нас девяносто два журнала, в них тысяча шестьдесят два пакета очереди.

Программа написана на языке C и с очередью ядра сама не разговаривает. Представьте клерка, который не знает языка ядра и пользуется готовым переводчиком: это библиотека libnetfilter_queue, набор готовых функций, открыть очередь, получить пакет, отправить вердикт. Внутрь библиотеки мы не заглядывали, читали только заголовочные файлы, поэтому говорим о ней лишь то, что видно из имён и описаний там.

Теперь цепочка. Функция main выбирает один из трёх вариантов главного цикла, по одному на операционную систему. Для Linux это nfq_main, мы зовём её главным циклом, а про BSD и Windows поговорим в конце выпуска. Главный цикл всё подготавливает и один раз вызывает настройщика очереди, функцию nfq_init.

Потом цикл бесконечно ждёт пакеты. Каждое сообщение ядра он отдаёт библиотеке, а та вызывает приёмщика, функцию nfq_cb, по разу на пакет. Приёмщик читает пакет и в конце отправляет ядру вердикт.

Между приёмщиком и стратегиями стоит маленькая функция processPacketData, проходная: она проверяет метку и передаёт пакет движку. Движок это главная функция разбора dpi_desync_packet и всё, что она вызывает: он разбирает пакет и запускает стратегии на Lua, а вердикт возвращается по той же цепочке к ядру.

Цепочка вызовов внутри программы Цепочка вызовов внутри nfqws2 в Linux. Один раз при запуске: main выбирает вариант цикла (nfqws.c:3380-3388; для BSD dvt_main и для Windows win_main, о них в конце выпуска), для Linux это nfq_main, главный цикл (nfqws.c:444), он один раз вызывает nfq_init, настройщика очереди (вызов nfqws.c:510). Дальше на каждый пакет: цикл читает сообщение ядра (recv, nfqws.c:566), отдаёт его библиотеке (nfq_handle_packet, nfqws.c:578), библиотека вызывает приёмщика nfq_cb (nfqws.c:285), тот зовёт проходную processPacketData (nfqws.c:154), проходная зовёт движок dpi_desync_packet (desync.c:2232), который запускает стратегии на Lua. Вердикт возвращается по той же цепочке к приёмщику, и он отдаёт его ядру вызовом nfq_set_verdict2 (nfqws.c:331, 334, 338). один раз при запуске выбор циклаmain()nfqws.c:3380-3388 Linuxглавный циклnfq_main()nfqws.c:444 dvt_main · BSD, win_main · Windows: в конце выпуска настройщик очередиnfq_init()вызов nfqws.c:510один раз потом цикл без конца на каждый пакет цикл: приёмrecv()nfqws.c:566 библиотекаnfq_handle_packetвызов nfqws.c:578 приёмщикnfq_cb()nfqws.c:285 проходнаяprocessPacketDatanfqws.c:154 движок и Luadpi_desync_packetdesync.c:2232 ядро: держит пакет в очереди 200 и ждёт вердиктапакет из очереди 200 вердикт идёт назад по цепочке nfq_set_verdict2 · nfqws.c:331, 334, 338 один раз за жизнь программы: nfq_init. На каждый пакет: nfq_cb, processPacketData и движок.

Проверь себя. Какая из этих функций вызывается один раз за всю жизнь программы, а какие на каждый пакет? Настройщик очереди один раз, при запуске. Приёмщик и проходная работают на каждый пакет, который пришёл из очереди.

Теперь вторая половина вопроса: что главный цикл делает до первого пакета и почему в таком порядке? Клерк не открывает окошко, как только вошёл. Сначала он включает лампу, достаёт печати, кладёт бумагу: иначе первый посетитель придёт, а помочь ему будет нечем. Очередь, то есть окошко, создаётся последней из ресурсов, нужных пакету: пакет не придёт к приёмщику раньше, чем готовы сокеты и Lua.

Первое: права. По общим сведениям, у процесса в Linux есть набор прав. Функция dropcaps, сбрасывающая права, оставляет два: на сетевое администрирование и на работу с сырыми сокетами, остальные убирает. В нашей лабораторной сборке смена пользователя отключена одной строкой, процесс остаётся root в своём изолированном пространстве имён, но урезание прав никуда не делось.

Дальше два сырых сокета, для IPv4 и IPv6: через них программа будет отправлять собственные пакеты, о них в третьей части. Потом запускается Lua, язык стратегий: пока он не готов, программа не умеет выполнять ни одной.

Потом два буфера. Приёмный: шестьдесят пять тысяч пятьсот тридцать шесть байт. Ещё четыре тысячи девяносто шесть, по комментарию автора, на служебные заголовки сообщения ядра. Сложим, получится шестьдесят девять тысяч шестьсот тридцать два. Второй буфер, для изменённого пакета, ровно шестьдесят пять тысяч пятьсот тридцать шесть. Ещё вспомогательный сокет, он нужен приёмщику для имён интерфейсов.

И только теперь создаётся очередь. Между шагами есть мелкие проверки, например файлов, их не разбираем. В журнале опыта t10 порядок тот же: строка про сырые сокеты и две группы по три строки про их буферы, потом запуск Lua и LUA INIT DONE, потом восемь строк про очередь и только потом первый пакет.

Порядок запуска: окошко открывается последним Порядок запуска главного цикла nfq_main до первого пакета, шесть шагов сверху вниз, как в коде nfqws.c: права (dropcaps, строки 463-464: остаются два права, NET_ADMIN и NET_RAW), два сырых сокета для IPv4 и IPv6 (rawsend_preinit, 474), Lua (lua_init, 486: при неудаче переход к выходу с ошибкой, очередь не создаётся), два буфера (malloc, 498: приёмный 65536 + 4096 = 69632 байта и второй для изменённого пакета 65536), вспомогательный сокет (504, нужен приёмщику для имён интерфейсов) и последней очередь (nfq_init, 510). Внизу журнал опыта: строка про сырые сокеты, две группы по три строки про их буферы, LUA INIT DONE, восемь строк про очередь, первый пакет. что делает nfq_main до первого пакета (nfqws.c:444-531) 1праваdropcaps()nfqws.c:463-464остаются два права: NET_ADMIN и NET_RAW 2два сырых сокетаrawsend_preinit()nfqws.c:474IPv4 и IPv6: через них программа шлёт свои пакеты 3Lualua_init()nfqws.c:486не запустился: goto err, очередь не создаётся, выход с кодом 1 4два буфераmalloc()nfqws.c:498приёмный 65536 + 4096 = 69632 Б; для изменённого пакета 65536 Б 5вспомогательный сокетsocket(AF_INET, SOCK_DGRAM, 0)nfqws.c:504нужен приёмщику, чтобы узнавать имена интерфейсов 6очередьnfq_init()nfqws.c:510создаётся последней: окошко открывают, когда всё готово журнал опыта: те же шаги в том же порядке initializing raw sockets bind-fix4=0 bind-fix6=0сырые сокеты set_socket_buffers … fd=3, fd=4 (две группы по три строки)буферы сырых сокетов LUA INIT DONELua готов opening nfq library handle … set receive buffer size to 1048576восемь строк про очередь packet: id=1 len=60 mark=00000000 …первый пакет Окошко открывается последним: пакет не придёт к приёмщику раньше, чем готовы сокеты и Lua.

Проверь себя. Что будет, если Lua не запустился? За вызовом запуска стоит проверка: при неудаче программа идёт к выходу с ошибкой, очередь не создаётся и пакеты в программу не приходят.

Итак, главная функция выбирает главный цикл, он всё готовит, один раз вызывает настройщика очереди, а затем на каждый пакет через библиотеку вызывает приёмщика, приёмщик проходную, проходная движок, а вердикт возвращается к ядру. Пойдём по ступеням: настройщик, цикл, приёмщик, три штампа и один настоящий пакет.

Настройщик очереди, первая половина: открыть, привязать, создать очередь

Разберём настройщика очереди, функцию nfq_init. В файле nfqws.c она занимает семьдесят семь строк, и это единственное место, где программа подключается к очереди ядра. Вопрос: как она получает именно ту очередь, в которую правило ядра кладёт пакеты? Пройдём её в два приёма, сейчас первая половина.

Представьте, что клерк устраивается в отделение. Он открывает личное дело, пишет заявления «принимаю письма такого-то вида», и только потом ему выдают окошко с номером. Каждый шаг может не получиться: на одних клерк сдаётся и уходит домой, на других только отмечает неудачу и работает дальше.

Что у функции на входе? Указатели на ручку библиотеки, на ручку очереди и на блок данных для приёмщика, а ещё номер очереди из параметров запуска. На выходе только «да» или «нет». Первой строкой функция вызывает nfq_deinit, «сбросить всё, что было»: в первый раз сбрасывать нечего, а при повторном вызове старая очередь уничтожается до создания новой.

Вызов nfq_open открывает ручку библиотеки, как личное дело: через неё идут все следующие вызовы. По общим сведениям, за ручкой стоит сокет netlink, канал, по которому программа разговаривает с ядром. Мы читали только заголовки библиотеки, так что это общее знание, а не проверенный факт. Не открылась ручка, это фатальная ошибка.

Дальше привязка к IPv4 из двух шагов: «отвяжи прежнего обработчика очередей, если он был», nfq_unbind_pf, и «привяжи нашу очередь для IPv4», nfq_bind_pf. Ошибка любого шага фатальна. Для IPv6 те же два шага, но ошибка только пишется в журнал, так что компьютер без IPv6 всё равно получит очередь для IPv4. Автор в комментарии пишет, что на ядрах начиная с версии три точка восемь эти привязки ничего не делают, а на старых нужны обе. Старые ядра мы не проверяли.

Проверь себя. Привязка к IPv6 не удалась, что будет? Ошибка попадёт в журнал, очередь для IPv4 заработает. А если не удалась привязка к IPv4? Функция сбросит всё сделанное и вернёт «нет», программа остановится с кодом один.

Главная строка: вызов nfq_create_queue, «создать очередь», с четырьмя аргументами. Ручка библиотеки. Номер очереди. Функция nfq_cb: так программа говорит библиотеке «когда придёт пакет, вызови вот эту», и такую функцию называют обратным вызовом, по-английски callback. И блок данных, который библиотека вернёт приёмщику при каждом вызове: в нём вспомогательный сокет и буфер для изменённого пакета. Не удалось создать очередь, фатально.

nfq_init: открыть, привязать, создать очередь Первая половина функции nfq_init (nfqws.c:355-396) по строкам кода. Строка 357 nfq_deinit сбрасывает старое; 360 nfq_open открывает ручку библиотеки (сбой фатален); 370 nfq_unbind_pf и 376 nfq_bind_pf для IPv4 (сбой любого шага фатален); 382 и 387 те же два шага для IPv6, ошибка только пишется в журнал; 392 nfq_create_queue с четырьмя аргументами: ручка библиотеки, номер очереди params.qnum, функция обратного вызова nfq_cb и блок данных cbdata (вспомогательный сокет и буфер на 65536 байт); сбой создания очереди фатален. Карточка функции: на входе h, qh, cbdata и params.qnum, на выходе true или false; вызывает nfq_deinit, nfq_open, nfq_unbind_pf, nfq_bind_pf, nfq_create_queue; вызывается из nfq_main один раз (nfqws.c:510). Фатальный сбой сбрасывает всё сделанное, программа выходит с кодом 1. nfq_init, первая половина: nfqws.c:355-396 357сбросnfq_deinit(h, qh);сбросить старое 360фатально*h = nfq_open();открыть ручку 370фатальноif (nfq_unbind_pf(*h, AF_INET) < 0)отвязать прежнего, IPv4 376фатальноif (nfq_bind_pf(*h, AF_INET) < 0)привязать, IPv4 382записьif (nfq_unbind_pf(*h, AF_INET6) < 0)отвязать, IPv6 387записьif (nfq_bind_pf(*h, AF_INET6) < 0)привязать, IPv6 392фатально*qh = nfq_create_queue(*h, params.qnum, &nfq_cb, cbdata);создать очередь четыре аргумента nfq_create_queue ручка библиотеки*hоткрыта в начале номер очередиparams.qnum200 в нашей лаборатории обратный вызов&nfq_cbвызвать при пакете блок данныхcbdataвернётся приёмщику cbdata (nfqws.c:452, 498-508): sock — вспомогательный сокет, mod — буфер для изменённого пакета, 65536 Б карточка функции входh, qh, cbdataparams.qnum выходtrue или false вызываетnfq_deinit, nfq_open,nfq_unbind_pf,nfq_bind_pf,nfq_create_queue вызываетсяиз nfq_main, один разnfqws.c:510 Фатальный сбой: всё сделанное сбрасывается, функция возвращает false, программа выходит с кодом 1.

Номер берётся из параметра qnum. Допустимы числа от нуля до шестидесяти пяти тысяч пятисот тридцати пяти, вне диапазона программа пишет bad qnum и останавливается с кодом один. Внутри значение по умолчанию минус один, «не задано»: если параметра нет, а перехват включён, что бывает по умолчанию, программа отказывается стартовать и пишет Need queue number. А с выключенным перехватом очередь не нужна: программа запускает Lua и таймеры и настройщика не вызывает.

Здесь встречаются два числа, из правила ядра и из параметра запуска. В нашем журнале строка binding this socket to queue, апостроф, двести, и в правиле стоит queue num двести. В руководстве сказано, что две программы не могут занять одну очередь, вторая получит ошибку. Мы этого не запускали.

А если никто не слушает очередь двести, например программа упала или в номере опечатка? Слово bypass в конце правила нужно как раз для этого. По общим сведениям, оно значит «если никто не слушает, пропусти пакет». В нашей лаборатории опыт без программы, t00, так и работает: правила стоят, программы нет, соединение проходит. В нашем тесте на пакетах UDP в изолированном пространстве имён без слушателя с этим словом доставлены два пакета из двух, без него ни одного. TCP и флаг fail-open так не проверяли, а опыта с неверным номером у нас нет, поэтому для опечатки это логический вывод, не измерение.

Два числа очереди должны совпасть Откуда берётся номер очереди. В правиле ядра стоит queue num 200, в параметре запуска --qnum=200, и в журнале строка binding this socket to queue '200': два числа обязаны совпасть. Допустимы номера от 0 до 65535; значение по умолчанию внутри программы −1 означает «не задано». Четыре случая: без --qnum при включённом перехвате программа пишет Need queue number и выходит с кодом 1 (nfqws.c:3178-3181); --qnum=-1 или 70000 даёт bad qnum и выход 1 (nfqws.c:2393-2397); --qnum=200 создаёт очередь 200; при --intercept=0 очередь не нужна и nfq_init не вызывается (nfqws.c:491-496). Внизу вопрос, что будет без слушателя очереди 200: в нашем тесте на UDP в изолированном пространстве имён с bypass дошли 2 пакета из 2, без bypass 0 из 2; TCP и опечатку в номере так не проверяли. номер очереди: правило ядра и параметр запуска правило ядра (nftables)… tcp dport {80,443} ct originalpackets 1-12 queue num 200 bypass параметр запуска программыnfqws2 --qnum=200 …params.qnum = atoi(optarg): nfqws.c:2392 200равны binding this socket to queue '200'строка журнала, nfqws.c:391 допустимые номера (шкала не в масштабе)065535от 0 до 65535 200: наш номер −1по умолчанию −1: не задано нет --qnum, перехват включёнNeed queue number (--qnum), выход с кодом 1nfqws.c:3178-3181 --qnum=-1 или --qnum=70000bad qnum, выход с кодом 1nfqws.c:2393-2397 --qnum=200очередь 200: nfq_create_queuenfqws.c:392 --intercept=0очередь не нужна: nfq_init не вызываетсяnfqws.c:491-496 а если очередь 200 никто не слушает? с bypass в правиле2 из 2 дошлибез bypass0 из 2 дошли: очередь без слушателя отбросила Наш тест: UDP в изолированном пространстве имён, nfqws2 не запущен. TCP так не проверяли;для опечатки в номере это логический вывод, не измерение.

Итак, первая половина настройщика: сбросить старое, открыть ручку, привязать IPv4, где ошибка фатальна, и IPv6, где она только пишется, и создать очередь вызовом с четырьмя аргументами. Номер приходит из параметра qnum, и в нашей лаборатории это двести, как в правиле ядра.

Настройщик очереди, вторая половина: условия работы, буфер, что фатально

Очередь создана, теперь программа объясняет ядру условия работы. Вопрос: что она просит у очереди и какие шаги настройщика фатальны, а какие нет?

Клерку на трёх карточках объясняют условия. Первая: присылай письмо целиком, а не только конверт. Вторая: в коридоре помещается не больше определённого числа писем. Третья: если не справляешься, письма не выбрасывай.

Первая карточка. Вызов nfq_set_mode, «установить режим», просит ядро копировать в сообщение пакет целиком, не больше шестидесяти пяти тысяч пятисот тридцати пяти байт. Наш запрос с именем сайта, триста два байта от заголовка IP, помещается. Одних заголовков программе не хватило бы: имя лежит внутри данных запроса TLS, мы нашли его байт за байтом во втором выпуске. Сбой этого шага фатален.

Вторая карточка. Вызов nfq_set_queue_maxlen, «длина очереди», задаёт четыре тысячи девяносто шесть пакетов. По общим сведениям, столько пакетов может ждать вердикта в ядре. Что происходит при переполнении, мы не проверяли, и в девяноста двух журналах переполнения не наблюдали: за один опыт в очередь попадало не больше двадцати четырёх пакетов, по двенадцать на направление. Сбой этого шага тоже фатален.

Третья карточка про переполнение: флаг fail-open, по-русски «при отказе открыто», вызов nfq_set_queue_flags. Комментарий автора: «принимать пакеты, если их нельзя обработать». По общим сведениям, это значит, что при невозможности обработать пакет ядро его пропускает. Мы этого не проверяли, ни один наш опыт не переполнял очередь, поэтому флаг мы в деле не видели. Если ядро флаг не поддерживает, программа лишь пишет сообщение. В нём сказано, что на ядрах до версии три точка шесть это нормально, но это слова автора, а не проверка версии: сообщение появится при любом сбое.

Теперь буфер приёма, лоток для сообщений ядра. Программа просит один мебибайт: тысяча двадцать четыре умножить на тысячу двадцать четыре, один миллион сорок восемь тысяч пятьсот семьдесят шесть байт. Библиотека возвращает фактический размер, программа делит его на два и сравнивает с запрошенным. Равно, пишет, что размер выставлен. Не равно, пишет, какой получился.

Почему деление на два? В нашем журнале есть похожий случай, для сырых сокетов. На приём запрошено две тысячи сорок восемь, показано четыре тысячи девяносто шесть. На отправку запрошено тридцать две тысячи семьсот шестьдесят восемь, показано шестьдесят пять тысяч пятьсот тридцать шесть. Ровно вдвое больше. Понятно, зачем программа делит ответ. Для самой очереди удвоение мы отдельно не проверяли и что ядро делает с этим размером, не проверяли тоже. В журнале t10 для очереди равенство выполнено.

Последняя настройка, про переполнение, называется NETLINK_NO_ENOBUFS. По общим сведениям, она просит ядро не сообщать сокету netlink об ошибке переполнения, как это работает, мы не проверяли. Обработка этой ошибки в главном цикле всё равно написана, об этом дальше.

Условия работы очереди: пять вызовов Вторая половина nfq_init: условия работы очереди, пять вызовов по строкам nfqws.c. 399 nfq_set_mode(…, NFQNL_COPY_PACKET, 0xffff): копировать пакет целиком, до 65535 байт (сбой фатален); 403 nfq_set_queue_maxlen(…, Q_MAXLEN): длина очереди 4096 пакетов (сбой фатален); 408 nfq_set_queue_flags(…, NFQA_CFG_F_FAIL_OPEN, …): флаг fail-open, комментарий автора «accept packets if they cant be handled», только запись; 414 nfnl_rcvbufsiz(…, Q_RCVBUF) / 2: буфер приёма 1048576 байт, только запись; 423 setsockopt(…, NETLINK_NO_ENOBUFS, …): про ошибку переполнения, только запись. Лента: копия от 0 до 65535 байт, наш запрос в 302 байта от заголовка IP помещается целиком. Предел 4096 пакетов: что при переполнении, не проверяли, в 92 журналах не видели, за опыт в очередь попадало не больше 24 пакетов. nfq_init, вторая половина: nfqws.c:398-425 399фатальноnfq_set_mode(*qh, NFQNL_COPY_PACKET, 0xffff)копировать пакет целиком0xffff это 65535 байт; наш запрос в 302 байта помещается 403фатальноnfq_set_queue_maxlen(*qh, Q_MAXLEN)длина очереди: 4096 пакетовпо общим сведениям, столько пакетов ждут вердикта в ядре 408записьnfq_set_queue_flags(*qh, NFQA_CFG_F_FAIL_OPEN, …)fail-open: не выбрасыватькомментарий автора: accept packets if they cant be handled; в деле не видели 414записьnfnl_rcvbufsiz(nfq_nfnlh(*h), Q_RCVBUF) / 2буфер приёма: 1048576 Бответ библиотеки делится на два и сравнивается с запрошенным 423записьsetsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, …)не сообщать о переполнениипо общим сведениям; как это работает, не проверяли что значит «копия до 65535 байт»от 0 до 65535 байт наш запрос: 302 байта от заголовка IP, помещается целиком 4096пакетов (Q_MAXLEN)params.h:27 что при переполнении, мы не проверяли;в 92 журналах переполнения не видели:за опыт в очередь попадало не больше 24 пакетов

Соберём. Фатальны открытие ручки, привязка IPv4, создание очереди, режим копирования и длина очереди: любой такой сбой сбрасывает всё сделанное и возвращает «нет». Только запись в журнал: привязка IPv6, флаг fail-open, размер буфера и настройка про переполнение.

В журнале это восемь строк в порядке вызовов. Они есть в каждом из девяноста двух журналов, и ни в одном нет ошибки флага или буфера.

Буфер приёма и что фатально Размер буфера приёма и что в настройщике фатально. Программа просит 1024 × 1024 = 1048576 байт (Q_RCVBUF, params.h:28), библиотека возвращает фактический размер, программа делит его на два и сравнивает с запрошенным (nfqws.c:414-418): равно, в журнал идёт «set receive buffer size to 1048576»; не равно, пишется «could not set … real size is», и это не фатально. Почему делить на два: на сырых сокетах в нашем журнале запрошено 2048 на приём, показано 4096, запрошено 32768 на отправку, показано 65536, ровно вдвое (darkmagic.c:1885-1886); для самой очереди удвоение отдельно не проверяли. Фатальны пять шагов: nfq_open, привязка IPv4, nfq_create_queue, режим копирования и длина очереди; только в журнал пишутся привязка IPv6, флаг fail-open, размер буфера и настройка NETLINK_NO_ENOBUFS. В журнале восемь строк по порядку вызовов, во всех 92 журналах нет ошибки флага или буфера. буфер приёма: nfqws.c:414-418 просим1024 × 1024 = 1048576 БQ_RCVBUF, params.h:28 библиотека отвечаетфактический размер Xnfnl_rcvbufsiz(…), :414 делим на дваX / 2 и сравниваемс запрошенным 1048576 равно: размер выставленset receive buffer size to 1048576не равно: пишет, какой получилсяcould not set … real size is X в обоих случаях только запись, программа идёт дальше почему на два? похожий случай в нашем журнале: сырые сокеты приёмзапрошено2048×2показано4096darkmagic.c:1886 отправказапрошено32768×2показано65536darkmagic.c:1885 Для самой очереди удвоение отдельно не проверяли; в журнале опыта равенство выполнено. что фатально, а что только запись фатально: сброс всего и выход с кодом 1nfq_open:359-364привязка IPv4:370-378nfq_create_queue:391-396режим копирования:398-402длина очереди:403-406 только запись в журналпривязка IPv6:382-389флаг fail-open:408-412размер буфера:414-418NETLINK_NO_ENOBUFS:423-424 В журнале восемь строк по порядку вызовов; во всех 92 журналах нет ошибки флага или буфера.

Проверь себя. Ядро выдало буфер приёма меньше запрошенного, остановится ли программа? Нет, напишет, какой размер получился, и продолжит. А если не создастся очередь? Это фатально: сообщение об ошибке и выход с кодом один.

Итак, настройщик просит копировать пакеты целиком, держать до четырёх тысяч девяноста шести пакетов, принимать необработанные и выделяет буфер в один мебибайт. Пять шагов фатальны, остальные только пишутся в журнал.

Главный цикл: один оборот

Теперь главный цикл: вторая половина функции nfq_main, около девяноста строк, в ней программа проводит почти всю жизнь. Вопрос: что она делает за один оборот?

У клерка рабочий день состоит из одинаковых оборотов. Подождать, пока что-нибудь придёт. Подмести. Посмотреть, нет ли записки «перечитай списки». Взять письмо и разобрать его. И снова с начала.

В коде два цикла, один внутри другого. Внешний нужен, чтобы после сигнала или ошибки переполнения вернуться к началу. Внутренний за оборот делает те самые четыре дела: ждёт, запускает уборку и проверку записки, читает данные из сокета и передаёт их приёмщику. Без таймеров ожидание совпадает с чтением. Если читать не удалось, внутренний цикл выходит, и причину разбирают отдельно.

Один оборот главного цикла Один оборот главного цикла nfq_main (nfqws.c:530-598), как рабочий день клерка по кругу: ждать (строки 537-561), подмести (lua_do_gc, 562), посмотреть записку «перечитай списки» (ReloadCheck, 563), взять письмо и разобрать (recv, 566, и nfq_handle_packet, 578), и снова с начала. Скелет кода: внешний цикл for на строке 532, в нём проверка флага завершения (534) и внутренний цикл for на строке 535 с этими четырьмя делами; если чтение не удалось, внутренний цикл выходит, причину разбирают отдельно (567, 591-598). Ожидание в двух режимах: если у скриптов есть таймеры, цикл ждёт вызовом select (547) до данных в сокете или срока ближайшего таймера, и при нуле recv пропускается (564); если таймеров нет, select не вызывается (bt_next = bt = 0; res = 1, строки 558-560), и цикл сидит в блокирующем recv. Какой режим работает в наших опытах, не проверяли: это устройство кода. главный цикл nfq_main: nfqws.c:530-598 1 ждатьпока что-то придётnfqws.c:537-561 2 подместиуборка мусора Lualua_do_gc() :562 3 запискане пришёл ли SIGHUPReloadCheck() :563 4 взять письмочитаем и разбираемrecv :566nfq_handle_packet :578 и снова с начала скелет кода внешний for(;;):532if (bQuit) goto quit;:534 внутренний for(;;):535 1ждать: select или сразу recv:537-561 2lua_do_gc();:562 3ReloadCheck();:563 4recv(), nfq_handle_packet():566, 578 таймеры Lua :582-589 разберём позже чтение не удалось (rd < 0): внутренний циклвыходит (:567), причину разбирают отдельно:591-598 ожидание в двух режимах есть таймеры у скриптов:537select(fd+1, &fdset, NULL, NULL, &tv):547вернётся: данные в сокете илисрок ближайшего таймера (будильник)res == 0: recv пропускается (:564) таймеров нет:556bt_next = bt = 0; res = 1;:558-560select не вызывается совсем: цикл сидитв recv, он блокируется до данных Какой режим в наших опытах, не проверяли. Уборка и записка стоят до чтения: с таймерами после select,без таймеров прямо перед recv, где цикл потом и ждёт. Приёмщик nfq_cb вызывается из этого же цикла, следующее чтение после его возврата.Вывод из кода: потоков нет; саму библиотеку не читали.

Ожидание работает в двух режимах. Без таймеров, если скрипты стратегий их не заводили, select не вызывается вообще: цикл сидит в вызове recv, который блокируется до прихода данных, как клерк спит, пока не позвонят. С таймерами цикл ждёт вызовом select, он вернётся, когда в сокете появились данные или подошёл срок ближайшего таймера, как будильник. Таймеры Lua разберём позже. Какой режим работает в наших опытах, мы не утверждаем, говорим только про устройство кода.

Уборка. Функция lua_do_gc собирает мусор Lua, то есть освобождает память, которую скрипты уже не используют. Если с прошлой сборки прошло шестьдесят секунд, это значение по умолчанию, запускается полная сборка и в журнал пишется строка LUA GARBAGE COLLECT. Отсчёт идёт от включения компьютера, поэтому на самом первом обороте сборка запускается, только если машина работает не меньше шестидесяти секунд. В журнале t10 эта строка стоит прямо перед первым пакетом.

Записка. Функция ReloadCheck смотрит, не пришёл ли сигнал SIGHUP, и если пришёл, программа перечитывает списки доменов и адресов. Уборка и записка стоят до чтения: с таймерами после ожидания в select, без таймеров прямо перед recv, где цикл потом и ждёт.

Чтение. Вызов recv кладёт данные ядра в приёмный буфер, шестьдесят девять тысяч шестьсот тридцать два байта. По комментарию автора, за один вызов в буфер может прийти сразу несколько сообщений подряд, мы такого не наблюдали. Ноль байтов программа считает фатальной ошибкой. Дальше функция библиотеки nfq_handle_packet разбирает данные и вызывает приёмщика. Это наш вывод из кода: потоков в программе нет, значит приёмщик вызывается из этого же цикла, и следующее чтение будет после его возврата. Саму библиотеку мы не читали. Ошибку библиотеки цикл лишь пишет в журнал.

Уборка Lua и чтение сокета Уборка и чтение внутри одного оборота. Уборка: функция lua_do_gc (lua.c:4699-4713) освобождает память, которую скрипты Lua уже не используют. Условие: now − gc_time ≥ params.lua_gc, где now берётся от включения компьютера (boottime_ms), gc_time стартует с нуля, а params.lua_gc по умолчанию 60 × 1000 = 60000 мс = 60 с (params.c:591, params.h:50). Поэтому на самом первом обороте уборка запускается, только если компьютер включён не меньше 60 секунд. Когда уборка сработала, в журнал пишется строка LUA GARBAGE COLLECT: 348K => 243K; в журнале опыта t10 она стоит прямо перед первым пакетом packet: id=1. Чтение: recv (nfqws.c:566) кладёт данные ядра в приёмный буфер 69632 байта = 65536 + 4096 (nfqws.c:443). Результат меньше нуля: внутренний цикл выходит (567). Ноль байт: фатальная ошибка «recv from nfq returned 0 !», выход с кодом 1 (568-572). Больше нуля: nfq_handle_packet (578) разбирает данные и вызывает приёмщика nfq_cb; ошибка библиотеки только пишется в журнал (579). Когда приёмщик вернулся, начинается следующий recv. По комментарию автора за один recv в буфер может прийти сразу несколько сообщений (nfqws.c:322-323), мы такого не наблюдали. уборка мусора Lua: lua.c:4699-4713чтение: nfqws.c:566-580 lua_do_gc(): уборка мусора Luanow = boottime_ms();:4703if ((now - gc_time) >= params.lua_gc):4704static uint64_t gc_time=0;:4698params.lua_gc = LUA_GC_INTERVAL*1000; 60000 мс = 60 сзначение по умолчаниюparams.c:591params.h:50LUA_GC_INTERVAL 60 (с) первый оборот: gc_time = 0 060 свремя от включения компьютерауборки нетуборка на первом обороте в журнале опыта t10, перед первым пакетом LUA GARBAGE COLLECT: 348K => 243Kpacket: id=1 len=60 mark=00000000 … память Lua до сборки 348K, после 243K (:4706-4709).Следующая сборка: не раньше чем через 60 с. приёмный буферrd = recv(fd, buf, NFQ_MAX_RECV_SIZE, 0);69632 Б = 65536 + 4096 (:443):566 rd < 0внутренний цикл выходитпричина разбирается отдельно:567 rd == 0фатально, код выхода 1recv from nfq returned 0 !:568-572 rd > 0разбор и вызов приёмщикаnfq_handle_packet(h, buf, rd):578 res < 0 после разбора: ошибка библиотеки,только запись в журнал (:579) что вызывается на одно сообщение recv nfq_handle_packet nfq_cb вернулись, следующий recv приёмщик вызывается синхронно: потоков нет,вывод из кода, библиотеку не читали Автор: в буфере за один recv может бытьнесколько сообщений (:322-323), не наблюдали.

Проверь себя. Если у скриптов нет таймеров, вызывается ли select? Нет, цикл сидит прямо в recv. Значит, на пустой очереди без таймеров он простаивает в recv и до следующей уборки не доходит. Это вывод из кода, в лаборатории мы его не проверяли.

Итак, главный цикл состоит из двух вложенных. За оборот внутренний ждёт, с таймерами или без, запускает уборку Lua и проверку сигнала перезагрузки списков, читает данные из сокета и передаёт их приёмщику, который вызывается синхронно.

Главный цикл: один поток, ошибки чтения, выход

Остались три вопроса. Почему пакеты обрабатываются строго по одному? Что делает цикл, если чтение не удалось? И как программа останавливается?

В программе один поток. В её исходных текстах нет ни одного вызова для работы с потоками: мы искали pthread, thrd_create и CreateThread и не нашли ничего. Единственное исключение, fork при запуске в режиме демона, но он не создаёт параллельной обработки пакетов. Значит, пакеты идут строго по одному, в порядке прихода. Пока один находится внутри скрипта на Lua, остальные ждут в очереди ядра, а по настройке очереди ждать может до четырёх тысяч девяноста шести.

Что говорят номера? В каждом из девяноста двух журналов номера в очереди идут подряд с единицы без пропусков. Это значит, что ни один пакет из попавших в очередь не остался без записи в журнале и что очередь была одна. Потеряло ли что-то само ядро, номера не показывают, и о потоках они ничего не говорят: о потоках говорит только код.

Теперь опыт t10 на часах. Пакет номер четыре, запрос, программа разрезала на две части, и обе ушли на провод. Сразу после выхода первой части на интерфейс клиента пришли два ответа сервера: подтверждение первой части, номер пять, и короткий ответ в четырнадцать байт, номер шесть. Наш тестовый сервер отвечает на первый же полученный кусок. А программа увидела ответы заметно позже, по нашему объяснению потому, что ещё дописывала обработку запроса, и пакеты пять и шесть ждали в очереди. Подтверждение второй части пришло позже: это пауза до ответа сервера, а не ожидание в очереди, причину мы не исследовали. Величину ожидания не называем: на неё влиял диск журнала. Важен порядок.

Один поток: остальные ждут Программа nfqws2 обрабатывает пакеты строго по одному. Поиск по исходным текстам (nfq2/*.c и nfq2/*.h): pthread, thrd_create и CreateThread дают 0 совпадений; единственный fork стоит в функции daemonize (sec.c:344) и параллельной обработки пакетов не создаёт. Значит, пока один пакет находится внутри скрипта на Lua, остальные ждут в очереди ядра; по настройке очереди ждать может до 4096 пакетов (Q_MAXLEN, params.h:27). Анимация по опыту t10: пакет 4, запрос, программа держит и разрезает на две части; пакеты 5 и 6, ответы сервера, стоят в очереди. Порядок событий на ленте: часть 1 вышла на провод, ответы 5 и 6 пришли на интерфейс клиента почти сразу, программа увидела ответы заметно позже, по нашему объяснению потому, что ещё дописывала обработку запроса; расстояния не в масштабе, величину ожидания не называем: на неё влиял диск журнала. Подтверждение второй части (7) пришло позже: это пауза до ответа сервера, а не ожидание в очереди. Номера в очереди в каждом из 92 журналов идут подряд с единицы без пропусков: ни один пакет из попавших в очередь не остался без записи, очередь одна; о потоках номера ничего не говорят, о потоках говорит только код. один поток: поиск по исходникам и опыт t10 поиск по исходникам nfq2/*.c, *.hpthread0 совпаденийthrd_create0 совпаденийCreateThread0 совпаденийfork(): только daemonize, sec.c:344 значит, пакеты строго по одномув порядке прихода; пока один пакет внутрискрипта на Lua, остальные ждут в очередиядра: по настройке до 4096 (Q_MAXLEN)params.h:27 опыт t10: пакеты 4, 5 и 6 очередь ядраголова очереди nfqws2: один потокпакет 4 в скрипте Lua:запрос режется на две части,обе уходят на провод берёт 4 5 6 ждётждёт вердикт пакета 4 ещё не вернулся порядок событий на ленте времени (расстояния не в масштабе) 1часть 1 вышлана провод 2ответы 5 и 6 на интерфейсеклиента, почти сразу 3программа увидела 5 и 6,заметно позже Почему позже: по нашему объяснению, программа ещё дописывалаобработку запроса. Это объяснение, не измерение. Величину ожидания не называем: на неё влиял диск журнала. Важен порядок. Номера в очереди в каждом из 92 журналов идут подряд с 1: пропусков среди попавшихв очередь нет, очередь одна. О потоках номера не говорят, о потоках говорит только код.

Проверь себя. Что будет с пакетом номер пять, пока обрабатывается номер четыре? Он ждёт в очереди ядра. Пакет номер пять пришёл, когда программа занята четвёртым. Чей вердикт вернётся ядру раньше? Четвёртого: поток один, порядок прихода. Это вывод из кода.

Ошибки чтения разбираются по виду. Чтение прервано сигналом: цикл возвращается к началу и проверяет флаг завершения. Ядро сообщает о переполнении буфера, ошибка называется ENOBUFS: она записывается в журнал, и работа продолжается. Любая другая ошибка завершает программу с кодом один. С вызовом select так же: прерывание сигналом просто повторяет ожидание, любая другая его ошибка завершает программу. ENOBUFS в нашей лаборатории не наблюдался.

Проверь себя. Что сделает программа, если recv вернул ENOBUFS? Запишет в журнал и продолжит. А если вернул ноль байтов? Это фатальная ошибка, остановка с кодом один.

Остановка. Сигналы SIGINT и SIGTERM взводят флаг завершения. Обработчики настроены без флага перезапуска вызова, поэтому, по общим сведениям, ждущий вызов сигналом прерывается, программа просыпается, видит флаг и идёт на выход. Оба обработчика ещё вызывают функцию lua_req_quit. По коду она ставит в Lua ловушку, которая на следующем шаге скрипта вызывает ошибку INTERRUPT, то есть работающий скрипт прерывается. Это чтение кода: посреди скрипта мы программу не останавливали.

Выход. Цикл пишет quit requested, программа освобождает буферы и вызывает nfq_deinit, ту самую функцию из начала настройщика: она пишет unbinding from queue двести и closing nfq library handle. Потом закрывается вспомогательный сокет, останавливается Lua, закрываются сырые сокеты. Код возврата при чистом выходе ноль, при ошибке один.

Лаборатория останавливает nfqws2 командой kill, то есть сигналом SIGTERM. В файле консоли появляется TERM received, а хвост журнала t10 повторяет порядок кода: quit requested, unbinding, closing, LUA SHUTDOWN.

Ошибки чтения и выход Ошибки чтения и выход из главного цикла. Ошибки: select прерван сигналом (EINTR) просто повторяет ожидание, любая другая ошибка select завершает программу с кодом 1 (nfqws.c:549-554); recv вернул ноль байт: фатально, код 1 (568-572); recv прерван сигналом (EINTR): возврат к началу внешнего цикла и проверка флага bQuit (591-592); ENOBUFS, ядро сообщает о переполнении буфера: запись в журнал и работа продолжается (593-597); любая другая ошибка recv: выход с кодом 1 (597, 599-601). ENOBUFS в нашей лаборатории не наблюдался. Остановка: сигналы SIGINT и SIGTERM взводят флаг bQuit и вызывают lua_req_quit (обработчики onint и onterm, nfqws.c:114-131; sa_flags = 0 без SA_RESTART, :139, поэтому ждущий вызов прерывается; lua_req_quit ставит в Lua ловушку, которая прервёт работающий скрипт ошибкой INTERRUPT, lua.c:4651-4660, это чтение кода, мы так программу не останавливали). Выход, строки 599-618, лесенкой: запись quit requested (604), освобождение буферов (609-610), nfq_deinit (611, пишет unbinding from queue 200 и closing nfq library handle), close вспомогательного сокета (612), lua_shutdown (613), rawsend_cleanup (617), код возврата 0 при чистом выходе и 1 при ошибке. В опыте t10 хвост журнала повторяет этот порядок: quit requested, unbinding from queue 200, closing nfq library handle, LUA SHUTDOWN; в файле консоли после kill (SIGTERM) строка TERM received !. ошибки чтения: nfqws.c:549-598остановка и выход: nfqws.c:599-618 select прерван сигналом:551EINTR: ждать заново select: другая ошибка:552-553выход с кодом 1 recv вернул 0 байт:568-572выход с кодом 1 recv прерван сигналом:591-592EINTR: в начало, проверить bQuit recv: ENOBUFS:593-597запись в журнал, работа продолжается recv: любая другая ошибка:597, 599-601выход с кодом 1 ENOBUFS: ядро сообщает о переполнении буфера.В нашей лаборатории такой ошибки не было. хвост журнала опыта t10 (kill = SIGTERM) quit requestedunbinding from queue 200closing nfq library handleLUA SHUTDOWN SIGINT, SIGTERMonint / onterm: bQuit = true;lua_req_quit();:114-131без SA_RESTART :139 ждущий вызов прерывается, цикл видит флаг lua_req_quit: ловушка INTERRUPT (lua.c:4651-4660),прервёт скрипт; по коду, не проверяли выход лесенкой quit requested:604 free(cbdata.mod); free(buf);:609-610 nfq_deinit(&h, &qh);:611 close(cbdata.sock);:612 lua_shutdown();:613 rawsend_cleanup();:617 nfq_deinit пишет: unbinding from queue 200,closing nfq library handle (:344, :350) return res; код 0: чистый выход:606, :618 return res; код 1: ошибка:600 файл консоли: TERM received !порядок журнала повторяет порядок кода.Остановку посреди скрипта не проверяли.

Итак, пакеты обрабатываются по одному в одном потоке, остальные ждут в очереди ядра. Сигнал, ошибка чтения и переполнение разбираются отдельно, а выход чисто закрывает очередь, Lua и сокеты.

Приёмщик: что он читает, что пишет в журнал, как зовёт проходную

Теперь приёмщик, функция nfq_cb. Она вызывается один раз на каждый пакет, который ядро положило в очередь, и занимает в файле nfqws.c пятьдесят пять строк. Вопрос: что приёмщик достаёт из пакета, что пишет в журнал и как зовёт проходную?

Клерк получает конверт-копию, а к ней приколота бирка. С бирки он снимает номер, метку, длину содержимого и названия комнат, откуда и куда письмо ехало. Всё это приёмщик делает в самом начале, ещё до того, как кому-либо покажет пакет.

Четыре аргумента. Ручка очереди, нужна только чтобы отправить вердикт. Заголовок сообщения, который тело не использует. Пакет в виде структуры библиотеки, из неё достаётся остальное. И блок данных из создания очереди. Возвращает приёмщик результат отправки вердикта, а сам вердикт уходит ядру.

Сначала заголовок пакета. Если его нет, функция возвращает ноль, автор в комментарии пишет, что так не бывает. Из заголовка берётся номер пакета в очереди. По общим сведениям, его присваивает ядро, и по нему ядро узнаёт, к какому пакету относится вердикт. Это не идентификатор в заголовке IP из второго выпуска и не порядковый номер TCP. В опыте t10 номера в очереди шли от одного до одиннадцати. Идентификаторы IP исходящих пакетов клиента на проводе: первый тридцать семь тысяч четыреста семьдесят, последний тридцать семь тысяч четыреста семьдесят пять.

Дальше метка и содержимое. Метка, mark, это число, которое было у пакета в момент постановки в очередь, то самое, которое проверяло правило ядра. Содержимое это байты пакета и их длина, и они начинаются с заголовка IP, а не с Ethernet.

Сверим со вторым выпуском. Запрос с двумястами пятьюдесятью байтами данных имеет в нашем журнале len триста два: двадцать байт IP, тридцать два байта TCP и двести пятьдесят данных. Кадр в записи был триста шестнадцать, разница четырнадцать байт, это Ethernet, в очередь он не попадает. Так же у SYN: кадр семьдесят четыре минус четырнадцать, шестьдесят. Простое подтверждение: шестьдесят шесть минус четырнадцать, пятьдесят два.

Проверь себя. В журнале len пятьдесят два. Сколько в пакете байт данных? Допустим, у заголовка TCP, как у наших пакетов после рукопожатия, тридцать два байта. Тогда двадцать плюс тридцать два как раз пятьдесят два, данных нет. В нашем опыте так выглядели простые подтверждения и пакеты закрытия соединения с флагом FIN.

Теперь имена интерфейсов. Ядро знает только номера. Автор оставил комментарий: обычная функция на каждый пакет создавала бы и закрывала сокет, а код держит один сокет на всё время работы, тот самый вспомогательный, и по его словам это экономит процессор в пять-десять раз. Мы этого не измеряли. Нулевой номер или ошибка вызова дают пустое имя. Позже движок по этим именам угадывает направление пакета, когда в памяти программы нет записи о соединении: входное имя непустое, а выходное пустое, значит пакет пришёл из сети.

Приёмщик nfq_cb: начало Приёмщик nfq_cb (nfqws.c:285-339, 55 строк): библиотека зовёт его один раз на каждый пакет, положенный ядром в очередь. Карточка функции. Вход: qh, ручка очереди, нужна только чтобы отправить вердикт; nfmsg, заголовок сообщения, тело его не использует; nfa, пакет в виде структуры библиотеки; cookie, блок данных из создания очереди (вспомогательный сокет и буфер для изменённого пакета). Выход: результат nfq_set_verdict2, вердикт уходит ядру, или 0. Вызывает: nfq_get_msg_packet_hdr, nfq_get_nfmark, nfq_get_payload, ioctl, processPacketData, nfq_set_verdict2. Вызывается библиотекой из nfq_handle_packet (nfqws.c:578). Начало функции по строкам: 296 заголовок пакета (если его нет, return 0, автор: так не бывает); 297 номер пакета в очереди id; 299 метка пакета mark; 300 содержимое и его длина ilen; 306-314 имена интерфейсов через ioctl на одном постоянном вспомогательном сокете, комментарий автора в 302-304 говорит, что это экономит процессор в 5-10 раз (мы не измеряли), нулевой номер или ошибка дают пустое имя; 316 строка журнала packet: id=… len=… mark=… ifin=… ifout=…. Всё это приёмщик делает до того, как кому-либо покажет пакет. приёмщик nfq_cb: nfqws.c:285-339, 55 строк входqhручка очередиnfmsgне используетсяnfaпакетcookieблок данных выходрезультатnfq_set_verdict2или 0, если нетзаголовка вызываетnfq_get_msg_packet_hdrnfq_get_nfmarknfq_get_payloadioctl, processPacketDatanfq_set_verdict2 вызываетсябиблиотекойиз nfq_handle_packetnfqws.c:578 начало функции по строкамчто делает строка 285static int nfq_cb(qh, nfmsg, nfa, cookie)четыре аргумента (сокращено) 296if (!(ph = nfq_get_msg_packet_hdr(nfa))) return 0;нет заголовка: return 0, «не бывает» 297id = ntohl(ph->packet_id);номер в очереди, его даёт ядро 299mark = nfq_get_nfmark(nfa);метка в момент постановки в очередь 300ilen = nfq_get_payload(nfa, &data);байты пакета с заголовка IP и длина 307ifr_out.ifr_ifindex = nfq_get_outdev(nfa);номер интерфейса выхода 308ioctl(cbdata->sock, SIOCGIFNAME, &ifr_out)номер → имя, один сокет на всё время 311то же для интерфейса входа (:311-314)пустое имя при номере 0 или ошибке 316DLOG("\npacket: id=%d len=%d mark=%08X …", …);строка журнала на каждый пакет Автор (:302-304): обычная функция открывала бы и закрывала сокет на каждый пакет;постоянный сокет, по его словам, экономит процессор в 5-10 раз. Мы не измеряли.

Теперь строка журнала, она стоит в начале каждого пакета. Наш запрос. Поле id равно четыре: третий исходящий пакет клиента, потому что номер два занял ответный SYN-ACK. Поле len триста два. Поле mark восемь нулей, метки нет. Поле ifin: имя пустое, в скобках ноль, входного интерфейса нет, обычно это пакет, который клиент отправляет сам. Поле ifout равно c0, в скобках три: уходит через интерфейс c0, а три это его номер в системе.

Для сравнения пакет номер два: len шестьдесят, ifin равно c0 и три, ifout пусто и ноль. Всё наоборот: пакет пришёл из сети, это ответный SYN-ACK. А mark у каждого из тысячи шестидесяти двух пакетов во всех журналах равен восьми нулям.

Строка журнала по полям Строка журнала, которую приёмщик пишет в начале каждого пакета (nfqws.c:316), разобранная по полям на примере запроса из опыта t10: packet: id=4 len=302 mark=00000000 ifin=(0) ifout=c0(3). id=4: номер пакета в очереди, его присваивает ядро; это третий исходящий пакет клиента, потому что номер 2 занял ответный SYN-ACK. len=302: байты пакета начиная с заголовка IP, без Ethernet: 20 IP + 32 TCP + 250 данных. mark=00000000: метки нет. ifin=(0): входного интерфейса нет, пакет клиент отправляет сам. ifout=c0(3): уходит через интерфейс c0, три это его номер в системе. Сверка с разбором пакета: кадр в записи 316 байт, минус 14 байт Ethernet, который в очередь не попадает, равно 302; так же SYN 74 − 14 = 60, простое подтверждение 66 − 14 = 52. Для сравнения пакет номер 2, ответный SYN-ACK: len=60, ifin=c0(3), ifout=(0). Правило: ifin пусто и ifout c0 означает исходящий пакет, ifin c0 и ifout пусто означает входящий; по этим именам движок позже угадывает направление, когда в памяти программы нет записи о соединении (desync.c:1291). Две шкалы не связаны: номера в очереди в опыте t10 шли от 1 до 11, а IP-идентификаторы исходящих пакетов клиента на проводе от 37470 до 37475. Во всех журналах 1062 пакета очереди, у каждого mark=00000000. строка журнала на каждый пакет: nfqws.c:316 packet: id=4 len=302 mark=00000000 ifin=(0) ifout=c0(3) id=4номер в очереди, даёт ядро: третий исходящий (2 занял SYN-ACK) len=302байты пакета с заголовка IP, без Ethernet: 20 + 32 + 250 mark=00000000метка в момент постановки в очередь; здесь метки нет ifin=(0)входного интерфейса нет: пакет клиент отправляет сам ifout=c0(3)уходит через c0; 3 это номер интерфейса в системе откуда 302: сверка с разбором пакета 14IP 20TCP 32данные 250 Ethernet 14: в очередь не попадает len = 20 + 32 + 250 = 302 кадр в записи: 316 = 14 + 302 так же: SYN 74 − 14 = 60, простое подтверждение 66 − 14 = 52 для сравнения: пакет id=2, ответный SYN-ACK packet: id=2 len=60 mark=00000000ifin=c0(3) ifout=(0) ifin пусто, ifout c0исходящий ifin c0, ifout пустовходящий Позже движок по этим именам угадывает направление,если в памяти нет записи о соединении (desync.c:1291). две шкалы, не связаны номер в очереди (опыт t10)1234567891011 IP-идентификатор исходящих клиента374703747137472374733747437475 номер даёт ядро; это не поле IP и не номер TCP 1062 пакета очереди: у всех mark=00000000

Проверь себя. В строке ifin пусто, ifout равно c0. Исходящий пакет или входящий? Исходящий. А если ifin равно c0, а ifout пусто? Входящий.

Если длина содержимого не отрицательна, пакет идёт движку, иначе его просто пропускают. Проходной приёмщик передаёт отдельный буфер для изменённого пакета, шестьдесят пять тысяч пятьсот тридцать шесть байт. Комментарий автора объясняет почему: приёмный буфер может содержать несколько сообщений подряд, и места для роста пакета в нём нет.

И про суммы. Комментарий автора в начале функции движка: операционная система может положить в очередь пакет с неправильной контрольной суммой, полагаться на неё нельзя. Мы такого в лаборатории не искали. В коде сказано, что Linux передаёт правильные суммы, а исправление суммы в исходном пакете включено только в сборках не для Linux. Как программа считает суммы своих пакетов, разберём в четвёртом выпуске.

Итак, приёмщик читает номер в очереди, метку, байты от заголовка IP и имена интерфейсов, пишет строку packet и вызывает проходную с отдельным буфером. Номер присваивает ядро, он не связан ни с идентификатором IP, ни с номером TCP.

Три штампа: проходная, байт вердикта, слово drop

Теперь что приёмщик делает с ответом движка. Главное правило части: на выходе из проходной всегда один из трёх штампов. Вопрос: как ответ движка превращается в ответ ядру?

У клерка на столе три штампа, PASS, MODIFY и DROP, и на письмо он ставит ровно один. А на полях штампа ещё две маленькие пометки, флажки: они тоже могут стоять, но за штамп не считаются.

Приёмщик вызывает проходную, функцию processPacketData, всего одиннадцать строк. Ей передают адрес метки, имена интерфейсов, байты пакета и длину, буфер для изменённого пакета и адрес его длины. Возвращает она один байт, вердикт. В первой строке проверка метки: если у метки есть бит программы, пакет пропускается без обработки, к этому вернёмся в третьей части. Пока считаем, что проверка не сработала, тогда проходная вызывает движок, dpi_desync_packet, и возвращает его ответ.

Что лежит в байте вердикта? Значение и служебные биты. Ноль это PASS, один MODIFY, два DROP. Число три приёмщик использует как маску: она оставляет два младших бита, а служебные отбрасывает.

Служебные биты в коде называются PRESERVE_NEXT, значение четыре, и NOCSUM, значение восемь, их смысл разберём позже. Допустим, в байте девять: это восемь плюс один, после маски остаётся один, MODIFY. Такое бывает: хитрость с первым пакетом соединения возвращает MODIFY с добавленным битом восемь. А десять, восемь плюс два, после маски два, DROP.

Проверь себя. Какой штамп получится, если в байте двенадцать? Двенадцать это восемь плюс четыре, оба служебных бита и нули в младших разрядах, после маски ноль, PASS. Это арифметика, таких значений в наших журналах не было.

Дальше оператор switch по значению вердикта, в нём два случая из трёх. Первый, MODIFY. Приёмщик пишет pass modified и две длины, исходного и нового пакета, и вызывает nfq_set_verdict2 с шестью аргументами: ручка очереди, номер пакета, вердикт NF_ACCEPT, то есть «принять», метка, длина новых байтов и сами байты из отдельного буфера. Пакет принят, но уйдёт с другими байтами. Второй случай, DROP: журнал drop, вердикт NF_DROP, байтов нет, пакет исчезает.

Байт вердикта и ответ ядру Проходная processPacketData (nfqws.c:154-164, одиннадцать строк) и байт вердикта. Вход: адрес метки mark, имена интерфейсов ifin и ifout, байты пакета data_pkt и len_pkt, буфер для изменённого пакета mod_pkt и адрес его длины len_mod_pkt. Выход: один байт вердикта. Строки 157-161: если у метки есть бит программы (params.desync_fwmark), пакет пропускается без обработки (VERDICT_PASS), к этому вернёмся в третьей части; строка 163: вызов движка dpi_desync_packet, его ответ возвращается. Байт вердикта: значение и служебные биты. Константы darkmagic.h:101-108: PASS 0, MODIFY 1, DROP 2, MASK 3 (оставляет два младших бита), служебные PRESERVE_NEXT 4 и NOCSUM 8. Примеры арифметики: 9 AND 3 = 1, MODIFY (хитрость с первым пакетом соединения возвращает MODIFY с битом 8); 10 AND 3 = 2, DROP; 12 AND 3 = 0, PASS; таких значений в наших журналах не было. Дальше приёмщик nfq_cb делает switch по verdict AND VERDICT_MASK (nfqws.c:327-338): MODIFY, строки 329-331, пишет «pass modified. len A => B» и зовёт nfq_set_verdict2 с шестью аргументами: ручка очереди qh, номер пакета id, вердикт NF_ACCEPT (1), метка mark, длина новых байтов и сами байты из отдельного буфера; DROP, строки 332-334, пишет «drop» и зовёт nfq_set_verdict2 с NF_DROP (0), без байтов; ветки PASS нет: всё остальное доходит до строк 337-338, «pass unmodified» и NF_ACCEPT без новых байтов, те же строки при ilen меньше нуля. Метка во всех трёх случаях возвращается та же, что пришла; что ядро делает с этим аргументом, не читали. проходная processPacketData: nfqws.c:154-164 157if (*mark & params.desync_fwmark)160 return VERDICT_PASS;163return dpi_desync_packet(*mark, …);бит программы есть: пропустить сразу, часть 3 входmark (указатель), ifin, ifout,data_pkt, len_pkt, mod_pkt,len_mod_pktвыход: один байт вердикта байт вердиктаконстанты: darkmagic.h:101-108 1286432168421 00001001пример: байт 9 = 8 + 1 значение служебные NOCSUM 8, PRESERVE_NEXT 4: смысл разберём позже PASS0 MODIFY1 DROP2 MASK3 PRESERVE_NEXT4 NOCSUM8 9 AND 3 = 1MODIFY1001 & 0011 = 0001 10 AND 3 = 2DROP1010 & 0011 = 0010 12 AND 3 = 0PASS1100 & 0011 = 0000 Служебные биты маска отбрасывает. Такие значения в наших журналах не встречались: это арифметика. приёмщик, switch (verdict & VERDICT_MASK): nfqws.c:327-338 MODIFY (1)pass modified. len A => Bnfq_set_verdict2(qh, id, NF_ACCEPT, mark, modlen, mod):329-331 DROP (2)dropnfq_set_verdict2(qh, id, NF_DROP, mark, 0, NULL):332-334 всё остальноеpass unmodifiednfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL):337-338 NF_DROP 0, NF_ACCEPT 1 (:49-50). Ветки PASS нет: те же строки и при ilen < 0.Метка уходит ядру та же, что пришла (:331, 334, 338). Что ядро делает с ней, не читали.

Ветки PASS нет. Если значение не MODIFY и не DROP, управление выходит из switch и доходит до двух последних строк: журнал pass unmodified и вердикт NF_ACCEPT без новых байтов. Те же строки выполняются, когда содержимое получить не удалось, тогда движок не вызывается. Метка во всех трёх случаях возвращается та же, что пришла: движку передаётся только её значение, так что в коде её никто не меняет. Что ядро делает с этим аргументом, не читали.

Статистика по всей лаборатории. В девяноста двух журналах тысяча шестьдесят два пакета очереди: девятьсот девяносто pass unmodified, двадцать два pass modified и пятьдесят drop. Девятьсот девяносто плюс двадцать два, тысяча двенадцать. Тысяча двенадцать плюс пятьдесят, тысяча шестьдесят два. У каждого пакета ровно одна строка с вердиктом.

Пример MODIFY, опыт h12: клиент отправляет не TLS, а обычный HTTP-запрос. Пакет сто тридцать три байта, из них восемьдесят один запрос: двадцать плюс тридцать два плюс восемьдесят один. Стратегия пишет имя в Host чередующимися большими и малыми буквами: было www.example.com, стало WwW.ExAmPlE.CoM. Длина прежняя: pass modified, len сто тридцать три в сто тридцать три.

Теперь предупреждение: слово drop не значит «данные потеряны». В нашей лаборатории оно встречается как минимум в трёх смыслах. Первый, запрос заменён вставленными частями. Опыт t10: запрос на двести пятьдесят байт получил drop, но программа заранее отправила его двумя частями, сто четыре и сто сорок шесть байт. Проверим: сто четыре плюс сто сорок шесть, двести пятьдесят.

Второй смысл: пакет удержан, пока программа дособирает запрос. Опыт r10: запрос в тысячу четыреста восемьдесят четыре байта в нашем тесте ядро само делит на тысячу четыреста сорок восемь и тридцать шесть. Оба пакета получают drop, а потом программа отправляет их сама, по одному. Посчитаем по проводу клиента: в очередь попало восемь исходящих, два выброшены, два отправлены программой, и на проводе снова восемь.

Третий смысл относится к вердикту стратегии, а не к строке журнала. Когда стратегия выбрасывает самый первый пакет соединения, программа подменяет его, и в журнале вместо drop стоит pass modified. В опыте t40 это SYN длиной шестьдесят: pass modified, шестьдесят в шестьдесят. Как это устроено и зачем, разберём в третьей части. И наоборот, pass unmodified тоже не значит, что ничего не произошло: так пишется и когда скрипт отработал и ничего не изменил, например стратегия pcap, и когда ничего не подошло.

Статистика вердиктов и три drop Статистика вердиктов и три смысла слова drop. По всей нашей лаборатории, 92 журнала: 1062 пакета очереди = 990 pass unmodified + 22 pass modified + 50 drop (990 + 22 = 1012, 1012 + 50 = 1062); у каждого пакета ровно одна строка с вердиктом. Пример MODIFY, опыт h12: клиент отправляет обычный HTTP-запрос, пакет 133 байта = 20 IP + 32 TCP + 81 запрос; стратегия пишет имя в Host чередующимися большими и малыми буквами, было www.example.com, стало WwW.ExAmPlE.CoM; длина прежняя: packet: id=4 pass modified. len 133 => 133. Слово drop не значит «данные потеряны». Смысл 1, запрос заменён вставленными частями (опыт t10): исходящий запрос 250 байт получил drop, но программа заранее отправила его двумя частями, 104 + 146 = 250. Смысл 2, пакет удержан, пока программа дособирает запрос (опыт r10): запрос 1484 байта ядро само поделило на 1448 и 36, оба получили drop, потом программа отправила их сама по одному; по проводу клиента: 8 в очереди − 2 выброшено + 2 отправлено = 8. Смысл 3, первый пакет соединения (опыт t40): когда стратегия выбрасывает самый первый пакет, программа подменяет его, и в журнале вместо drop стоит pass modified, у SYN длиной 60: pass modified. len 60 => 60; как это устроено, разберём в третьей части. И наоборот, pass unmodified не значит, что ничего не произошло: так пишется и когда скрипт отработал и ничего не изменил (например, стратегия pcap), и когда ничего не подошло. вердикты во всех журналах нашей лаборатории pass unmodified 990 22 pass modified50 drop 990 + 22 = 1012; 1012 + 50 = 1062 пакета очереди в 92 журналах: у каждого ровно одна строка с вердиктом MODIFY: опыт h12, обычный HTTP-запрос packet: id=4 pass modified. len 133 => 133133 = 20 + 32 + 81 Host:былоwww.example.com сталоWwW.ExAmPlE.CoM слово drop не значит «данные потеряны»: три смысла 1 заменён частямиопыт t10 2 удержан, пока дособираютопыт r10 3 первый пакет соединенияопыт t40 запрос 250 Б: drop104146части отправила программа:104 + 146 = 250 1448 + 36 = 1484: оба drop1448отправила программа сама8 в очереди − 2 + 2 = 8 SYN длиной 60pass modified. len 60 => 60вместо drop программаподменила пакет: часть 3 pass unmodified тоже не значит, что ничего не произошло: так пишется и когдаскрипт отработал и ничего не изменил (стратегия pcap), и когда ничего не подошло. Когда в журнале drop, смотрим, что программа отправила сама:в опыте t10 перед drop стоят две строки отправки, в r10 пакеты удержаны и отправлены позже.

Проверь себя. В журнале стоит drop. Данные потеряны? Не обязательно, нужно смотреть, что программа отправила сама: в опыте t10 перед строкой drop стоят две строки отправки, а в опыте r10 пакеты удержаны и отправлены позже.

Итак, MODIFY становится NF_ACCEPT с новыми байтами, DROP становится NF_DROP, всё остальное NF_ACCEPT с исходным пакетом, метка прежняя. Выброшенный при этом не значит потерянный.

Один настоящий пакет: вход, разбор, стратегии и вердикт

Теперь всё вместе на одном настоящем пакете. Опыт t10: стратегия multisplit разрезает запрос на сто четыре и сто сорок шесть байт по метке посередине слова example. Что это за стратегия, разберём в десятом выпуске, сейчас важен только путь пакета. Пакет номер четыре, запрос на двести пятьдесят байт, его блок в журнале пройдём по этапам.

Письмо входит в комнату. Клерк принимает его, читает адрес, проверяет печать, читает содержимое и ищет, какие правила на него есть.

Первая строка нам знакома, её печатает приёмщик: packet, id четыре, len триста два, mark восемь нулей, ifin ноль, ifout c0. Вторая от движка, это заголовок IP. От десять точка один точка ноль точка один к десять точка два точка ноль точка два, протокол tcp, время жизни шестьдесят четыре, порты тридцать четыре тысячи пятьсот сорок четыре и четыреста сорок три. Флаги A и P: подтверждение и «протолкнуть данные».

Третья строка про TCP. Len двести пятьдесят и первые байты: один-шесть, ноль-три, ноль-один, ноль-ноль, эф-пять. Это начало записи TLS из второго выпуска: первый байт тип, следующие два версия, последние два длина. Эф-пять в шестнадцатеричной записи это двести сорок пять, прибавим пять байт заголовка записи: двести пятьдесят. Тот же запрос.

Пакет номер четыре входит в программу Пакет номер четыре входит в программу: три строки журнала опыта t10. Строка 90 от приёмщика (nfqws.c:316): packet id=4 len=302 mark=00000000 ifin=(0) ifout=c0(3). Строка 91 от движка (desync.c:144), заголовок IP по полям: адреса 10.1.0.1 → 10.2.0.2, протокол tcp, время жизни 64, порты 34544 → 443, флаги A и P (подтверждение и «протолкнуть данные»). Строка 92 от движка (desync.c:160): TCP len=250 и первые байты 16 03 01 00 F5, это начало записи TLS: 16 тип, 03 01 версия, 00 F5 длина записи; 0x00F5 = 245, 245 + 5 байт заголовка записи = 250, противоречия нет. Дальше проходная (nfqws.c:157) проверяет метку: mark = 00000000, бита программы нет, и пакет идёт к движку dpi_desync_packet (desync.c:2232). пакет номер четыре входит в программу: три строки журнала 90packet: id=4 len=302 mark=00000000 ifin=(0) ifout=c0(3)приёмщикnfqws.c:316 91IP4: 10.1.0.1 => 10.2.0.2 proto=tcp ttl=64 sport=34544 dport=443 flags=APдвижокdesync.c:144 адреса10.1.0.1 → 10.2.0.2 протоколtcp время жизниttl 64 порты34544 → 443 флагиA: подтверждениеP: протолкнуть данные 92TCP: len=250 : 16 03 01 00 F5 01 00 00 F1 03 03 0B ...движокdesync.c:160 16 03 01 00 F5 типверсиядлина записи 00 F5 в шестнадцатеричной записи это 245 245 + 5 байт самого заголовка записи = 250 тот же запрос: len=250 в строке; противоречия нет проходнаяprocessPacketDatanfqws.c:157 проверка метки mark=00000000бита программы нет(по умолчанию бит 0x40000000) в движокdpi_desync_packetdesync.c:2232

Проверь себя. Длина записи двести сорок пять, а len в строке про TCP двести пятьдесят. Противоречие? Нет: длина записи считается без пяти байт самого заголовка записи.

Проходная проверяет метку: восемь нулей, бита программы нет, пакет идёт к движку. Движок разбирает заголовки IP и TCP, как мы делали руками во втором выпуске, и находит профиль, набор настроек для соединения. Профиль взят из кэша: он остался в памяти программы от SYN. Это первый профиль, называется один, noname, то есть без имени, а фильтр на порт четыреста сорок три он получил из параметра filter-tcp. Дальше движок определяет нагрузку: tls_client_hello, первое сообщение клиента в TLS, целое. Версия записи TLS один точка ноль, рукопожатия TLS один точка два, расширение версий называет TLS один точка три.

Движок достаёт имя www.example.com и заново ищет профиль по имени: профиль один найден. Разбор изучим в четвёртом и шестом выпусках, профили в седьмом, память о соединениях в пятом.

Дальше движок для каждой записанной в профиле стратегии сам проверяет диапазон и тип нагрузки, пишет, что нагрузка подходит под фильтр, и вызывает её. Первая, pcap, ничего не меняет, а только записывает пакет в файл, поэтому вердикт остаётся PASS. Вторая, multisplit, сообщает про cutoff. Это отметка внутри Lua: эта стратегия больше не будет вызываться для входящих пакетов этого соединения. Это не перенастройка ядра: пакеты с пятого по одиннадцатый всё равно попадут в очередь. Подробнее в восьмом выпуске.

Потом метка midsld, середина слова example, превращается в позицию сто четыре: номер байта, перед которым запрос будет разрезан.

Что движок делает с пакетом Что движок делает с пакетом номер четыре до вставок, строки журнала 93-116. 1) Профиль из кэша (строка 94, desync.c:1300): профиль «1 (noname)» остался в памяти от SYN, его фильтр на порт 443 задан параметром filter-tcp. 2) Вид нагрузки (95, desync.c:41): tls_client_hello; разбор печатает TLSDebug (desync.c:121): версия записи TLS 1.0, рукопожатия TLS 1.2, расширение версий называет TLS 1.3. 3) Имя и повторный поиск профиля (102-106, desync.c:1356, 331, 339): hostname www.example.com, профиль 1 найден. 4) Стратегии профиля (108-113, desync.c:1044, 1053-1054): для каждой движок проверяет диапазон и тип нагрузки и вызывает её; pcap_1_1 только записывает пакет в файл, вердикт остаётся PASS; multisplit_1_2 вызывается следом. 5) cutoff (114, nfq2/lua.c:924): instance cutoff for multisplit_1_2 in=1 out=0, отметка внутри Lua: этот экземпляр больше не вызывается для входящих пакетов соединения; правила ядра не менялись, пакеты с 5 по 11 всё равно приходят в очередь. 6) Метка midsld, середина слова example, становится позицией 104: номером байта, перед которым запрос будет разрезан (115-116, lua/zapret-antidpi.lua:499-501). движок разбирает пакет номер четыре: журнал, строки 93-116 1профиль из кэшаdesync.c:1300остался в памяти от SYN; называется 1 (noname), фильтр на порт 443 из параметра filter-tcp94 using cached desync profile 1 (noname) 2вид нагрузкиdesync.c:41, 121первое сообщение клиента в TLS; версии: запись 1.0, рукопожатие 1.2, расширение версий 1.395 packet contains tls_client_hello payload 3имя и поиск по имениdesync.c:1356, 331, 339имя достаётся из запроса, профиль ищется заново и находится тот же102 hostname: www.example.com 4стратегии профиляdesync.c:1044, 1053для каждой: диапазон, тип нагрузки, вызов. pcap только записывает (вердикт PASS)108-113 pcap_1_1, multisplit_1_2: desync 5cutofflua.c:924отметка внутри Lua: multisplit больше не вызывается для входящих пакетов соединения114 instance cutoff for 'multisplit_1_2' in=1 out=0 6метка midsld в числоzapret-antidpi.lua:499-501середина слова example: запрос будет разрезан перед байтом 104116 resolved split pos: 104 cutoff это не перенастройка ядра: пакеты с 5 по 11 всё равно приходят в очередьвызов cutoff из стратегии: direction_cutoff_opposite, zapret-antidpi.lua:489

Теперь вставка. Строка sending part один: байты с нулевого по сто третий, длина сто четыре. Сразу за ней rawsend_dissect: size сто пятьдесят шесть, badsum ноль, ifout c0, fwmark ноль-икс четыре и семь нулей. Сто пятьдесят шесть это двадцать байт IP, тридцать два TCP и сто четыре данных. Вторая часть: байты со сто четвёртого по двести сорок девятый, длина сто сорок шесть, size сто девяносто восемь: двадцать плюс тридцать два плюс сто сорок шесть. Про badsum в четвёртом выпуске, про метку в третьей части. Обе части уходят до вердикта по исходному пакету: он всё это время стоит у окошка.

Проверь себя. Если бы метка делила запрос на сто и сто пятьдесят байт, какие были бы size? Двадцать плюс тридцать два плюс сто, сто пятьдесят два, и двадцать плюс тридцать два плюс сто пятьдесят, двести два. Это только арифметика на тех же заголовках, такого прогона у нас не было.

Последняя строка блока: packet id четыре, drop. Стратегия вернула DROP, и ядру ушёл NF_DROP. Данные не пропали: те же двести пятьдесят байт ушли двумя частями, и сервер в нашем тесте собрал из них исходный запрос. Для сравнения опыт t01: стратегия pass только пишет слово pass. Блок тот же до самого разбора, потом строка LUA pass и сразу pass unmodified, строк отправки нет, исходный пакет уходит как был.

Две вставки и вердикт Две вставки и вердикт для пакета номер четыре, строки журнала 117-121. Запрос в 250 байт разрезан по метке midsld на позиции 104: часть 1 байты 0-103 (len 104, кончается на «…www.exa»), часть 2 байты 104-249 (len 146, начинается с «mple.com»). Каждая часть уходит как готовый пакет: 20 байт IP + 32 байта TCP + данные, size=156 для части 1 (20 + 32 + 104) и size=198 для части 2 (20 + 32 + 146); в журнале badsum=0, ifout=c0, fwmark=40000000 (nfq2/lua.c:3026). Порядок: сначала часть 1, потом часть 2, и только потом вердикт по исходному пакету, он всё это время стоит у окошка. Стратегия multisplit возвращает DROP, если не задан nodrop (lua/zapret-antidpi.lua:527), приёмщик пишет packet id=4 drop и отдаёт ядру NF_DROP (nfqws.c:333-334). Данные не пропали: сервер в нашем тесте получил куски 104 и 146 байт, всего 250, и собрал исходный запрос. Сравнение, опыт t01 со стратегией pass: те же строки до стратегии, потом «LUA: pass» (lua/zapret-lib.lua:43-45) и «pass unmodified» (nfqws.c:337-338), вставок нет, исходный пакет уходит как был. Арифметика без прогона: при метке на 100 и 150 были бы size 152 и 202. две вставки, потом вердикт: журнал, строки 117-121 запрос: 250 байт, номера 0-249байты 0-103байты 104-249 метка midsld, позиция 104…www.example.com… вставка 1, строки 117-118IP 20TCP 32данные 104rawsend_dissect size=15620 + 32 + 104 = 156 · fwmark=40000000 · badsum=0 вставка 2, строки 119-120IP 20TCP 32данные 146rawsend_dissect size=19820 + 32 + 146 = 198 · fwmark=40000000 · badsum=0 сервер в нашем тесте получил куски 104 и 146 байт, всего 250, и собрал исходный запрос порядок событий 1 часть 1 уходитстрока 118 2 часть 2 уходитстрока 120 3 вердикт по оригиналустрока 121 исходный пакет (302 байта) всё это время держит ядро у окошка и ждёт вердикта вердикт: DROPmultisplit: nodrop не задан, значит DROPlua/zapret-antidpi.lua:527nfq_set_verdict2(NF_DROP) · nfqws.c:333-334 сравнение: стратегия passте же строки до стратегии, потом LUA: passи pass unmodified, вставок нетzapret-lib.lua:43-45 · nfqws.c:337-338 данные не пропали: те же 250 байт ушли двумя частями проверь себя, только арифметика: если бы метка делила запрос на 100 и 150,size были бы 20 + 32 + 100 = 152 и 20 + 32 + 150 = 202; такого прогона у нас не было

Итак, движок узнал в пакете номер четыре запрос TLS с именем www.example.com, стратегия multisplit посчитала позицию сто четыре, отправила две вставки по сто четыре и сто сорок шесть байт и вернула DROP, а приёмщик передал ядру NF_DROP.

Счёт пакетов и время одного прогона

Последний вопрос про этот пакет: сходится ли счёт? Сколько пакетов прошло через очередь, сколько на проводе клиента и откуда берётся разница?

Пройдём одиннадцать пакетов очереди опыта t10 по группам. Рукопожатие. Номер один: исходящий SYN, длина шестьдесят. Номер два: входящий SYN-ACK от сервера, тоже шестьдесят. Номер три: исходящий ACK, пятьдесят два. Все три pass unmodified. Номер четыре это запрос, длина триста два, он получил drop, и две вставленные части в очередь не попали вообще: для них в журнале очереди нет ни одной строки packet. Почему вставки не вернулись в очередь, разберём в третьей части, в разговоре о метке.

Ответы сервера. Номер пять: входящее подтверждение первой части, пятьдесят два. Номер шесть: входящий короткий ответ в четырнадцать байт, длина шестьдесят шесть. Номер семь: входящее подтверждение второй части, пятьдесят два. Закрытие. Номер восемь: исходящий ACK. Девять: исходящий с флагами A и F. Десять: входящий такой же. Одиннадцать: исходящий ACK. Все они пятьдесят два байта и все pass unmodified.

Теперь счёт. В очереди одиннадцать пакетов. Исходящих шесть: номера один, три, четыре, восемь, девять и одиннадцать. Входящих пять: два, пять, шесть, семь и десять. Шесть плюс пять, одиннадцать. На проводе клиента двенадцать: семь исходящих и пять входящих. Откуда семь? Исходящих в очереди шесть, минус один выброшенный, плюс две вставленные части: шесть минус один плюс два, семь. Входящих пять, и они не меняются: в этом опыте входящие никто не выбрасывает и не добавляет.

Счёт пакетов: очередь и провод Счёт пакетов опыта t10: одиннадцать пакетов очереди по группам и две бухгалтерские колонки. Рукопожатие: № 1 исходящий SYN, len 60; № 2 входящий SYN-ACK, 60; № 3 исходящий ACK, 52. Запрос: № 4 исходящий PSH,ACK, len 302, вердикт drop; рядом две вставки 156 и 198 байт, в очереди их нет. Ответы сервера: № 5 входящий ACK, 52, подтверждает часть 1; № 6 входящий PSH,ACK, 66, ответ в 14 байт; № 7 входящий ACK, 52, подтверждает часть 2. Закрытие: № 8 исходящий ACK, № 9 исходящий FIN,ACK, № 10 входящий FIN,ACK, № 11 исходящий ACK, все по 52. Все пакеты, кроме № 4, получили pass unmodified. Очередь видела 11: исходящих 6 (№ 1, 3, 4, 8, 9, 11) и входящих 5 (№ 2, 5, 6, 7, 10), 6 + 5 = 11. На проводе клиента 12: исходящих 6 − 1 + 2 = 7 (один выброшен, две вставки добавлены), входящих 5, которых никто не меняет; 7 + 5 = 12. Для сравнения опыт t01 со стратегией pass: 6 исходящих и 4 входящих, то есть 10, и на проводе те же 10. В t10 входящих на один больше, потому что сервер подтвердил каждую из двух частей отдельным пакетом, это № 5 и № 7. опыт t10: одиннадцать пакетов очереди №напр.флагиlenвердиктчто это рукопожатие 1исх.SYN60pass 2вх.SYN,ACK60pass 3исх.ACK52pass запрос 4исх.PSH,ACK302dropзапрос TLS, 250 байт —исх.PSH,ACK156вставка 1: в очереди нет —исх.PSH,ACK198вставка 2: в очереди нет ответы сервера 5вх.ACK52passподтверждает часть 1 6вх.PSH,ACK66passответ в 14 байт 7вх.ACK52passподтверждает часть 2 закрытие 8исх.ACK52pass 9исх.FIN,ACK52pass 10вх.FIN,ACK52pass 11исх.ACK52pass очередь видела: 11исходящих 6: № 1, 3, 4, 8, 9, 11входящих 5: № 2, 5, 6, 7, 106 + 5 = 11 провод клиента, c0: 12исходящих: 6 − 1 + 2 = 7выброшен № 4, плюс две частивходящих 5: их никто не меняет7 + 5 = 12 сравнение: t01, pass6 исходящих + 4 входящих = 10на проводе те же 10, вставок нет почему в t10 входящих большесервер подтвердил каждую издвух частей отдельным пакетом:это № 5 и № 7 кроме № 4, все пакеты получили pass unmodified

Проверь себя. Сколько пакетов пройдёт через очередь и по проводу клиента в опыте t01, где стратегия pass ничего не вставляет? Десять: шесть исходящих и четыре входящих, и столько же на проводе. А почему в t10 входящим пакетом больше? Потому что сервер подтвердил каждую из двух частей отдельным пакетом, это номера пять и семь.

И время. Прогоны, записанные раньше, писали журнал и файл с пакетами на медленный диск; какой из двух файлов виноват, не разделяли. Цифры в них в разных замерах оказались от двадцати до двухсот с лишним раз больше, чем при повторе, по нашему объяснению из-за диска. Поэтому t10 повторили на локальном диске. Один такой повтор: стратегия pcap записала запрос через ноль целых пятьсот двадцать тысячных миллисекунды после первого SYN на проводе. Первая часть появилась на проводе ещё через ноль целых шестьсот девяносто шесть тысячных, вторая через ноль целых четыреста восемь тысячных после первой. Сложим: от записи запроса до выхода второй части одна целая сто четыре тысячных миллисекунды.

Порядок событий в повторе такой же, как в записанном прогоне, а числа намного меньше. Сервер в нём прислал одним подтверждением меньше, поэтому счёт пакетов берём из записанного прогона, а время из повтора. Это один прогон на одной машине, ориентир, а не измерение. В шести повторах тот же отрезок составил от нуля целых восьмидесяти семи сотых до одной целой девяноста восьми сотых миллисекунды, медиана одна целая двадцать одна сотая. Для настоящих компьютеров и сетей цифр у нас нет.

Время одного прохода пакета Время одного прохода пакета с запросом, повтор опыта t10 на локальном диске, миллисекунды от первого SYN на проводе. Стратегия pcap записала запрос через 0,520 мс; часть 1 появилась на проводе в 1,216 мс (+0,696 после записи), часть 2 в 1,624 мс (+0,408 после первой); от записи запроса до выхода второй части 1,624 − 0,520 = 1,104 мс. В шести повторах тот же отрезок от 0,87 до 1,98 мс (0,867; 0,886; 1,182; 1,247; 1,432; 1,984), медиана 1,21; показанные 1,104 лежат внутри разброса, но это один прогон, а не медиана. В прогоне, записанном раньше (журнал и файл с пакетами на медленном диске), тот же отрезок 24,792 мс, около 20 раз больше медианы повтора; по нашему объяснению из-за диска, какой из двух файлов виноват, не разделяли. Порядок событий в повторе тот же, масштаб другой. Один прогон на одной машине, ориентир, не измерение; для настоящих компьютеров и сетей цифр у нас нет. один повтор опыта t10 на локальном диске, миллисекунды от первого SYN на проводе 0,00,20,40,60,81,01,21,41,61,8 запрос записан0,520 часть 1 на проводе1,216 часть 2 на проводе1,624 +0,696+0,408 1,624 − 0,520 = 1,104 мс: от записи запроса до выхода части 2 шесть повторов: тот же отрезок, мс 0,81,01,21,41,61,82,0 0,871,98медиана 1,21 показанный прогон 1,104: внутри разброса, но это один прогон, не медиана тот же отрезок в прогоне, записанном раньше записанный прогон (журнал и файл с пакетами на медленном диске): 24,792 мс повтор на локальном диске: 1,104 мсмедиана шести повторов 1,21: в записанном прогоне около 20 раз больше (24,792 / 1,21) по нашему объяснению разница из-за диска; какой из двух файлов виноват, не разделялиОдин прогон на одной машине: ориентир, не измерение.Для настоящих компьютеров и сетей цифр у нас нет.

Итак, очередь видела одиннадцать пакетов, провод клиента двенадцать, и разница это ровно одна выброшенная и две вставленные части.

Итог части: что мы знаем и чего не проверяли

Подведём итог второй части. Мы вошли в программу и прошли по ней вслед за пакетом. Соберём, что узнали.

Путь по порядку. При запуске программа урезает права, создаёт сырые сокеты, запускает Lua, готовит буферы, и только потом настройщик создаёт очередь. Главный цикл ждёт, читает сообщения ядра и по одному передаёт приёмщику. Приёмщик читает номер, метку, байты и интерфейсы, пишет строку и вызывает проходную. Проходная проверяет метку и передаёт пакет движку, а приёмщик превращает ответ стратегий в один из трёх штампов.

Путь пакета по программе Путь по программе nfqws2 в Linux, итог второй части. Один раз при запуске, по порядку (nfqws.c): урезать права, dropcaps (463-464); создать сырые сокеты, rawsend_preinit (474); запустить Lua, lua_init (486); подготовить буферы, malloc (498); и только потом создать очередь, nfq_init (510). Дальше на каждый пакет: главный цикл nfq_main ждёт, читает сообщение ядра (recv, 566) и по одному передаёт библиотеке (nfq_handle_packet, 578); приёмщик nfq_cb (285) читает номер, метку, байты и интерфейсы, пишет строку журнала и вызывает проходную; проходная processPacketData (154) проверяет метку и передаёт пакет движку; движок dpi_desync_packet (desync.c:2232) разбирает пакет, находит профиль и запускает стратегии на Lua. Ответ стратегий приёмщик превращает в один из трёх штампов, PASS, MODIFY или DROP (nfqws.c:327-338), и отдаёт ядру вызовом nfq_set_verdict2 (nfqws.c:331, 334, 338). один раз при запуске, по порядку урезать праваdropcapsnfqws.c:463-464 сырые сокетыrawsend_preinitnfqws.c:474 запустить Lualua_initnfqws.c:486 буферыmallocnfqws.c:498 очередьnfq_initnfqws.c:510 на каждый пакет главный циклnfq_mainждёт, читаетсообщение ядра,отдаёт по одномуnfqws.c:444, 566, 578 приёмщикnfq_cbчитает номер, метку,байты, интерфейсы;пишет строку журналаnfqws.c:285 проходнаяprocessPacketDataпроверяет метку,отдаёт пакет движкуnfqws.c:154 движокdpi_desync_packetразбор, профиль,стратегии на Luadesync.c:2232 ответ назад стратегии на Luaфункции из профилякаждая может вернутьответ для пакета приёмщик: ответ в штампPASS · MODIFY · DROPодин из трёх штамповна каждый пакетnfqws.c:327-338 ядроnfq_set_verdict2получает вердикт ивыпускает или выбрасываетnfqws.c:331, 334, 338 запуск один раз; дальше для каждого пакета одна и та же цепочка до вердикта

У пакета в программе шесть выученных полей: номер в очереди, длина от заголовка IP, метка, входной интерфейс, выходной интерфейс и вердикт. Для нашего запроса: номер четыре, длина триста два, метка восемь нулей, входного интерфейса нет, выходной c0, вердикт drop. Ключевые числа: очередь двести, в очереди до четырёх тысяч девяноста шести пакетов. И тысяча шестьдесят два пакета по всем журналам: девятьсот девяносто пропущено, двадцать два изменено, пятьдесят выброшено.

Теперь честно о том, чего мы не проверяли. Внутрь библиотеки очереди мы не заглядывали, читали только заголовки. Мы не проверяли, что ядро делает при переполнении очереди и как работает fail-open. Мы не запускали программу с неправильным номером очереди и не останавливали её посреди работающего скрипта. Цены на настоящем оборудовании у нас нет, только оценка на одном компьютере. А про настоящее ТСПУ мы по-прежнему ничего не знаем, у нас только модель.

Карточка пакета и непроверенное Итог второй части: карточка пакета в программе, ключевые числа и то, что не проверяли. Шесть выученных полей пакета номер четыре из журнала: id 4 (номер в очереди), len 302 (длина от заголовка IP), mark 00000000 (метка), ifin (0) (входного интерфейса нет), ifout c0(3) (выходной интерфейс), вердикт drop. Ключевые числа: очередь 200; в очереди до 4096 пакетов (nfq2/params.h:27); буфер приёма 1048576 байт (params.h:28); 1062 пакета по всем журналам лаборатории: 990 пропущено без изменений, 22 изменено, 50 выброшено (1062 = 990 + 22 + 50); в опыте t10 очередь видела 11 пакетов, а на проводе клиента 12. Чего не проверяли: внутренности библиотеки очереди (читали только заголовки); что ядро делает при переполнении очереди и как работает fail-open; запуск с неправильным номером очереди; остановку посреди работающего скрипта; цены на настоящем оборудовании (есть только оценка на одном компьютере); про настоящее ТСПУ мы по-прежнему ничего не знаем, у нас только модель. Внизу карта третьей части: отправщик сырых пакетов, метка и ловушка петли, предел двенадцати, другие системы, первый пакет соединения. карточка пакета в программечего мы не проверяли idномер в очереди4 lenдлина от заголовка IP302 markметка00000000 ifinвходной интерфейс: нет(0) ifoutвыходной интерфейсc0(3) verdictвердиктdrop ?внутрь библиотеки очереди не заглядывали:читали только заголовки ?что ядро делает при переполнении очередии как работает fail-open ?запуск с неправильным номером очереди ?остановку посреди работающего скрипта ?цены на настоящем оборудовании: толькооценка на одном компьютере ?настоящее ТСПУ: у нас только модель,о самом устройстве мы ничего не знаем ключевые числа 200номер очередив лаборатории 4096пакетов в очередиparams.h:27 1048576буфер приёмабайт, params.h:28 1062= 990 + 22 + 50по всем журналам 11 и 12очередь и проводопыт t10 в третьей части отправщиксырых пакетов метка иловушка петли пределдвенадцати другиесистемы первый пакетсоединения

Что дальше? В третьей части обратная сторона: как программа сама отправляет пакеты через сырые сокеты, что за метка стоит на этих пакетах и почему они не возвращаются в очередь, почему вставленные пакеты не тратят предел в двенадцать, чем отличаются другие системы и что происходит с первым пакетом соединения.

Итак, мы знаем путь пакета через программу и увидели его на одном настоящем запросе. Осталось понять, как программа отправляет свои собственные пакеты.

Повтор и вставка пакета: сырой сокет

Это третья часть третьего выпуска. Её можно смотреть отдельно от первых двух, поэтому сначала короткий повтор.

Представьте комнату с окошком. Окошко это правило ядра: оно выбирает часть пакетов и кладёт их на ленту, это очередь, в нашей лаборатории номер двести. На другом конце сидит клерк, наша программа nfqws2. Он берёт пакеты по одному и ставит штамп: PASS, пропустить как есть, MODIFY, пропустить с заменой содержимого, или DROP, выбросить. Правило лаборатории берёт только начало разговора: по двенадцать первых пакетов в каждую сторону, для TCP на портах восемьдесят и четыреста сорок три и для UDP на порту четыреста сорок три.

В опыте t10 запрос на двести пятьдесят байт получил в очереди номер четыре и штамп DROP, но данные не пропали: вместо запроса с клиента ушли две вставки, в сто четыре и в сто сорок шесть байт. Обе отправила сама программа. Сегодня разберём, как.

Проверьте счёт. Через очередь прошло шесть исходящих пакетов клиента. Один выброшен, две вставки добавились: шесть минус один плюс два, семь. Входящих пять, в этом опыте их не меняют. На проводе клиента двенадцать пакетов, а очередь видела одиннадцать.

У части пять вопросов. Как программа сама отправляет пакеты? Что мешает вставкам вернуться к окошку и пойти по кругу? Тратят ли они двенадцать пакетов предела? Что будет, если выбросить самый первый пакет соединения? И как всё это выглядит на BSD и на Windows?

Начнём с первого. Игрушечный пример, не данные лаборатории. Обычное письмо вы приносите на почту только с вложением, а конверт, адреса и номер оформляет отделение. Но можно принести конверт, оформленный целиком, и тогда на нём всё решаете вы.

С пакетами так же. По общим сведениям о Linux, обычный сокет, через который работает браузер, принимает от программы только данные, а заголовки IP и TCP строит ядро. Сырой сокет принимает пакет целиком, начиная с заголовка IP, который собрала сама программа. В коде сокет создаётся с типом SOCK_RAW и протоколом IPPROTO_RAW, а в отправку уходит буфер, который начинается с заголовка IP. Зачем это нашей программе? Это наше объяснение, не цитата из кода: ей нужны пакеты с полями, которые выбрала она, например часть запроса с нужным порядковым номером, а обычный сокет поставил бы свои значения.

Обычная и сырая отправка Обычная и сырая отправка пакета. Игрушечный пример с почтой, не данные лаборатории: обычное письмо вы приносите на почту только с вложением, а конверт, адреса и номер оформляет отделение; можно принести конверт, оформленный целиком, тогда на нём всё решаете вы. С пакетами так же, по общим сведениям о Linux, не по цитате из кода: через обычный сокет программа отдаёт ядру только данные, заголовки IP и TCP строит ядро; сырой сокет принимает пакет целиком, начиная с заголовка IP, который собрала сама программа. В коде nfqws2 (nfq2/darkmagic.c): сокет создаётся с типом SOCK_RAW (строка 1881, функция rawsend_socket_raw), для Linux с протоколом IPPROTO_RAW (1941), а в отправку sendto уходит буфер data длиной len, который начинается с заголовка IP (2032). Зачем это программе, наше объяснение, не цитата из кода: ей нужны пакеты с полями, которые выбрала она сама, например часть запроса с нужным порядковым номером, а обычный сокет поставил бы свои значения. обычная и сырая отправкаигрушка: почта, не данные лаборатории обычная отправка сырая отправка вы несётевложение отделениеоформляетконверт адресаномервложение адресаномервложение отделениепринимаеткак есть на конвертевсё решаетевы то же с пакетом, общие сведения о Linux (не цитата из кода) программа даёт ядру только данные:программа даёт пакет целиком, с заголовка IP: IPядроTCPядроданныепрограмма IPсамаTCPсамаданныесама заголовки IP и TCP строит ядрозаголовки собрала программа сама в коде nfqws2, Linux: nfq2/darkmagic.c 1881int fd = socket(domain, SOCK_RAW, proto);rawsend_socket_raw, :1879сырой сокет: тип SOCK_RAW 1941*sock = rawsend_socket_raw(family, IPPROTO_RAW);ветка Linuxпротокол IPPROTO_RAW 2032bytes = sendto(sock, data, len, 0, ...);начинается с заголовка IP; строка сокращенауходит буфер data длиной len зачем: наше объяснение, не цитата из кода. Программе нужны пакеты с полями, которые выбрала она,например часть запроса с нужным порядковым номером; обычный сокет поставил бы свои значения.

Пакет, который программа собрала целиком и отправила сама, мы называем вставкой. Первая вставка t10 занимает сто пятьдесят шесть байт: двадцать байт заголовка IP, тридцать два байта заголовка TCP и сто четыре байта данных, начало запроса. Весь буфер готов ещё до отправки. Вставки не приходят из очереди, окошко их не выбирало.

Проверь себя. Кто выбрал порядковый номер TCP в такой вставке, программа или ядро? Программа: номер лежит в заголовке TCP, а заголовок уже внутри готового буфера.

Буфер первой вставки Раскладка буфера первой вставки опыта t10: 156 байт, готовых ещё до отправки: 20 байт заголовка IP, 32 байта заголовка TCP и 104 байта данных, начало запроса (в журнале rawsend_dissect size=156; первые байты данных 16 03 01 00 F5). Вставка не приходит из очереди: окошко, то есть правило ядра, её не выбирало. Буфер целиком уходит в сырой сокет вызовом sendto(sock, data, len, ...) (nfq2/darkmagic.c:2032). Проверь себя: порядковый номер TCP лежит в заголовке TCP (в записи пакетов у первой вставки 1196648707), а заголовок уже внутри готового буфера, поэтому его выбрала программа, не ядро. У второй вставки номер 1196648811 = 1196648707 + 104: программа сдвигает номер на длину первой части (lua/zapret-lib.lua:1287). первая вставка опыта t10: готовый буфер в 156 байт IP20 байт TCP32 байта данные104 байта: начало запроса 02052156номера байтов буфера заголовок IPзаголовок TCPпервые байты: 16 03 01 00 F5 ... в журнале: rawsend_dissect repeats=1 size=156 ...nfq2/lua.c:3026 порядковый номер TCP (seq)байты 4-7 заголовка TCP: 1196648707 sendto(sock, data, len, 0, ...)буфер data из 156 байт целиком уходит в сырой сокет · darkmagic.c:2032 собрала программа, не ядрозаголовок уже внутри готового буфера у вставки 2 номер 1196648811 = 1196648707 + 104: сдвиг на длину первой части (zapret-lib.lua:1287)числа из записи пакетов на интерфейсе клиента; вторая вставка устроена так же: IP 20, TCP 32, данные 146 вставка не приходит из очереди: окошко, то есть правило ядра, её не выбирало

Итак, вставка это пакет, который программа собрала целиком, вместе с заголовками, и отправила через сырой сокет. Дальше пройдём её путь от стратегии на Lua до провода.

Путь вставки: от стратегии до повторителя

Вопрос этой сцены: через какие функции вставка идёт от стратегии до отправщика?

Игрушечная картинка, не данные лаборатории: конвейер из трёх рук. Первая рука, стратегия на Lua, заполняет листок: какие у пакета поля и какие данные. Вторая превращает листок в настоящие байты и узнаёт адрес получателя. Третья отправляет. В программе тоже три таких участника.

Стратегия, например multisplit, сама режет данные на части и каждую отдаёт цепочке вспомогательных функций Lua: они собирают пакет из куска данных, подставляют данные и порядковый номер и при необходимости дробят по размеру сегмента. Последняя вызывает отправку по таблице, в коде rawsend_dissect: ей передают таблицу с полями пакета, о таблице поговорим в следующем выпуске. За этим именем стоит функция на C, переходник из Lua в C, luacall_rawsend_dissect. Он делает шесть шагов.

Путь вставки: три руки и цепочка вызовов Путь вставки от стратегии до провода. Игрушечный конвейер из трёх рук (не данные лаборатории): рука Lua заполняет листок с полями пакета и данными, рука-переходник на C превращает листок в настоящие байты и узнаёт адрес, рука-отправщик отправляет. В программе цепочка вызовов такая: multisplit (lua/zapret-antidpi.lua:483) отдаёт кусок данных в rawsend_payload_segmented (lua/zapret-lib.lua:1282), затем rawsend_dissect_segmented (1233) и rawsend_dissect_ipfrag (1197), дальше rawsend_dissect, за этим именем стоит C-функция luacall_rawsend_dissect (nfq2/lua.c:3000), она зовёт rawsend_rep (nfq2/darkmagic.c:2047), тот rawsend (darkmagic.c:1984), а тот sendto (darkmagic.c:2032). Помощники Lua не режут данные по стратегии: режет сама стратегия. Если отправка не получилась, multisplit возвращает PASS (zapret-antidpi.lua:523). Путь вставки: от стратегии до провода игрушка, не данные лаборатории рука 1: стратегия на Luaзаполняет листок:поля пакета и данные рука 2: переходник на Cлисток в настоящие байты,узнаёт адрес получателя рука 3: отправщикбайты уходят на проводчерез сырой сокет цепочка вызовов определение или вызов multisplitlua/zapret-antidpi.lua:483режет данные, каждый кусок отдаёт вниз rawsend_payload_segmentedlua/zapret-lib.lua:1282подставляет данные и порядковый номер rawsend_dissect_segmentedlua/zapret-lib.lua:1233fooling, IP id, при нужде дробит по MSS rawsend_dissect_ipfraglua/zapret-lib.lua:1197IP-фрагменты, если просили; иначе дальше luacall_rawsend_dissectnfq2/lua.c:3000в Lua зовётся rawsend_dissect rawsend_repnfq2/darkmagic.c:2047цикл: отправить repeats раз rawsendnfq2/darkmagic.c:1984сокет, метка, адрес sendtonfq2/darkmagic.c:2032ядро отправляет готовый пакет помощники Lua не режут данные по стратегии: режет сама стратегия; они собирают пакет и при нужде дробят по MSS отправка не удалась: multisplit возвращает PASS (zapret-antidpi.lua:523); ушедшие части остаются (по коду)

Шаг первый: переходник читает настройки отправки, то есть сколько раз повторить (поле repeats), какой интерфейс (ifout) и какая метка (fwmark), и параметры сборки, например badsum, «сделать неправильную контрольную сумму». Правило для повторов такое: ноль заменяется на один, отрицательное число это ошибка «отрицательное число повторов», а если настроек нет вовсе, повтор один, метка программная, интерфейс не задан.

Проверь себя. Стратегия передала повторов ноль: сколько раз уйдёт пакет? Один. Передала два? Два. Минус один? Ни разу, будет ошибка. А если настроек нет? Один раз.

Шаг второй: из таблицы строятся настоящие байты, как именно, разберём в следующем выпуске; если таблица неправильная, ошибка «неверные данные таблицы». Шаг третий: из готового заголовка IP достаётся адрес назначения; если заголовок не распознан ни как IPv4, ни как IPv6, ошибка «неверный заголовок IP».

Шаг четвёртый: строка в журнале. Для первой вставки t10 в ней повторов один, размер сто пятьдесят шесть, порча суммы выключена, интерфейс c0, метка ноль-икс четыре и семь нулей. В журнале эти поля подписаны repeats, size, badsum, ifout и fwmark. Во второй строке размер сто девяносто восемь.

Шаг пятый: вызов повторителя, в коде rawsend_rep. Это цикл: отправить те же байты столько раз, сколько просили, и остановиться на первой неудаче. Второй пример, опыт q12, QUIC поверх UDP. Повторов два, в журнале одна строка с размером шестьсот сорок восемь, а на проводе клиента две одинаковые датаграммы с разницей в шесть сотых миллисекунды. В каждой двадцать байт IP, восемь байт UDP и шестьсот двадцать байт данных. Исходная датаграмма уходит позже.

Шаг шестой: переходник возвращает в Lua «получилось» или «не получилось». Если не получилось, решает стратегия: multisplit возвращает PASS, исходный пакет идёт дальше как есть, а части, которые уже ушли, остаются на проводе. Это по коду, мы такого не запускали. Есть ещё Lua-функция с тем же именем rawsend, не путайте её с функцией на C из сцены про отправщик: она принимает не таблицу, а готовую строку байт, в остальном то же самое.

Шесть шагов переходника из Lua в C Шесть шагов переходника luacall_rawsend_dissect (nfq2/lua.c:3016-3028). Шаг 1 (строки 3017-3018): читает настройки отправки (repeats, ifout, fwmark) и параметры сборки (badsum); правило для repeats по lua.c:2938-2970: ноль заменяется на один, один остаётся одним, два остаются двумя, отрицательное число даёт ошибку «rawsend: negative repeats», а если таблицы настроек нет вовсе, повтор один, метка программная, интерфейс не задан. Шаг 2 (3021-3022): из таблицы строятся байты, при неправильной таблице ошибка «invalid dissect data». Шаг 3 (3024-3025): из заголовка IP берётся адрес назначения, иначе ошибка «bad ip4/ip6 header». Шаг 4 (3026): строка журнала, для двух вставок опыта t10: repeats=1 size=156 badsum=0 ifout=c0 fwmark=40000000, затем size=198. Шаг 5 (3027): rawsend_rep (nfq2/darkmagic.c:2047-2053) в цикле отправляет те же байты repeats раз и останавливается на первой неудаче; в опыте q12 (QUIC поверх UDP, repeats=2) на проводе клиента две одинаковые датаграммы по 648 байт (20 IP, 8 UDP, 620 данных) с разницей 0,06 мс, исходная в 1228 байт позже, на 2,41 мс. Шаг 6 (3028): в Lua возвращается true или false, при false решает стратегия. luacall_rawsend_dissect: шесть шагов (nfq2/lua.c:3016-3028, строки сокращены) 1 2 3 4 5 6 3016luaL_checktype(L,1,LUA_TTABLE);3017lua_rawsend_extract_options(L,2, &repeats, &fwmark, &ifout);3018lua_reconstruct_extract_options(L, 3, &keepsum, &badsum, …);3021if (!lua_reconstruct_dissect(L, 1, buf, &len, keepsum, …))3022 luaL_error(L, "invalid dissect data");3024if (!extract_dst(buf, len, (struct sockaddr*)&sa))3025 luaL_error(L, "bad ip4/ip6 header");3026DLOG("rawsend_dissect repeats=%d size=%zu badsum=%u …", …);3027b = rawsend_rep(repeats, (struct sockaddr*)&sa, fwmark, …);3028lua_pushboolean(L, b); 1настройки отправки и сборки:repeats, ifout, fwmark; badsum 2из таблицы строятся байты;ошибка: invalid dissect data 3адрес из заголовка IP;ошибка: bad ip4/ip6 header 4строка журнала (пример ниже):repeats size badsum ifout fwmark 5повторитель rawsend_rep:цикл, darkmagic.c:2047-2053 6в Lua: true или false;при false решает стратегия repeats: что станет (nfq2/lua.c:2938-2970) repeats = 0один раз (lua.c:2954) repeats = 1один раз repeats = 2два раза repeats = -1ошибка (2953): negative repeats таблицы нетодин раз, метка программы (2942-44) журнал t10, строки 118 и 120repeats=1 size=156 badsum=0 ifout=c0 fwmark=40000000repeats=1 size=198 badsum=0 ifout=c0 fwmark=40000000 rawsend_rep (nfq2/darkmagic.c:2049-2052) 2049for (int i = 0; i < repeats; i++)2050 if (!rawsend(dst, fwmark, ifout, data, len))2051 return false;2052return true; опыт q12, QUIC поверх UDP, repeats=2: клиент c0 648 Б0,00 мс648 Б0,06 мсисходная 1228 Б2,41 мс 648 = 20 IP + 8 UDP + 620 данных; обе вставки одинаковывремя по записи клиента; блоки не в масштабе

Итак, путь вставки: стратегия, вспомогательные функции Lua, отправка по таблице, переходник на C, повторитель, и дальше отправщик сырых пакетов.

Отправщик сырых пакетов: сокеты, пять строк и синхронность

Вопрос этой сцены: что делает отправщик сырых пакетов, в коде rawsend, который кладёт пакет на провод, и в какой момент вставка действительно уходит? Сначала посмотрим, что готовится заранее.

Игрушечная картинка, не данные лаборатории. Курьер не открывает рабочее место заново для каждой посылки: два места, для двух видов адресов, готовы с начала смены, а печать на ящик он ставит перед каждой посылкой. Так и в программе. Подготовка сокетов, в коде rawsend_preinit, при запуске создаёт два сырых сокета, для IPv4 и для IPv6. IPv4 обязателен: без него программа не запустится. IPv6 может не создаться только в одном случае, когда система сообщает, что такие адреса не поддерживаются, как на компьютерах с выключенным IPv6.

Реальный пример, журнал t10. Для каждого из двух сокетов запрошено две тысячи сорок восемь байт на приём. На отправку запрошено тридцать две тысячи семьсот шестьдесят восемь, в коде это половина от шестидесяти четырёх килобайт. Ядро показывает вдвое больше. Почему удвоение, это знание ядра, мы не проверяли, как и остальные настройки сокета. Важно одно: метки среди них нет.

Подготовка сокетов при запуске Подготовка сокетов при запуске: функция rawsend_preinit (nfq2/darkmagic.c:1977-1983) запоминает, включена ли привязка к интерфейсу (bind-fix для IPv4 и IPv6), и создаёт два сырых сокета, для IPv4 и для IPv6. Сокет IPv4 обязателен: без него программа не запустится (nfq2/nfqws.c:474). Сокет IPv6 может не создаться только с ошибкой EAFNOSUPPORT, то есть когда система сообщает, что такие адреса не поддерживаются, как на компьютерах с выключенным IPv6 (комментарий автора «allow ipv6 disabled systems»). Игрушка, не данные лаборатории: курьер с двумя рабочими местами, готовыми с начала смены. Журнал опыта t10, строки 19-24: для каждого из двух сокетов (fd=3 и fd=4) запрошено 2048 байт на приём и 32768 на отправку, ядро показывает 4096 и 65536, то есть вдвое больше; 32768 = 64 × 1024 / 2 (RAW_SNDBUF/2, nfq2/params.h:25 и darkmagic.c:1885), 2048 × 2 = 4096, 32768 × 2 = 65536. Почему удвоение, это знание ядра, мы не проверяли; метки среди настроек сокета нет. rawsend_preinit при запуске (nfq2/darkmagic.c:1977-1983) 1977bool rawsend_preinit(bool bind_fix4, bool bind_fix6)1979 b_bind_fix4 = bind_fix4;1980 b_bind_fix6 = bind_fix6;1981 // allow ipv6 disabled systems1982 return rawsend_socket(AF_INET)!=-1 && (rawsend_socket(AF_INET6)!=-1 || errno==EAFNOSUPPORT); 1979-1980запоминает, включена ли привязка к интерфейсу (bind-fix) 1982сокет IPv4 обязателен: нет сокета, нет запуска (nfqws.c:474) 1981-1982IPv6 может не создаться, только если errno == EAFNOSUPPORT игрушка, не данные лабораторииместо IPv4обязательноеместо IPv6необязательноеоба места готовы с начала смены журнал опыта t10, строки 19-24 сокетприём (байт)отправка (байт)fd=32048 → 409632768 → 65536fd=42048 → 409632768 → 65536слева запрошено программой, справа показано ядром откуда числа 64 × 1024 / 2 = 32768RAW_SNDBUF / 2 (params.h:25; darkmagic.c:1885) 2048 × 2 = 4096приём: запрошено 2048 (darkmagic.c:1886) 32768 × 2 = 65536отправка: запрошено 32768 удвоение: знание ядра, мы не проверяли;среди настроек сокета метки нет

Теперь rawsend по строкам. Первая: взять сокет нужного семейства, а если его ещё нет, создать; не вышло, значит, «не получилось». Вторая: поставить метку, это делает установщик метки, в коде set_socket_fwmark, и делает перед каждой отправкой, а не один раз при создании. Что за метка, разберём в сцене про петлю. Третья: скопировать адрес назначения и для IPv6 обнулить в копии порт, иначе, по комментарию автора, Linux вернёт ошибку «недопустимый аргумент», EINVAL.

Проверь себя. Отправляются подряд две вставки, и для одной нужна одна метка, для другой другая. Нужно ли открывать два сокета для IPv4? Нет: метка меняется на том же сокете перед каждой отправкой.

Четвёртая строка работает не всегда. Только если включена привязка к интерфейсу, параметр bind-fix, для этого семейства адресов, сокет привязывается к интерфейсу из аргумента: по комментарию автора, при правилах маршрутизации Linux может выбрать интерфейс неправильно. В нашей лаборатории оба bind-fix выключены, в журнале два нуля, значит, интерфейс c0 из строки журнала на отправку не влияет. Это вывод из чтения кода, отдельно мы его не проверяли. Пятая строка: сам вызов отправки sendto в цикле; повтор только если вызов прервал сигнал, любая другая ошибка пишется в журнал, и возвращается «не получилось». Один вызов, один пакет.

Отсюда главное свойство для t10: отправка синхронная. Когда rawsend вернулась, пакет уже передан ядру, а стратегия идёт дальше. Исходный пакет при этом ещё не отпущен, его судьбу решит вердикт, когда стратегия закончится. Поэтому в журнале t10 порядок такой: вставка, вставка и только потом «packet id four drop».

Размеры: сто пятьдесят шесть это двадцать плюс тридцать два плюс сто четыре, сто девяносто восемь это двадцать плюс тридцать два плюс сто сорок шесть. Тридцать два байта TCP мы вывели из записи трафика: заголовок длиннее минимального из-за параметров.

rawsend по строкам и порядок вставок Отправщик сырых пакетов rawsend (nfq2/darkmagic.c:1984-2042) по пяти шагам. 1 (строки 1987-1988): взять сокет нужного семейства адреса, нет сокета значит «не получилось». 2 (1990): поставить метку установщиком set_socket_fwmark перед каждой отправкой, а не один раз при создании. 3 (1994-1996): скопировать адрес назначения и для IPv6 обнулить в копии порт, иначе, по комментарию автора, Linux вернёт EINVAL. 4 (2013-2023): только если включена привязка к интерфейсу (bind-fix) для этого семейства адресов, сокет привязывается к интерфейсу через SO_BINDTODEVICE; в лаборатории журнал t10, строка 18, показывает bind-fix4=0 bind-fix6=0, значит привязки нет (вывод из чтения кода). 5 (2032-2040): sendto в цикле, повтор только при прерывании сигналом (EINTR), другая ошибка пишется в журнал и возвращается «не получилось». Игрушка, не данные лаборатории: один ящик и печать, которую меняют перед каждой посылкой. Отправка синхронная: в журнале t10 порядок такой: строка 117 часть 1, 118 rawsend_dissect size=156, 119 часть 2, 120 size=198 и только потом 121 «packet: id=4 drop». Размеры: 156 = 20 IP + 32 TCP + 104 данных, 198 = 20 + 32 + 146; 32 байта TCP выведены из записи трафика (заголовок с параметрами); в опыте q12 648 = 20 IP + 8 UDP + 620 данных. rawsend: пять шагов (nfq2/darkmagic.c:1984-2042, строки сокращены) 1 2 3 4 5 1987int sock=rawsend_socket(dst->sa_family);1988if (sock==-1) return false;1990if (!set_socket_fwmark(sock,fwmark)) return false;1994memcpy(&dst2,dst,salen);1995if (dst->sa_family==AF_INET6)1996 ((struct sockaddr_in6 *)&dst2)->sin6_port = 0;2013 if (!b_bind_fix4) goto nofix;2016 if (!b_bind_fix6) goto nofix;2023if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, …) == -1)2032while ((bytes = sendto(sock, data, len, …))<0 && errno==EINTR);2033if (bytes<0)2038 return false;2040return true; 1сокет по семейству адреса;нет сокета: «не получилось» 2метка перед каждой отправкой,а не один раз при создании 3копия адреса; у IPv6 порт 0,иначе EINVAL (комментарий 1996) 4привязка к интерфейсу толькопри включённом bind-fix 5sendto, повтор при EINTR;другая ошибка: журнал, false игрушка, не данные лаборатории: ящик и печатьящикодин и тот жепосылка 1: печать Aпосылка 2: печать Bпечать меняютперед каждойпосылкой журнал t10, строка 18initializing raw sockets bind-fix4=0 bind-fix6=0 bind-fix выключен: привязки нет, интерфейс c0на отправку не влияет (вывод из кода) журнал t10, строки 117-121: порядок 117sending part 1 … len=104часть 1 готова 118rawsend_dissect … size=156sendto вернулся: ушла 119sending part 2 … len=146часть 2 готова 120rawsend_dissect … size=198ушла 121packet: id=4 dropвердикт оригиналу отправка синхронная: вернулась, пакет уже у ядра размеры вставок 156 = 20 IP + 32 TCP + 104 данных 198 = 20 IP + 32 TCP + 146 данных 32 байта TCP выведены из записи трафика;в опыте q12: 648 = 20 IP + 8 UDP + 620 данных

Итак, сокеты создаются один раз при запуске, IPv4 обязателен. Отправщик берёт сокет, ставит метку перед каждой отправкой, копирует адрес, при включённой привязке к интерфейсу привязывает его и вызывает отправку. Отправка синхронная, поэтому вставки уходят раньше, чем исходный пакет получает вердикт.

Ловушка петли, метка и её значение

Теперь вопрос, который вы могли задать себе в прошлой сцене. Отправщик отправляет пакеты через то же ядро. Значит, вставка проедет мимо того же окошка?

Игрушечный пример, цифры выдуманные. Клерк режет письмо на две части. Обе едут мимо того же окошка, окошко кладёт обе к клерку, клерк режет каждую снова: два, четыре, восемь, и так по кругу. Это называется петлёй. В документации сказано прямо: без фильтрации по метке система может зависнуть или работать неправильно.

Что такое метка? По общим сведениям о Linux, это число, которое ядро хранит рядом с пакетом, пока тот внутри компьютера. В байты пакета оно не входит и на провод не уходит. В нашем лабораторном правиле стоит условие: meta mark и ноль-икс четыре и семь нулей равно нулю. По-человечески: к клерку пускаем только пакеты, у которых этого бита нет.

Два примера. Сначала правило: побитовое И оставляет только те биты, которые стоят в обоих числах. Обычный пакет, метка восемь нулей: побитовое И с ноль-икс четыре и семь нулей даёт ноль, условие выполнено, пакет идёт в очередь. Помеченный, метка ноль-икс четыре и семь нулей: И даёт само ноль-икс четыре и семь нулей, это не ноль, условие не выполнено, в очередь пакет не идёт. В метке программы стоит один бит, тридцатый, если нумеровать с нуля. Обычный пакет идёт в очередь, если остальные условия правила тоже выполнены.

Петля и метка: игрушка и правило ядра Ловушка петли и метка. Игрушка с выдуманными числами, не данные лаборатории: клерк режет письмо на две части, обе едут мимо того же окошка, окошко кладёт их к клерку, клерк режет каждую снова, писем становится 1, 2, 4, 8 и так по кругу; у окошка нужна табличка «с наклейкой «своё» не берём». Документация zapret2 говорит прямо: «Filtering by mark is necessary to prevent loops. Without this, the system may hang or malfunction.» (docs/manual.en.md:396). Правило ядра (docs/readme.md:83, в лаборатории такое же): meta mark and 0x40000000 == 0. Побитовое И оставляет только биты, стоящие в обоих числах. В метке программы 0x40000000 стоит один бит, тридцатый, если считать с нуля (2 в степени 30). Обычный пакет с меткой 0x00000000: И с маской даёт 0, условие «равно нулю» верно, пакет идёт в очередь, если остальные условия правила тоже выполнены. Помеченный пакет с меткой 0x40000000: И даёт 0x40000000, это не ноль, условие не выполнено, в очередь пакет не идёт. Метка по общим сведениям о Linux это число, которое ядро хранит рядом с пакетом; в байты пакета оно не входит и на провод не уходит. игрушка: петля метка и правило ядра игрушка, числа выдуманы, не данные лаборатории клеркрежет письмо окошкоберёт письма две части обе снова к клерку: круг 1 2 4 8 … писем: 1, 2, 4, 8 и так по кругу у окошка табличка: «с наклейкой „своё“ не берём» «Filtering by mark is necessary to preventloops. Without this, the system may hang ormalfunction.»документация zapret2: docs/manual.en.md:396 docs/readme.md:83 (в лаборатории то же условие)… meta mark and 0x40000000 == 0 … queue num 200 bypass маска правила 0x4000000030 побитовое И: остаются биты, стоящие в обоих числах обычный пакет, метка 0x00000000И с маской = 0x00000000: условие «равно нулю» верно,пакет идёт в очередь (если и остальное верно) помеченный пакет, метка 0x40000000И с маской = 0x40000000: не ноль, условие неверно,в очередь пакет не идёт бит 30, если считать с нуля: 0x40000000 = 2 в степени 30

Как метка попадает на пакет? Установщик метки просит ядро пометить сокет: вызывает setsockopt с параметром SO_MARK на сыром сокете. Так в Linux ставят метку на все пакеты, уходящие через сокет, это общее знание.

Теперь какое значение ставится. Отправщик ничего не добавляет, ставит ровно переданное. Бит добавляет переходник из Lua в C: он берёт поле fwmark из настроек и объединяет побитовым ИЛИ с меткой программы. ИЛИ значит: бит стоит в результате, если он стоит хотя бы в одном из двух чисел.

Метка программы на Linux по умолчанию ноль-икс четыре и семь нулей, её можно изменить параметром запуска fwmark, но ноль запрещён. Поэтому бит программы стоит всегда; автор написал в Lua в комментарии: бит метки будет установлен безусловно.

Возьмём t10 по шагам. Запрос стоит в очереди с меткой из восьми нулей. Стандартная вспомогательная функция Lua кладёт эту метку в поле fwmark. Переходник считает ноль ИЛИ ноль-икс четыре и семь нулей и получает ноль-икс четыре и семь нулей. Отправщик ставит значение на сокет, и в журнале у обеих вставок fwmark ноль-икс четыре и семь нулей.

Второй пример, арифметика по коду, в лаборатории такого значения не было. Допустим, стратегия передала ноль-икс два и семь нулей: так в готовых скриптах запуска помечаются пакеты режима после трансляции адресов. Ноль-икс два и семь нулей ИЛИ ноль-икс четыре и семь нулей даёт ноль-икс шесть и семь нулей: оба бита, бит программы на месте.

Проверь себя. Стратегия передала в поле fwmark ноль: что в журнале? Ноль-икс четыре и семь нулей. А если параметром запуска изменить метку программы, правило ядра увидит изменённый бит? Только если то же значение стоит и в правиле: так выходит из чтения кода, мы такого не запускали. Готовые скрипты запуска передают одно значение и в правила, и в программу.

Откуда берётся значение метки у вставки Откуда берётся значение метки у вставки, три звена. Звено 1, стратегия на Lua: rawsend_opts (lua/zapret-lib.lua:397-403) берёт fwmark = desync.arg.fwmark or desync.fwmark; в опыте t10 это метка исходного пакета из очереди, 00000000. Звено 2, переходник из Lua в C (nfq2/lua.c:2960): *fwmark = (uint32_t)lua_tolint(...) | params.desync_fwmark, то есть побитовое ИЛИ с меткой программы: 0 | 0x40000000 = 0x40000000. Звено 3, отправщик (nfq2/darkmagic.c:1990 и 1911): set_socket_fwmark вызывает setsockopt с параметром SO_MARK на сыром сокете перед каждой отправкой; отправщик ставит ровно переданное, ничего не добавляет; в журнале t10 у обеих вставок fwmark=40000000. Второй пример, арифметика по коду, в лаборатории такого значения не было: 0x20000000 | 0x40000000 = 0x60000000, оба бита стоят. Бит программы стоит всегда: в Lua-комментарии сказано, что бит метки будет установлен безусловно (zapret-lib.lua:396). Метка программы по умолчанию на Linux 0x40000000 (nfq2/desync.h:15), параметр --fwmark принимает 0xHEX или десятичное число, ноль отвергается (nfq2/nfqws.c:2527-2541). Готовые скрипты запуска передают одно значение и в правила ядра, и в программу (config.default:58, init.d/sysv/functions:76). три звена: откуда у вставки значение метки (опыт t10) Lua: rawsend_optslua/zapret-lib.lua:401fwmark = desync.arg.fwmark or desync.fwmarkметка исходного пакета00000000 переходник Lua в Cnfq2/lua.c:2960*fwmark=(uint32_t)lua_tolint(L,-1) | params.desync_fwmark;0 | 0x40000000= 0x40000000 отправщик: rawsendnfq2/darkmagic.c:1990, 1911if (!set_socket_fwmark(sock,fwmark)) return false;setsockopt(sock, SOL_SOCKET, SO_MARK, &fwmark, …)на сокете и в журналеfwmark=40000000 метка у отправщика ровно та, что ему передали; бит добавляет переходник второй пример: арифметика по коду, такой метки в лаборатории не было стратегия передала 0x20000000 (режим после NAT) метка программы 0x40000000, побитовое ИЛИ результат 0x60000000: стоят оба бита бит программы стоит всегда, так сказано в Lua:«…will be set unconditionally» (zapret-lib.lua:396) по умолчанию на Linux 0x40000000 (desync.h:15);параметр --fwmark: 0xHEX или число, ноль отвергается скрипты запуска передают одно значение и в правила,и в программу (config.default:58; init.d/sysv/functions:76)

Итак, вставка могла бы пойти по кругу, потому что отправляется через то же ядро. Защита построена на метке: у вставок, которые отправляют стандартные вспомогательные функции Lua, она равна метке исходного пакета, объединённой побитовым ИЛИ с меткой программы, по умолчанию ноль-икс четыре и семь нулей, и правило ядра помеченные пакеты в очередь не берёт.

Три преграды: правило, notrack и проверка в проходной

Вопрос этой сцены: какие три преграды стоят на пути помеченного пакета? Игрушечная картинка: помеченное письмо можно остановить в трёх местах, у окошка, у входа в хранилище записей и у стола клерка, и каждое место проверяет своё.

Первая преграда: правило ядра, которое мы читали в первой части выпуска. Проверка метки стоит в начале каждого правила очереди, в нашей лаборатории их четыре, два для TCP и два для UDP, по одному на направление. Пакет с битом программы в такое правило не входит и в очередь не идёт. Это самая дешёвая защита: пакет даже не доезжает до программы.

Вторая: отдельная цепочка predefrag на точке output с приоритетом минус четыреста один. В правиле из документации и в нашей лаборатории она отключает отслеживание для пакетов с битом программы; слово для этого notrack, «не отслеживать». По общим сведениям, такие пакеты не попадают в таблицу отслеживания соединений в ядре, в conntrack. Слово conntrack относится только к ядру, не путайте его с памятью программы о соединениях, о ней будет пятый выпуск.

Зачем это? Документация объясняет заботой о NAT: пакеты nfqws2 не должны проходить проверки NAT, иначе стандартные правила могут их выбросить. А почему notrack помогает ещё и от петли, это наш вывод, не слова документации: пакет без записи в таблице не проходит условие про счётчик пакетов соединения, и правило очереди с ним не совпадёт. В нашем тесте на UDP, о нём в сцене с лабораторными данными, одного notrack хватило. Где эта точка стоит среди других точек ядра, это знание ядра, мы не проверяли.

В правилах готовых скриптов notrack ставится не для всех помеченных пакетов, а для части: с битом режима после трансляции адресов, для фрагментов и по условию на флаги TCP.

Три преграды на пути помеченного пакета Три преграды на пути помеченного пакета (метка 0x40000000), рядом путь обычного пакета. Преграда 1, правило ядра (docs/readme.md:83): условие meta mark and 0x40000000 == 0 стоит в начале каждого правила очереди, в нашей лаборатории таких правил четыре (TCP и UDP, по одному на каждое направление); помеченный пакет в очередь не идёт и даже не доезжает до программы, это самая дешёвая защита. Преграда 2, цепочка predefrag на точке output с приоритетом −401 (docs/readme.md:91-92): правило «mark & 0x40000000 != 0x00000000 notrack» отключает для таких пакетов отслеживание соединений в ядре (по общим сведениям, они не попадают в таблицу conntrack); документация объясняет это заботой о NAT: пакеты nfqws2 не должны проходить проверки NAT (docs/manual.en.md:398-399); наш вывод, не слова документации: пакет без записи не проходит условие про счётчик пакетов соединения, и правило очереди с ним не совпадёт. Преграда 3, проходная в самой программе: processPacketData (nfq2/nfqws.c:154-164) пропускает помеченный пакет без обработки. Обычный пакет проходит условие первого правила и входит в очередь. Слово conntrack относится только к ядру: память программы о соединениях это другое. три преграды для пакета с битом программы (метка 0x40000000) 1правило окошкаметка проверяется первой 2цепочка predefragnotrack для помеченных 3проходная в программеprocessPacketData docs/readme.md:83meta mark and 0x40000000 == 0в начале каждого правилаочереди; в лаборатории ихчетыре: TCP и UDP, по одномуна направлениедешёвая защита: пакет недоезжает до программы docs/readme.md:91-92mark & 0x40000000 != 0x00000000 notracknotrack: пакет не попадает вconntrack ядра (общие сведения)документация (manual.en.md:398):пакеты не должны проходить NATнаш вывод: без записи условиепро счётчик пакетов не пройдёт nfq2/nfqws.c:154-164if (*mark & params.desync_fwmark)первые строки processPacketData,через неё идёт каждый пакетиз очереди; общий бит с меткойпрограммы: пишем в журнал«ignoring generated packet»и возвращаем PASS путь пакета обычный пакет, метка 0x00000000 помеченный пакет, метка 0x40000000 в очередьусловие верно не берём в очередь если дошёл: notrack если дошёл: PASS conntrack ядра и память программы о соединениях это два разных учёта: слово conntrack только про ядро

Третья преграда уже в самой программе: это первые строки проходной, функции processPacketData, через которую идёт каждый пакет из очереди. Условие: метка пакета, объединённая побитовым И с меткой программы, не равна нулю, то есть у них есть общий бит. Тогда в журнал пишется «ignoring generated packet», игнорируем сгенерированный пакет, и возвращается PASS. Иначе пакет идёт к движку.

Примеры. Метка ноль-икс четыре и семь нулей: И с меткой программы даёт не ноль, пакет игнорируется. Метка восемь нулей: ноль, пакет идёт к движку. Только бит режима после трансляции, ноль-икс два и семь нулей: с битом программы ноль, к движку. Оба бита, ноль-икс шесть и семь нулей: не ноль, игнорируется. Это арифметика по двум строкам кода, таких меток в лаборатории не было.

И две оговорки. Проверка идёт по И, а не на равенство: хватает одного общего бита. И она есть только в сборке для Linux, в вариантах для BSD и Windows её нет.

Проходная: проверка метки в программе Третья преграда в самой программе: первые строки функции processPacketData (nfq2/nfqws.c:154-164), через которую идёт каждый пакет из очереди. Строка 157: если метка пакета, объединённая побитовым И с меткой программы (params.desync_fwmark), не равна нулю, то есть у них есть общий бит, строка 159 пишет в журнал «ignoring generated packet» (игнорируем сгенерированный пакет), строка 160 возвращает PASS. Иначе строка 163 передаёт пакет движку, dpi_desync_packet. Блок под #ifdef __linux__ (строки 156 и 162): проверка есть только в сборке для Linux, в вариантах для BSD и Windows её нет. Проверка идёт по И, а не на равенство: хватает одного общего бита. Четыре случая при метке программы 0x40000000 (арифметика по двум строкам кода, таких меток в лаборатории не было): метка 0x40000000 даёт 0x40000000, не ноль, пакет игнорируется; метка 0x00000000 даёт ноль, пакет идёт к движку; только бит режима после трансляции адресов 0x20000000 даёт ноль, к движку; оба бита 0x60000000 дают 0x40000000, не ноль, игнорируется. nfq2/nfqws.c:154-164 метка пакета & метка программы 0x40000000 1 2 3 154static uint8_t processPacketData(…)156#ifdef __linux__157 if (*mark & params.desync_fwmark)159 DLOG("ignoring generated packet\n");160 return VERDICT_PASS;162#endif163 return dpi_desync_packet(*mark, ifin, …);164} 1157: условиеметка & метка программы не ноль: есть общий бит 2159-160: журнал и PASSпакет идёт дальше без обработки 3163: иначепакет получает движок dpi_desync_packet строки 156 и 162: только в сборке для Linux;в вариантах для BSD и Windows проверки нетИ, а не равенство: хватает одного общего бита метка программы 0x40000000 (nfq2/desync.h:15) метка 0x40000000И = 0x40000000, не ноль: игнорируем метка 0x00000000И = 0: пакет идёт к движку метка 0x20000000 (режим после NAT)И = 0: пакет идёт к движку метка 0x60000000 (оба бита)И = 0x40000000, не ноль: игнорируем арифметика по двум строкам кода:таких меток в лаборатории не было

Итак, преград три. Правило ядра не берёт помеченные пакеты в очередь. Цепочка predefrag в нашем правиле отключает для них отслеживание. А проходная пропускает помеченный пакет без обработки, если у него есть общий бит с меткой программы.

Что показала лаборатория: кто остановил вставки и тратят ли они предел

Вопрос этой сцены: работает ли защита от петли, какая из трёх преград её обеспечивает и тратят ли вставки предел в двенадцать пакетов?

Игрушечный пример. Среди писем у стола клерка нет ни одного с наклейкой «своё». Хорошо ли работает сторож у стола клерка? Не знаем: сторожа у окошка и у входа могли остановить все письма раньше.

Теперь числа. Во всех девяноста двух журналах лаборатории тысяча шестьдесят два пакета очереди, и у каждого метка из восьми нулей. Каждая строка о вставке содержит метку ноль-икс четыре и семь нулей. Строки «игнорируем сгенерированный пакет» нет ни в одном журнале.

Один опыт, t10. В нём одиннадцать записей очереди, номера с первого по одиннадцатый без пропусков. Две вставки ушли, пока шла обработка пакета номер четыре, а следующая запись идёт под номером пять. Вернись вставки в очередь, они заняли бы номера пять и шесть. Но пятый номер это подтверждение сервера.

Ни у одной записи нет длины сто пятьдесят шесть или сто девяносто восемь, а в файле, куда пишет сама программа, одиннадцать пакетов и нет ни ста четырёх, ни ста сорока шести байт данных. Это наш вывод по счёту номеров. Значит, ни один помеченный пакет до очереди не дошёл, но какая из трёх преград его остановила, по журналам не сказать: стоят все три.

Журналы: ни одного помеченного пакета в очереди Что показали журналы лаборатории: ни одного помеченного пакета в очереди. Цифры: 92 журнала, 1062 пакета очереди и у всех метка mark=00000000, 0 строк ignoring generated packet, а в каждой строке о вставке стоит fwmark=40000000. Опыт t10: в очереди 11 записей с номерами от 1 до 11 без пропусков (SYN клиента, SYN-ACK сервера, ACK клиента, запрос клиента, затем ещё семь записей: ответы, подтверждения и FIN). Две вставки ушли на провод, пока шла обработка пакета 4. Если бы они вернулись в очередь, получили бы номера 5 и 6, но пятый номер занимает подтверждение сервера, и в записях очереди нет ни длины 156, ни 198. В файле захвата самой программы 11 пакетов с байтами данных 0, 0, 0, 250, 0, 14, 0, 0, 0, 0, 0: ни 104, ни 146 байт данных нет. Наш вывод по счёту номеров: ни один помеченный пакет до очереди не дошёл, но какая из трёх преград его остановила, по журналам не видно: стоят все три. что показали журналы лаборатории: помеченных пакетов в очереди нет 92журналав лаборатории 1062пакета очереди,у всех mark=00000000 0строк «ignoringgenerated packet» fwmark=40000000в каждой строкео вставке опыт t10: записи очереди по номерам синие: пакеты клиента, серые: пакеты сервера 1SYN 2SYN-ACK 3ACK 4запросdrop 5ACK 6ответ 7ACK 8ACK 9FIN 10FIN 11ACK 5?6? вставки ушли на провод.Вернись они в очередь — они заняли бы номера 5 и 6,но пятый номер у ACK сервера В записях очереди нет ни длины 156, ни 198 (размеры двух вставок). файл захвата самой программы в том же опыте: байт данных в пакете 0 0 0 250 0 14 0 0 0 0 0 Пакетов 11, как и записей в очереди; байт данных 104 или 146 нет ни в одном: части до программы не дошли. Наш вывод по счёту номеров: ни один помеченный пакет до очереди не дошёл.Какая из трёх преград его остановила, по журналам не видно: стоят все три.

Поэтому мы поставили отдельный тест: изолированные пространства имён, пакеты UDP, то же ядро и та же сборка, программа вставляет две поддельные копии пакета. Только проверка метки в правиле: вставки в очередь не попали. Только notrack: тоже не попали. Убрали и то и другое: помеченные пакеты дошли до очереди, программа записала метку ноль-икс четыре и семь нулей, потом «игнорируем сгенерированный пакет» и «пропущен без изменений», пакеты доставлены, повторной вставки не было. Выходит, в нашем тесте каждой из двух первых преград хватает одной, проверка в проходной срабатывает, и приёмщик читает ту метку, которую поставил отправщик. Оговорка: это UDP, на TCP мы опыт не повторяли.

Проверь себя. Убрать только третью преграду: изменится ли что-то в записанных прогонах? Нет, помеченные пакеты до неё не доходили. Такого опыта мы не ставили, это рассуждение по журналам.

И два замечания по коду, мы их не запускали. Если бит в правилах ядра не совпадает с параметром fwmark программы, вставки попадут в очередь, третья преграда их пропустит, но каждый раз это круг через программу. И есть одно место с меткой ноль: сброс клиенту, который повторяет запрос, в логике автоматических списков доменов. В лаборатории оно не срабатывало, а вернётся ли такой пакет в очередь, зависит от правил.

Тест: что остановит вставку, если снять преграды Отдельный тест на пакетах UDP: порт 443, один пакет в 1200 байт, программа вставляет две поддельные копии с меткой, пространства имён изолированы, ядро и сборка те же. Таблица по трём преградам от петли: проверка метки в правиле ядра, notrack, проверка в проходной программы; проходную не снимали. Вариант A, только проверка метки: вставки в очередь не попали, в очереди 1 пакет. Вариант B, только notrack: тоже не попали, в очереди 1. Вариант C, ни проверки метки, ни notrack: помеченные пакеты дошли до очереди, в очереди 3 пакета, у двух вставок метка 40000000. Контроль D, все слои на месте: в очереди 1. В варианте C для каждой вставки в журнале строки: packet id=2 с mark=40000000, затем ignoring generated packet, затем pass unmodified. Доставлены все 3 датаграммы, повторной вставки не было: в журнале одна строка rawsend_dissect. Вывод для нашего теста: каждой из двух первых преград хватает одной, проверка в проходной срабатывает, приёмщик читает ту метку, которую поставил отправщик. Оговорка: только UDP, TCP не повторяли. Два замечания по коду, их не запускали. Первое: если бит в правилах ядра не совпадает с параметром fwmark программы, вставки попадут в очередь, третья преграда их пропустит, но каждый раз это круг через программу. Второе: сброс клиенту, который повторяет запрос, в логике автоматических списков доменов отправляется с меткой ноль (desync.c:442, функция auto_hostlist_retrans); в лаборатории это место не срабатывало, вернётся ли такой пакет в очередь, зависит от правил. Проверь себя: убрать только третью преграду, изменится ли что-то в записанных прогонах? Нет, помеченные пакеты до неё не доходили; такого опыта не ставили, это рассуждение по журналам. отдельный тест: что останавливает вставку, если снять преграды UDP, порт 443, один пакет в 1200 байт; программа вставляет две поддельные копии с меткой.Пространства имён изолированы, ядро и сборка те же, что в основных опытах. проверка меткив правиле ядраnotrackв predefragпроверкав проходнойдошлодо очереди?пакетовв очереди Aтолько проверка меткистоитснятастоитнет1 из 3 Bтолько notrackснятастоитстоитнет1 из 3 Cни того ни другогоснятаснятастоитда3 из 3 Dвсе слои, контрольстоитстоитстоитнет1 из 3 вариант C: помеченный пакет дошёл до очереди, и вот что сказал журнал packet: id=2 len=648 mark=40000000 ignoring generated packet packet: id=2 pass unmodified очередь в варианте C id 1mark=00000000 id 2mark=40000000 id 3mark=40000000 id 2 и 3: метка 40000000, это две вставкиДоставлены все 3 датаграммы. В журнале однастрока rawsend_dissect: второй вставки не было. В тесте каждой из двух первых преград хватило одной; проверка в проходной срабатывает:приёмщик читает ту метку, которую поставил отправщик. Оговорка: только UDP, TCP не повторяли. два замечания по коду: эти места мы не запускали бит в правилах не совпадает с fwmarkВставки попадут в очередь,третья преграда их пропустит,но каждый раз это круг через программу.вывод из чтения кода сброс клиенту с меткой нольdesync.c:442, функция auto_hostlist_retrans:клиент повторяет запрос, ему идёт RST.Вернётся ли пакет в очередь, зависит от правил.В лаборатории это место не срабатывало. Проверь себя: убрать только третью преграду, изменится ли что-то в записанных прогонах? Нет:помеченные пакеты до неё не доходили. Опыта не ставили, это рассуждение по журналам.

Теперь вопрос о пределе: тратят ли его вставки? Напомним, считаются первые двенадцать пакетов каждого направления, SYN это номер один, у каждой стороны счёт свой. Игрушка: у входа турникет с табло на двенадцать человек, курьер проходит через служебную дверь с наклейкой «своё» и деления не зажигает. Проверим.

Опыт t20. Стратегия вставляет поддельные пакеты без обманных приёмов: сервер принимает их за настоящие данные, клиент ответа так и не получает и повторяет отправку, поток получается длинным. На проводе клиента тринадцать собственных исходящих пакетов и шесть вставленных поддельных, по шестьсот восемьдесят байт данных. В очередь попало ровно двенадцать собственных. Тринадцать минус двенадцать: один пакет, подтверждение, вышел за предел.

Теперь главное. Когда в очередь пришёл двенадцатый собственный пакет, пять поддельных уже были вставлены. Если бы они считались, счётчик показал бы двенадцать плюс пять, семнадцать, и двенадцатый вышел бы за предел. Но правило сработало, значит, вставки предел не потратили. Это согласуется с notrack, но по косвенным признакам: по общим сведениям, пакеты, к которым правило применило notrack, не попадают в таблицу соединений ядра, а ограничитель считает именно её.

Опыт t20: вставки предел не тратят Тратят ли вставки предел «первые 12 пакетов в каждую сторону»: опыт t20. Игрушка: турникет с табло на 12 делений зажигает деление за каждого обычного человека, а курьер проходит через служебную дверь с наклейкой «своё» и деления не зажигает. Числа опыта: на проводе клиента 13 собственных исходящих кадров, вставленных поддельных 6, по 680 байт данных (размер вставки 732 = 20 + 32 + 680), в очереди исходящих ровно 12; 13 − 12 = 1, один пакет, подтверждение, вышел за предел. Ось времени исходящих, расстояния не в масштабе: пять вставок отправлены, затем в очередь пришёл двенадцатый собственный пакет (номер в очереди 24), шестая вставка позже. Если бы вставки считались, счётчик показал бы 12 + 5 = 17 и двенадцатый пакет вышел бы за предел; но правило сработало, значит вставки предел не потратили. Это согласуется с notrack, но по косвенным признакам: по общим сведениям, пакеты, к которым правило применило notrack, не попадают в таблицу соединений ядра, а ограничитель считает именно её. игрушка: турникет и служебная дверь 12 обычный пакеттурникет вставкаслужебная дверь «своё» деления не зажигает опыт t20: сколько пакетов где собственные исходящие пакеты на проводе13 вставленные поддельные, по 680 Б данных6 исходящих в очереди12 13 − 12 = 1: один пакет (ACK) вышел за пределразмер вставки 732 = 20 + 32 + 680, где 680 это данные исходящие пакеты клиента во времени (расстояния не в масштабе) 1 2 3 4 5 5 вставок уже отправлены 12-й собственный пакетв очереди, id 24 6-я вставка, позже если бы вставки считались:12 + 5 = 17двенадцатый вышел бы за предел но правило сработало на 12-м пакете:вставки предел не потратили Это согласуется с notrack, но по косвенным признакам: по общим сведениям, пакеты,к которым правило применило notrack, не попадают в таблицу соединений ядра,а ограничитель считает именно её.

Прямая проверка из того же теста, на пакетах UDP: один обычный пакет, пять вставленных сырым сокетом с меткой, одиннадцать обычных. Сначала посчитаем сами. Если вставки считаются, после шести пакетов счётчик показывает шесть, остаётся шесть, правило сработает на шести из одиннадцати обычных, и всего один плюс шесть, семь. Если не считаются, правило сработает на всех: один плюс одиннадцать, двенадцать.

В основных опытах предел на UDP не проверялся, в нашем тесте проверен: с notrack двенадцать срабатываний, без него семь. Без notrack вставки съедали предел. Учитывается ли выброшенный пакет, мы не проверяли: в t20 ничего не выбрасывалось.

UDP: 12 срабатываний с notrack и 7 без него Прямая проверка предела на пакетах UDP из нашего теста. Порядок отправки в одном 5-кортеже: 1 обычный пакет, 5 пакетов, вставленных сырым сокетом с меткой, и 11 обычных; сервер получил 17. С notrack вставки в счёт ограничителя не идут: все 12 обычных пакетов получили номера с 1 по 12, правило сработало 12 раз, 1 + 11 = 12. Без notrack вставки идут в счёт: после шести пакетов счётчик показывает 6, остаётся 6, правило сработало на шести из одиннадцати обычных, всего 1 + 6 = 7; пакеты с номерами с 13 по 17 прошли мимо. Итог: без notrack вставки съедали предел, с notrack нет. Учитывается ли выброшенный пакет, не проверяли: в опыте t20 ничего не выбрасывалось. В основных опытах предел на UDP не проверялся, в нашем тесте проверен. Проверь себя: результат 7 означает, что вставки считались, результат 12 означает, что нет. прямая проверка предела на UDP: считаются ли вставки Один 5-кортеж: 1 обычный пакет, 5 вставленных сырым сокетом с меткой, 11 обычных; сервер получил 17. порядок отправки N обычный пакет, M вставка с меткой NMMMMMNNNNNNNNNNN с notrack: номер в счёте ограничителя у каждого пакета 1—————23456789101112 вставки в счёт не идут: правило сработало 12 раз, 1 + 11 = 12 без notrack: вставки получают номера в счёте 1234567891011121314151617 после шести пакетов счёт 6, остаётся 6: 1 + 6 = 7 срабатываний Без notrack вставки съедали предел: в очередь не попадают, но двигают счёт, с номера 13 правило мимо. Выброшенный пакет в счёте? Не проверяли: в t20 ничего не выбрасывалось.В основных опытах предел на UDP не проверялся, в этом тесте проверен. Только UDP. Проверь себя. Результат 7: вставки считались? Да, считались.А результат 12? Нет, не считались.

Проверь себя. Результат семь: вставки считались или нет? Считались. А двенадцать? Нет.

Итак, в лаборатории ни один помеченный пакет не дошёл до очереди, но какая из трёх преград его остановила, сказать нельзя: стояли все три. Наш тест на UDP показал, что каждой из двух первых преград хватает одной, а третья срабатывает, если убрать первые две. И вставки, судя по t20 и этому тесту, предел в двенадцать пакетов не расходуют: двенадцать срабатываний с notrack и семь без него.

Хитрость с первым пакетом: опыты t40 и t10

Вопрос этой сцены: что делает программа, когда стратегия выбрасывает самый первый пакет соединения, и чем отличается t10, где выброшен не первый?

Игрушечная картинка, наша иллюстрация к комментарию автора, а не проверенный факт. Ядро заводит запись о разговоре по первому пакету; если изъять его совсем, по словам автора, запись может развалиться. Тогда клерк подменяет первый пакет нарочно испорченной копией: по комментарию автора, у неё недопустимый протокол и малое время жизни. Что с ней происходит дальше, мы увидим в опыте.

Теперь реальный код. В конце обработки TCP-пакета в движке стоит строка: если пакет идёт от нашей стороны соединения и стратегия вернула DROP, вызвать хитрость с первым пакетом, в коде ct_new_postnat_fix; иначе вернуть вердикт как есть. То есть только для нашего направления и только для DROP, потому что, по нашему пониманию, DROP единственный штамп, при котором пакет исчезает. Такая же строка есть и в функции для UDP.

Первый пакет: игрушка и вызов в конце обработки Игрушечная картинка, наша иллюстрация к комментарию автора, не проверенный факт: ядро заводит запись о разговоре по первому пакету; если изъять его совсем (штамп DROP), по словам автора, запись может развалиться; тогда клерк подкладывает вместо него нарочно испорченную копию: по комментарию автора, у неё недопустимый протокол и малое время жизни. Код: в конце обработки TCP-пакета, desync.c:1660, стоит условие: пакет нашей стороны соединения (не ps.bReverseFixed) и стратегия вернула DROP; тогда вызывается ct_new_postnat_fix, иначе вердикт возвращается как есть. Анимация: штамп DROP на пакете номер один заменяется янтарным штампом MODIFY. Такая же строка есть в функции для UDP, desync.c:1993. DROP единственный штамп, при котором пакет исчезает, поэтому проверка только для него: это наше понимание. наша картинка к комментарию автора, не проверенный факт 1. первое письмо заводитзапись о разговорепакет 1запись естьтак бывает при обычной работе 2. письмо изъято (DROP):запись, по словам автора,может развалитьсяпакет 1запись?письмо не дошло до записиDROP 3. клерк подкладываетнегодную копиюкопиязапись целапротокол 255, время жизни 1по комментарию автора что стоит в конце обработки TCP-пакета: desync.c:1659-1660 1659pass: 1660return (!ps.bReverseFixed && (ps.verdict & VERDICT_MASK)== VERDICT_DROP)? ct_new_postnat_fix(ps.ctrack, dis, mod_pkt, len_mod_pkt): ps.verdict; строка 1660 перенесена на несколько строк для показа пакет 1 DROP MODIFY штамп DROP сталянтарным MODIFY Условие: пакет нашей стороны (не обратное направление) и стратегия вернула DROP.Тогда вызывается ct_new_postnat_fix; иначе вердикт возвращается как есть. Такая же строка есть в функции для UDP: desync.c:1993.Проверка только для DROP: по нашему пониманию, это единственный штамп, при котором пакет исчезает.

Условие внутри состоит из двух частей, достаточно любой. Первая: запись о соединении в памяти программы есть, и счётчик пакетов нашей стороны равен единице, то есть это первый пакет, который программа видела от этой стороны; в режиме сервера берётся счётчик стороны сервера. Про счётчики подробно в пятом выпуске. Вторая: записи нет вовсе, и пакет должен быть SYN или SYN-ACK.

Если условие выполнено и пакет помещается в буфер, функция копирует пакет и меняет два поля. Для IPv4 протокол в заголовке IP становится двести пятьдесят пять, а время жизни единица; для IPv6 то же с полями следующего заголовка и предела переходов. Возвращается MODIFY с флагом «не пересчитывать суммы». Автор пишет, что это, вероятно, делает неверной и контрольную сумму заголовка IPv4.

Если пакет не помещается, в журнал идёт ошибка «обходной путь применить нельзя» и пакет выбрасывается; если условие не выполнено, тоже обычный DROP. Всё это только в сборке для Linux.

Зачем это? Единственное объяснение в коде это комментарий автора: если правила стоят после перевода адресов, выброс самого первого пакета приведёт к разрушению записи о соединении в таблице ядра. Это его слова, мы этого не проверяли и не утверждаем.

Функция ct_new_postnat_fix по строкам Функция ct_new_postnat_fix (desync.c:640-673) по строкам. Весь код внутри только для Linux (#ifdef __linux__, строка 642); в других сборках сразу возвращается обычный DROP (672). Комментарий автора (643-645): если правила стоят после перевода адресов, выброс первого пакета приведёт к разрушению записи о соединении; это слова автора, мы этого не проверяли. Условие (646-647), достаточно любой из двух частей: запись о соединении есть и счётчик пакетов нашей стороны равен единице (в режиме сервера берётся счётчик стороны сервера), или записи нет и пакет это SYN или SYN-ACK. Если пакет не помещается в буфер (649-650), в журнал пишется ошибка и дальше обычный DROP. Иначе: копия всего пакета (653), строка в журнал «applying linux postnat conntrack workaround» (654), для IPv6 следующий заголовок 255 и предел переходов 1 (658-659), для IPv4 протокол 255 и время жизни 1 (664-665; по комментарию автора 663, это, вероятно, делает неверной и контрольную сумму заголовка IPv4), возвращается MODIFY с флагом NOCSUM, суммы не пересчитывать (667-668). Иначе обычный DROP (672). Что меняется в копии: протокол IP 6 → 255, время жизни 64 → 1, длина пакета не меняется; значения «до» только для иллюстрации. ct_new_postnat_fix: desync.c:640-673, только для Linux что делает строка 640ct_new_postnat_fix(ctrack, dis, mod_pkt, len_mod_pkt)запись, пакет, буфер для копии 642#ifdef __linux__только сборка для Linux 643// if used in postnat chain, dropping initial packet…слова автора, не проверяли 646if ((ctrack && …pcounter == 1) ||вариант 1: первый пакет стороны 647(!ctrack && dis->tcp && (tcp_syn_segment(…) || …вариант 2: нет записи, SYN 649if (dis->len_pkt > *len_mod_pkt)не помещается в буфер: 650DLOG_ERR("…workaround cannot be applied\n");ошибка в журнал, дальше DROP 653memcpy(mod_pkt, dis->data_pkt, dis->len_pkt);копия всего пакета 654DLOG("applying linux postnat conntrack workaround\n");строка журнала 658-659ip6_un1_nxt = 255; ip6_un1_hlim = 1;IPv6: поля 255 и 1 664-665ip_p = 255; ip_ttl = 1;IPv4: поля 255 и 1 668return VERDICT_MODIFY | VERDICT_NOCSUM;MODIFY, суммы не пересчитывать 672return VERDICT_DROP;иначе обычный DROP Код сокращён там, где строка не помещается: 640, 643, 646-647, 650 (многоточие). что меняется в копии (значения «до» для иллюстрации) протокол в заголовке IP6 → 255было TCP, стало недопустимое время жизни пакета64 → 1малое время жизни длина пакета60 → 60не меняется, копия целиком

Возьмём опыт t40, со стратегией syndata; её разберём в выпуске про хитрости рукопожатия. Первый пакет соединения, SYN длиной шестьдесят байт: двадцать байт IP и сорок байт TCP с параметрами. Стратегия вставляет другой SYN с шестнадцатью байтами данных, длина семьдесят шесть, и возвращает DROP для исходного. Журнал, четыре строки. Пакет номер один, длина шестьдесят. Вставка по таблице, размер семьдесят шесть, метка ноль-икс четыре и семь нулей. Запись о применении обходного пути. И запись «пропущен изменённым, длина шестьдесят в шестьдесят»: стратегия вернула DROP, а приёмщик отдал ядру изменённый пакет той же длины, ведь копируется весь пакет и меняются два поля.

На проводе клиента сначала вышел вставленный SYN, через три целых семь десятых миллисекунды исходный пакет, уже не как TCP SYN, а как пакет IPv4 с протоколом двести пятьдесят пять и временем жизни один. На входе роутера он записан, а на втором интерфейсе роутера и на сервере его нет. Почему, мы не знаем. Известно два факта: у пакета неверная контрольная сумма заголовка IP, и сообщения об ошибке на клиент не пришло. Причину они не показывают, поэтому мы не говорим, что его остановило время жизни, равное единице.

Ещё одно открытое наблюдение. Следующие два пакета очереди, подтверждение и запрос, программа пропустила без изменений, но ни в одной точке записи они не появились. Запрос клиент, похоже, повторил на отметке примерно двести девяносто четыре миллисекунды от начала записи, и ответ сервера в итоге пришёл. А куда делись эти два пакета, мы не выяснили.

Опыт t40: журнал и провод Опыт t40, стратегия syndata: выброшен самый первый пакет соединения, SYN длиной 60 байт (60 = 20 байт IP + 40 байт TCP с параметрами). Журнал, четыре строки: packet id=1 len=60; rawsend_dissect repeats=1 size=76 и fwmark=40000000 (вставлен другой SYN с 16 байтами данных, 76 = 60 + 16); запись о применении обходного пути applying linux postnat conntrack workaround; packet id=1 pass modified, длина 60 и после изменения 60: стратегия вернула DROP, а приёмщик отдал ядру изменённый пакет той же длины, ведь копируется весь пакет и меняются два поля. Что записано на проводе: на проводе клиента сначала вставленный SYN, через 3,708 мс исходный пакет уже не как TCP SYN, а как пакет IPv4 с протоколом 255 и временем жизни 1 (кадр 74 байта, 74 = 60 + 14 байт Ethernet); на входе роутера он записан; на втором интерфейсе роутера и на сервере его нет. Причина остановки не установлена: известно только, что у пакета неверная контрольная сумма заголовка IP и сообщение об ошибке ICMP time-exceeded на клиент не пришло; поэтому мы не говорим, что его остановило время жизни, равное единице. Открытое наблюдение: следующие два пакета очереди, подтверждение (номер 2) и запрос (номер 3), программа пропустила без изменений, но ни в одной точке записи они не появились; запрос клиент, похоже, повторил на отметке около 294 мс от начала записи, и ответ сервера в итоге пришёл. Куда делись эти два пакета, мы не выяснили. опыт t40, стратегия syndata: первый пакет соединения 52packet: id=1 len=60SYN: 60 = 20 + 40 65rawsend_dissect repeats=1 size=76 … fwmark=40000000вставка: 76 = 60 + 16 66applying linux postnat conntrack workaroundобходной путь 67packet: id=1 pass modified. len 60 => 60изменён, но та же длина Стратегия вернула DROP, а приёмщик отдал ядру изменённый пакет той же длины:копируется весь пакет и меняются два поля. Слева номера строк журнала опыта, не исходного кода. что записано на проводе: четыре точки по пути пакета провод клиентасначала вставленныйSYN, 76 Б; через3,708 мс исходный:протокол 255, TTL 1,кадр 74 Б вход роутераисходный записан интерфейс 2 роутераисходного нет серверисходного нет 74 = 60 + 14 (14 байт заголовка Ethernet). Времена от начала записи в нашем опыте. Причина остановки на роутере не установлена.Известно два факта: у пакета неверная контрольная сумма заголовка IP, и сообщениеоб ошибке (ICMP time-exceeded) на клиент не пришло. Причину они не показывают. Два пакета без следа: номер 2 (ACK) и номер 3 (запрос) пропущены без изменений,но ни в одной точке записи они не появились.Запрос клиент, похоже, повторил на отметке около 294 мс; ответ сервера в итоге пришёл.Куда делись эти два пакета, мы не выяснили.

Для сравнения t10: выброшен запрос, но не первый. Номера очереди: один, SYN клиента; два, SYN-ACK сервера; три, подтверждение клиента; четыре, запрос. Счётчик клиента: SYN первый, подтверждение второе, запрос третий. Он равен трём, условие «равен единице» не выполнено, приёмщик получает обычный DROP, в журнале пакет номер четыре и слово drop. Номер четыре и счётчик три различаются потому, что номер два в общей очереди занял ответ сервера. Проверь себя: выброшено подтверждение клиента, хитрость сработает? Нет, счётчик два. А SYN клиента? Да, счётчик единица.

В нашем тесте хитрость сработала и в трёх опытах на стороне сервера, там первым идёт SYN-ACK: строка про обходной путь есть в четырёх записанных прогонах, в t40 и этих трёх.

Сравнение: t10, счётчик равен трём Для сравнения опыт t10: выброшен запрос, но не первый пакет. Номера общей очереди: 1, SYN клиента; 2, SYN-ACK сервера; 3, подтверждение клиента; 4, запрос клиента. Счётчик пакетов клиента: SYN первый, подтверждение второе, запрос третий (у пакета сервера счётчик клиента не меняется). Запрос с номером 4 и счётчик 3 различаются, потому что номер 2 в общей очереди занял ответ сервера. Счётчик равен трём, условие «равен единице» не выполнено, приёмщик получает обычный DROP, в журнале пакет id=4 и слово drop. Проверь себя: выброшено подтверждение клиента, сработает ли хитрость? Нет, счётчик 2. А SYN клиента? Да, счётчик 1. В нашем тесте хитрость сработала и в трёх опытах на стороне сервера, там первым идёт SYN-ACK: строка про обходной путь есть в четырёх записанных прогонах, в t40 и этих трёх. Итог: слово drop в журнале бывает разным. В t10 выброшенный запрос заменили двумя вставками. В t40 выброшенный первый пакет заменили негодной копией, по-настоящему он не выбрасывался. Куда делась копия, мы не выяснили, а причина подмены только в комментарии автора. для сравнения опыт t10: выброшен запрос, но не первый пакет номера в общей очереди 1SYN клиента 2SYN-ACK сервера 3ACK клиента 4запрос клиента счётчик пакетов клиента 1 — 2 3 Номер 4 и счётчик 3 различаются, потому что номер 2 в общей очереди занял ответ сервера. что происходит с выброшенным запросом запрос, номер 4стратегия вернула DROP условие «счётчик равен 1»?3 ≠ 1: не выполнено обычный DROPбез подмены копией packet: id=4 dropв журнале номер 4 и слово drop Проверь себя: выброшено ACK клиента,хитрость сработает? Нет, счётчик 2.А SYN клиента? Да, счётчик 1. Серверные опыты: хитрость сработалаи в трёх опытах со стороны сервера, тампервым идёт SYN-ACK (всего 4 прогона). слово drop в журнале бывает разным опыт t10drop заменили двумя вставками:запрос 250 Б ушёл как 104 + 146 опыт t40drop заменили негодной копией:по-настоящему пакет не выбрасывался Куда делась копия, мы не выяснили, а причина подмены только в комментарии автора.

Итак, слово drop в журнале бывает разным. В t10 выброшенный запрос заменили двумя вставками. В t40 выброшенный первый пакет заменили негодной копией, по-настоящему он не выбрасывался. Куда делась копия, мы не выяснили, а причина подмены только в комментарии автора.

Другие платформы: BSD, Windows и что у трёх вариантов общего

Вопрос этой сцены: как выглядят другие системы и что у трёх вариантов программы общего? Сразу оговорка: код вариантов для BSD и Windows мы прочитали, но не запускали, так что это чтение кода, а не наблюдение.

Какой вариант получится, решает сборка. В функции main три ветки, остаётся одна: для Linux главный цикл nfq_main и программа nfqws2, для BSD цикл dvt_main и программа dvtws2, для Windows цикл win_main и программа winws2. Неизвестная система останавливает сборку ошибкой.

Начнём с BSD, то есть FreeBSD и OpenBSD. Игрушечная картинка: вместо ленты почтовая ячейка. Ядро кладёт пакеты в ячейку, программа забирает, и чтобы пакет продолжил путь, его нужно положить обратно. В коде это divert-сокет. Очереди nfqueue там нет: программа открывает divert-сокет и привязывает его к порту из параметра port.

Три варианта программы и путь пакета Какой вариант программы получится, решает сборка: в функции main (nfqws.c:3380-3388) три ветки, остаётся одна. Для Linux главный цикл nfq_main и программа nfqws2 (Makefile:146), для BSD цикл dvt_main и программа dvtws2 (Makefile:158, BSDmakefile:45), для Windows цикл win_main и программа winws2 (Makefile:161, 163). Неизвестная система останавливает сборку ошибкой (#error unsupported OS, 3387). Три колонки показывают путь пакета. Linux: правило ядра, очередь NFQUEUE, программа читает recv, проходная и движок, вердикт ядру. BSD: правило ipfw или pf, divert-сокет, программа читает select и recvfrom, проходная и движок, sendto обратно в сокет. Windows: драйвер WinDivert с фильтром, пачка до 128 пакетов, разрез по длине из заголовка IP, проходная и движок, windivert_send. Игрушка: почтовая ячейка вместо ленты, ядро кладёт пакет в ячейку, программа забирает и кладёт обратно. Код вариантов для BSD и Windows мы прочитали, но не запускали: это чтение кода, а не наблюдение. сборка выбирает вариант: nfqws.c:3380-3388 только чтение кода, не запускали 3380-3381#ifdef __linux__result = nfq_main();nfqws2: Linux 3382-3383#elif defined(BSD)result = dvt_main();dvtws2: BSD 3384-3385#elif defined(__CYGWIN__)result = win_main();winws2: Windows 3386-3387#else#error unsupported OSсборка останавливается путь пакета в каждом варианте Linuxnfqws2 (Makefile:146) правило ядра, nftables очередь NFQUEUE программа читает recv проходная и движок вердикт ядру BSDdvtws2 (Makefile:158) правило ipfw или pf divert-сокет, порт port select и recvfrom проходная и движок sendto обратно в сокет Windowswinws2 (Makefile:161) драйвер WinDivert, фильтр пачка до 128 пакетов разрез по длине из IP проходная и движок windivert_send игрушка для BSD: почтовая ячейка вместо ленты ядроячейка (divert-сокет)программаЯдро кладёт пакет в ячейку, программа забирает; чтобы пакет продолжил путь, его кладут обратно.

Нужные пакеты в этот порт направляют пакетные фильтры ядра. По документации, на FreeBSD для этого берут ipfw, а pf там из-за защиты от петли на практике не годится; на OpenBSD используют только pf, и его защита от петли, по документации, работает.

Цикл ждёт вызовом select и читает вызовом recvfrom. Дальше знакомое: пакет идёт в ту же проходную с нулевой меткой. Направление определяется по адресу источника: по комментарию автора, у входящего он выставлен в адрес интерфейса, у исходящего равен нулю.

Вердикта для ядра здесь нет, поэтому три штампа означают действия с сокетом. PASS это отправить пакет обратно в тот же сокет, в журнале запись о возврате без изменений. MODIFY это отправить новые байты, в журнале запись о возврате с изменением и две длины. DROP это ничего не отправлять и только записать drop. Проверь себя: что на BSD значит выбросить пакет? Ничего не отправлять: пакет в ядро не возвращается. А если отправка обратно не удалась? Ошибка возврата пакета, а при частичной отправке сообщение, что не все данные возвращены.

BSD: цикл divert-сокета и три штампа Цикл dvt_main для BSD по строкам (nfqws.c:762-819), только чтение кода, не запускали. Вместо очереди divert-сокет: recvfrom читает пакет (762), метка нулевая (771), направление определяется по адресу источника (776-777): по комментарию автора, у входящего он выставлен в адрес интерфейса, у исходящего равен нулю. Дальше знакомая проходная processPacketData (798). Вердикта для ядра здесь нет, три штампа означают действия с сокетом. PASS и MODIFY (801-803): отправить обратно в тот же сокет вызовом sendto (810); PASS пишет в журнал reinject unmodified (805), MODIFY пишет reinject modified и две длины (809). Ошибки возврата: reinject sendto (812) и сообщение, что не все данные возвращены (814). DROP (816-817): ничего не отправлять, только запись drop. Затем id++ (819). Таблица по штампам. PASS: на Linux вердикт ACCEPT и запись pass unmodified, на BSD sendto тех же байт и запись reinject unmodified. MODIFY: на Linux ACCEPT с новыми байтами и запись pass modified, на BSD sendto новых байт и запись reinject modified. DROP: на Linux вердикт DROP и запись drop, на BSD ничего не отправляется и запись drop. Проверь себя: что на BSD значит выбросить пакет? Ничего не отправлять, пакет в ядро не возвращается. А если отправка обратно не удалась? Ошибка возврата пакета, а при частичной отправке сообщение, что не все данные возвращены. BSD, dvt_main: nfqws.c:762-819 только чтение кода, не запускали 762rd = recvfrom(fd[i], buf, sizeof(buf), 0, …)читаем пакет из divert-сокета 771uint32_t mark = 0;метка нулевая, как в проходной 777bIncoming = sa_has_addr(…&sa_from);направление по адресу 798verdict = processPacketData(&mark, …, &modlen);знакомая проходная и движок 801-803case VERDICT_PASS: case VERDICT_MODIFY:PASS, MODIFY: обратно в сокет 805DLOG("packet: id=%u reinject unmodified\n", id);запись для PASS 809DLOG("… reinject modified len %zu => %zu\n", …);запись для MODIFY, две длины 810wr = sendto(fd[i], buf, modlen, 0, …);новые байты обратно в сокет 812-814DLOG_PERROR("reinject sendto"); … not all data…ошибка возврата или неполный 816-817default: DLOG("packet: id=%u drop\n", id);DROP: ничего не отправляем 819id++;свой счётчик номеров Строка 776: комментарий автора, у входящего адрес равен адресу интерфейса,у исходящего нулю. Код сокращён многоточием. что значат три штампа на Linux и на BSD Linux: вердикт ядруBSD: действие с сокетом PASSACCEPT ядруpass unmodifiedsendto тех же байтreinject unmodified MODIFYACCEPT с новыми байтамиpass modified. len N => Msendto новых байтreinject modified len N => M DROPвердикт DROPdropничего не отправляемdrop Проверь себя: что на BSD значит выбросить пакет?Ничего не отправлять: в ядро он не возвращается.А если возврат не удался? Ошибка возврата, а при неполнойотправке сообщение, что не все данные возвращены.

Собственные пакеты на BSD, по комментарию автора, тоже идут через divert-сокет: FreeBSD не разрешает сырые заголовки IP для IPv6, а OpenBSD не разрешает отправлять сырые кадры TCP. Метка там другая настройка сокета, SO_USER_COOKIE, по умолчанию пятьсот двенадцать там, где параметр определён. В правилах ipfw из документации есть условие not diverted, но какую роль в защите от петли играет SO_USER_COOKIE, в проверенных нами файлах не сказано, и мы этого не утверждаем.

Теперь Windows. Вместо очереди драйвер WinDivert, программа открывает его с фильтром, строкой условий. Первое условие «не impostor», по-русски «не самозванец». Что этот флаг значит внутри WinDivert, в исходниках проекта не описано, поэтому говорим только так: фильтр такие пакеты исключает.

Цикл получает пакеты пачками, до ста двадцати восьми за раз, и режет пачку по длине из заголовка IP. Если флаг impostor всё же стоит, цикл пропускает пакет сразу, пишет об этом в журнал и проходной не передаёт; это вторая проверка того же флага. У остальных метка ноль, они идут в проходную. Вердикты, как на BSD, это действия: PASS и MODIFY возвращают пакет вызовом windivert_send, DROP ничего не делает.

Собственные пакеты на Windows отправляет совсем другая функция: она описывает пакет структурой для WinDivert, берёт интерфейс из имени «номер точка подномер», а без имени использует интерфейс один, по комментарию автора обычно петлевой, и вызывает windivert_send. Параметр fwmark у неё есть, но в теле не используется; в документации сказано, что для Windows правильный ifout обязателен. Проверь себя: защищает ли от петли на Windows метка? Нет, метки там нет. В коде есть фильтр «не impostor» и повторная проверка флага в цикле, но что именно они предотвращают, в проекте не описано.

Windows: фильтр, пачки и отправка своих пакетов Windows, программа winws2: только чтение кода, не запускали. Вместо очереди драйвер WinDivert, который программа открывает с фильтром, строкой условий; первое условие «!impostor», по-русски «не самозванец» (nfqws.c:1699, 1718). Что этот флаг значит внутри WinDivert, в исходниках проекта не описано, поэтому говорим только так: фильтр такие пакеты исключает. Цикл win_main получает пакеты пачками, до 128 за раз (941), и режет пачку по длине из заголовка IP (968-985); имя интерфейса «номер.подномер» (988). Если флаг impostor всё же стоит (990-994), цикл пропускает пакет сразу, пишет в журнал windivert: passing impostor packet и проходной не передаёт: это вторая проверка того же флага. У остальных метка ноль, они идут в проходную processPacketData (997-999). Вердикты, как на BSD, это действия: PASS и MODIFY возвращают пакет вызовом windivert_send (1005, 1010), DROP ничего не делает. Собственные пакеты на Windows отправляет функция rawsend (darkmagic.c:1743-1776): параметр fwmark у неё есть, но в теле не используется; интерфейс берётся из имени «номер.подномер» (1749-1755), без имени используется интерфейс 1, по комментарию автора обычно петлевой (1757-1762); флаги (1763-1767); вызов windivert_send (1768). В документации сказано, что для Windows правильный ifout обязателен. Проверь себя: защищает ли от петли на Windows метка? Нет, метки там нет. В коде есть фильтр «не impostor» и повторная проверка флага в цикле, но что именно они предотвращают, в проекте не описано. Windows, win_main: nfqws.c:855-1026 только чтение кода, не запускали 1699#define DIVERT_PROLOG "!impostor"первое условие фильтра 1718snprintf(wf, len, "%s", DIVERT_PROLOG);фильтр начинается с него 941wa_count = WINDIVERT_BULK_MAX;пачка: до 128 пакетов 980packet_len = ntohs(((WINDIVERT_IPHDR*)packet)->Length)режем пачку по длине из IP 988snprintf(ifname, …, "%u.%u", …IfIdx, …SubIfIdx);имя «номер.подномер» 990if (wa[n].Impostor)флаг impostor стоит: 992-993DLOG("…passing impostor packet\n"); verdict = VERDICT_PASS;пропуск без проходной 997-999mark = 0; … verdict = processPacketData(&mark, …);остальные: метка 0, проходная 1005windivert_send(packet, packet_len, wa+n)PASS: вернуть пакет 1010windivert_send(mod, modlen, wa+n)MODIFY: вернуть новые байты DROP: ничего не вызывается. Что флаг impostor значит внутри WinDivert, в проекте не описано.Говорим так: фильтр такие пакеты исключает. Код сокращён многоточием. свои пакеты на Windows: darkmagic.c:1743-1776 1743bool rawsend(dst, fwmark, ifout, data, len)fwmark в теле не используется 1749-1762sscanf(ifout,"%u.%u",…); иначе IfIdx=1интерфейс из имени или 1 1763-1767wa.Outbound=1; IPChecksum=1; TCPChecksum=1; …флаги исходящего пакета 1768windivert_send(data,len,&wa)отправка Без имени берётся интерфейс 1, по комментарию автора обычно петлевой. В документации ifout обязателен. Проверь себя: защищает ли от петли на Windows метка? Нет, метки там нет.В коде есть фильтр «не impostor» и повторная проверка флага в цикле,но что именно они предотвращают, в проекте не описано.Мы знаем только то, что написано в коде.

Что общего у трёх вариантов? В середине один код: проходная, движок и вердикты те же. Различается способ вернуть пакет: на Linux вердикт ядру, на BSD и Windows отправка обратно.

И последнее, про предел в двенадцать пакетов. В документации сказано: ipfw, pf и WinDivert не умеют ограничивать по номеру пакета в соединении. Если перехватывать порт целиком, весь поток этого направления идёт в программу, а это нагрузка на процессор. Для Windows документация советует писать собственные фильтры WinDivert, которые хотя бы частично проверяют тип нагрузки, а остальное досеивает winws2. Это слова документации, мы их не проверяли.

Linux, BSD, Windows: что общего и что разное Что общего у трёх вариантов программы и чем они различаются; BSD и Windows известны только по чтению кода, не запускали. Таблица Linux, BSD, Windows. Как пакет попадает в программу: Linux, правило ядра и очередь NFQUEUE; BSD, правило ipfw или pf и divert-сокет; Windows, драйвер WinDivert с фильтром и пачками. Как возвращается: Linux, вердикт ядру; BSD, sendto обратно в тот же сокет; Windows, windivert_send. Защита от петли: Linux, метка и три преграды; BSD, в правилах ipfw из документации есть условие not diverted, роль SO_USER_COOKIE в защите не выяснена, pf на FreeBSD по документации непригоден из-за защиты от петли, на OpenBSD её защита работает; Windows, фильтр !impostor и проверка флага в цикле, роль не выяснена. Ограничитель «первые N пакетов»: на Linux есть, в правиле ядра; на BSD и Windows нет. В середине один общий код: проходная, движок и вердикты те же; различается только способ вернуть пакет, на Linux вердикт ядру, на BSD и Windows отправка обратно. Из документации (readme.md:102-105): ipfw, pf и WinDivert не умеют ограничивать по номеру пакета в соединении; если перехватывать порт целиком, весь поток этого направления идёт в программу, а это нагрузка на процессор; для Windows советуют писать собственные фильтры WinDivert, которые хотя бы частично проверяют тип нагрузки, остальное досеивает winws2. Это слова документации, мы их не проверяли. три варианта рядом только чтение кода, не запускали Linuxnfqws2BSDdvtws2Windowswinws2 как пакет попадаетв программуправило ядра:очередь NFQUEUEipfw или pf:divert-сокетWinDivert: фильтри пачки пакетов как пакетвозвращаетсявердикт ядруsendto обратнов тот же сокетwindivert_send защита от петлиметка и трипреградыв правилах ipfw: «notdiverted»; роль меткиSO_USER_COOKIEне выясненафильтр «!impostor» ипроверка флага в цикле;роль не выяснена ограничитель«первые N пакетов»есть, в правилеядранетнет проходная, движок и вердикты: один и тот же код во всех трёх Различается способ вернуть пакет: на Linux вердикт ядру, на BSD и Windows отправка обратно.По документации: pf на FreeBSD из-за защиты от петли непригоден, на OpenBSD её защита работает. Из документации (readme.md:102-105), мы это не проверяли:ipfw, pf и WinDivert не умеют ограничивать по номеру пакета в соединении;при перехвате порта целиком весь поток направления идёт в программу, а это нагрузкана процессор. Для Windows советуют свои фильтры WinDivert, остальное досеивает winws2.

Итак, на BSD вместо очереди divert-сокет, на Windows драйвер WinDivert с пачками пакетов, и в обоих случаях пропустить значит вернуть пакет обратно. Метки в проходной там нет; на Windows есть фильтр и проверка флага impostor, а их роль в защите от петли мы не выяснили. Всё это мы знаем только по коду.

Итог части и всего выпуска: что мы знаем и что дальше

Соберём всё в одну картину, сначала эту часть. Вставка это пакет, который программа собрала целиком и отправила через сырой сокет, синхронно, поэтому в t10 обе вставки ушли раньше, чем исходный запрос получил вердикт. Метка на вставке, которую отправляют стандартные помощники Lua, это метка исходного пакета, объединённая побитовым ИЛИ с меткой программы, по умолчанию на Linux ноль-икс четыре и семь нулей. От петли защищают три преграды: правило ядра, notrack в цепочке predefrag и проходная в программе. Вставки, судя по t20 и нашему тесту, предел в двенадцать пакетов не расходуют.

Если на Linux стратегия выбрасывает самый первый пакет соединения, программа подменяет его негодной копией; почему, сказано только в комментарии автора, а что стало с соединением в t40, мы не выяснили. На BSD и Windows механизм другой, и о нём мы знаем только по коду.

Проверим на одном примере, вставка номер один из t10. Метка? Ноль-икс четыре и семь нулей. Размер? Сто пятьдесят шесть байт. Попадёт ли в очередь? Нет, хотя какая именно преграда её остановила, по журналам не видно. Потратит ли предел? Судя по t20, нет.

Итог части: шесть пунктов и проверка на вставке Итог части про вставки, шесть пунктов с галочками. 1. Вставка это пакет, который программа собрала целиком и отправила через сырой сокет, синхронно, поэтому в t10 обе вставки ушли раньше, чем исходный запрос получил вердикт. 2. Метка вставки это метка исходного пакета, объединённая побитовым ИЛИ с меткой программы (для стандартных помощников Lua), по умолчанию на Linux 40000000. 3. От петли защищают три преграды: правило ядра, notrack в цепочке predefrag и проходная в программе. 4. Вставки, судя по t20 и нашему тесту на UDP, предел в двенадцать пакетов не расходуют: двенадцать срабатываний с notrack и семь без него. 5. Если на Linux стратегия выбрасывает самый первый пакет соединения, программа подменяет его негодной копией; почему, сказано только в комментарии автора, а что стало с соединением в t40, мы не выяснили. 6. На BSD и Windows механизм другой, и о нём мы знаем только по коду. Проверь себя на вставке номер один из t10. Метка: 40000000. Размер: 156 байт, это 20 + 32 + 104. Попадёт ли в очередь: нет, хотя какая именно преграда её остановила, по журналам не видно. Потратит ли предел: судя по t20, нет. итог части: что мы узнали про вставки ✓вставка: пакет, собранный программой и отправленный через сырой сокет ✓метка вставки: метка исходного пакета ИЛИ бит программы (стандартные помощники Lua) ✓три преграды от петли: правило ядра, notrack, проходная в программе ✓вставки не тратят предел «первые 12» (судя по t20 и нашему тесту на UDP) ✓выброшенный первый пакет на Linux подменяется негодной копией ~BSD и Windows: только по коду, не запускали проверь себя: вставка номер один из t10 метка?40000000на ней бит программы размер?156 байт20 + 32 + 104 в очередь попадёт?неткакая, не видно потратит предел?нетсудя по t20

Теперь итог всего выпуска. Правило ядра отправляет первые двенадцать пакетов каждого направления в очередь двести, настройщик очереди её создаёт, главный цикл в одном потоке читает пакеты по одному, приёмщик превращает вердикт в ответ ядру. Запрос из двухсот пятидесяти байт в t10 получил DROP, а вместо него ушли две вставки, в сто четыре и сто сорок шесть байт, с меткой, которая не даёт им вернуться в очередь. Очередь увидела одиннадцать пакетов, провод клиента двенадцать: одиннадцать, минус выброшенный запрос, плюс две вставки.

Весь выпуск: от правила ядра до вставок на проводе Итог всего выпуска одной цепочкой, блоки зажигаются по одному. Правило ядра отправляет первые двенадцать пакетов каждого направления в очередь 200 (с проверкой метки), настройщик очереди nfq_init (nfqws.c:355) её создаёт, главный цикл в одном потоке читает пакеты по одному (recv, nfqws.c:566, и nfq_handle_packet, 578), приёмщик nfq_cb (285-339) превращает вердикт в ответ ядру, проходная processPacketData (154-164) с проверкой метки в программе, движок и стратегия, три штампа PASS, MODIFY, DROP, вставки с меткой 40000000 через отправщик сырых пакетов rawsend (darkmagic.c:1984-2042), провод клиента. Хитрость с первым пакетом: desync.c:640-673. Числа выпуска: 12 и 12, первые двенадцать пакетов в каждую сторону; запрос из 250 байт в t10 получил DROP, а вместо него ушли две вставки, 104 и 146 байт, 250 = 104 + 146; очередь увидела 11 пакетов, провод клиента 12: одиннадцать, минус выброшенный запрос, плюс две вставки, 11 − 1 + 2 = 12. С меткой, которая не даёт вставкам вернуться в очередь. весь выпуск одной цепочкой: от правила ядра до провода правило ядра12 в каждуюсторону, метка очередь 200 настройщикочереди главный цикл(один поток) приёмщикnfq_cb проходнаяпроверка меткив программе движок истратегия три штампаPASS, MODIFY,DROP вставкиметка 40000000,сырой сокет провод клиента В коде: nfq_init nfqws.c:355, цикл :566 и :578, приёмщик :285-339, проходная :154-164,отправщик darkmagic.c:1984-2042, хитрость с первым пакетом desync.c:640-673. 12 / 12первые пакетыв каждую сторону 250 → 104 + 146запрос в t10: DROP,вместо него 2 вставки 11пакетов увиделаочередь 12пакетов на проводеклиента Одиннадцать в очереди − выброшенный запрос + две вставки = 12 на проводе: 11 − 1 + 2 = 12.

Что дальше. Мы говорили о пакете как о наборе байтов, но брали готовые слова: «заголовок IP», «порядковый номер», «контрольная сумма». Что движок делает с этими байтами, мы ещё не видели. В следующем выпуске: как из байтов получается таблица, с которой работают скрипты на Lua, и как собрать её обратно в байты, пересчитав длины и контрольные суммы.

Дальше: из байтов в таблицу Что дальше: следующий выпуск называется «Из байтов в таблицу». Три пиктограммы. Первая: заголовки как лента с указателем, лента из заголовка IP, заголовка TCP и данных (в масштабе вставки номер один из t10: 20, 32 и 104 байта). Вторая: таблица с полями, например заголовок IP, порядковый номер, контрольная сумма. Третья: контрольная сумма, число, посчитанное по байтам. Мы говорили о пакете как о наборе байтов, но брали готовые слова: заголовок IP, порядковый номер, контрольная сумма. Что движок делает с этими байтами, мы ещё не видели. В следующем выпуске: как из байтов получается таблица, с которой работают скрипты на Lua, и как собрать её обратно в байты, пересчитав длины и контрольные суммы. следующий выпуск Из байтов в таблицу IPTCPданныезаголовки как лентамасштаб: 20, 32 и 104 байта указатель таблица с полямизаголовок IP…порядковый номер…контрольная сумма…с ней работают скрипты Lua контрольная суммабайтысуммачислопересчитывается при сборке Мы говорили о пакете как о наборе байтов, но брали готовые слова: «заголовок IP»,«порядковый номер», «контрольная сумма». Что движок делает с байтами, мы ещё не видели. Дальше: как из байтов получается таблица для скриптов на Lua и как собрать её обратно,пересчитав длины и контрольные суммы.

Итак, пакет прошёл путь от правила ядра до вставок на проводе. Дальше выпуск четыре: из байтов в таблицу.

См. также


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

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