---
title: "Видео 3. Читаем командную строку zapret2"
date: 2026-10-25
tags:
  - zapret2
  - видео
  - сценарий
  - nfqws2
  - profile
  - lua-desync
  - filter
aliases:
  - "Видео 3"
description: "Видео 3 серии о Zapret 2: профили, виды опций, строка стратегии, аргументы, условия и диапазоны: способ чтения настоящей команды, с анимированными схемами."
---

> [!info] О чём заметка
> Это полный текст видео номер 3 из серии о том, как работает [[Zapret2/Zapret2|Zapret 2]], разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.

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


## Настоящая команда и вопрос выпуска

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

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

Первое напоминание. В выпуске про письмо в сортировочном центре мы сравнили пакет с письмом, а оборудование на пути с сортировщиком, который читает адреса.

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

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

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

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

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

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

![[wiki-anim-video03-s01-command-split.svg|Команда лаборатории: заготовка и конец]]

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

![[wiki-anim-video03-s01-preview-result.svg|Итог опыта: одиннадцать пакетов]]

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

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

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


## Куски строки и профиль номер один

Возьмём команду и разрежем её на куски. Пока по виду, не вникая в смысл.

Команда состоит из опций. Опция это слово с двумя дефисами впереди, иногда со знаком «равно» и значением: «номер очереди равно двести», «фильтр tcp равно четыреста сорок три». Опции разделены пробелами. Слово в самом начале, nfqws2, это название программы, а не опция.

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

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

Второй кусок: «фильтр tcp равно четыреста сорок три». Это условие профиля, то, что написано над чертой. Программа будет заниматься только пакетами TCP на порт четыреста сорок три.

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

Четвёртый кусок: настройки вызова, то есть слова, которые сами ничего не вызывают, а настраивают вызовы справа от себя. Первые два называются диапазонами. Диапазон отвечает на вопрос: на каких пакетах звать этот вызов. В строке диапазон записан латинской буквой a или x, по-русски «эй» или «икс». «Эй» значит «на всех пакетах», «икс» значит «ни на одном». У нас оба диапазона про входящие пакеты, то есть те, что идут от сервера к клиенту.

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

Раскрасим строку. Серое: служебное. Синее: условие профиля. Золотое: настройка вызова. Зелёное: сам вызов. И заметим, чего в строке нет: слов «новый профиль».

![[wiki-anim-video03-s02-line-colored.svg|Строка команды по кускам]]

Проверь себя. Какая из опций в нашей строке условие профиля, какая вызов, а какая ни то ни другое? Возьмите фильтр tcp, lua-desync и диапазон входящих.

Фильтр tcp условие профиля. Lua-desync вызов. А диапазон входящих не условие профиля и не вызов: это настройка, которую программа вкладывает в вызовы правее себя.

Теперь слово «профиль». Страница тетради целиком, условия плюс действия, и есть профиль. Но откуда он берётся, если слов «новый профиль» в строке нет?

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

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

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

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

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

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

![[wiki-anim-video03-s02-profile-one.svg|Профиль один существует с самого начала]]

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

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

И ещё деталь. Если профильных опций нет вообще, профиль один всё равно существует: он пустой, и по логике кода ему подходит любой пакет.

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


## Три вида опций: общие, профильные и «ручка»

Относятся ли все опции строки к профилю? Нет. Они бывают трёх видов, и третий самый коварный.

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

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

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

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

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

Условия профиля, например порт, по коду можно написать в любом месте профиля, хоть до вызовов, хоть после: они записываются прямо в профиль. Порядок важен только для ручки.

![[wiki-anim-video03-s03-three-kinds.svg|Три вида опций командной строки]]

Что значит «вид данных». Данные в пакете программа умеет распознавать. У SYN, первого пакета соединения, данных нет, и вид такой: «пусто». Если в пакете клиентское приветствие, вид tls_client_hello. Если данные есть, но ни на что известное не похожи, вид «неизвестно». Как программа это определяет, рассказывает выпуск «Тип нагрузки и имя сайта»: там вид данных называется типом нагрузки. Нам пока достаточно того, что у каждого пакета такой вид есть.

Начальные значения такие. Фильтр на вид данных: любые. Диапазон входящих «икс», то есть «никогда»: по умолчанию вызов не получает пакеты сервера. Диапазон исходящих «эй», то есть «всегда». Остальные буквы диапазонов разберём позже.

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

Первая строка: pcap. Входящие «эй», исходящие «эй», данные любые: фильтр на вид данных в команде стоит позже этого вызова. Вторая: multisplit. Входящие уже «икс», потому что перед этим вызовом скрипт написал «икс». Исходящие «эй». Данные: клиентское приветствие. Третья: pass, те же значения, что у multisplit.

Одно уточнение про запись на экране. В журнале «эй» напечатано как «a ноль», а «икс» как «x ноль»: у этих двух значений числа нет, и программа печатает вместо него ноль. Диапазон там записан как «от» и «до», поэтому мы видим «a ноль, тире, a ноль».

![[wiki-anim-video03-s03-pen-stamp.svg|Ручка копируется в каждый вызов]]

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

Проверь себя. Диапазон исходящих написан между двумя вызовами pass. Получат ли его оба вызова?

Нет, только второй. Первый к этому моменту уже разобран и получил прежние значения.

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


## Разделитель new: чистая страница, а не копия

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

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

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

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

![[wiki-anim-video03-s04-new-steps.svg|Разделитель --new шаг за шагом]]

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

Проверь себя. В первом профиле «фильтр tcp равно четыреста сорок три» и вызов pass. Потом разделитель и вызов drop, это функция, которая выбрасывает пакет. Какой фильтр порта у второго профиля?

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

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

Команда в трёх частях. Первая: фильтр на клиентское приветствие, диапазон входящих «эй», диапазон исходящих «икс» и вызов pass. Вторая: разделитель и ещё один pass. Третья: разделитель, диапазон входящих «эй» и два вызова pass подряд.

Программа сообщила: три профиля, заданных пользователем. Два разделителя, три профиля. Профиль один: входящие «эй», исходящие «икс», данные клиентское приветствие, всё как мы написали. Профиль два: входящие «икс», исходящие «эй», данные любые. Это начальные значения: во втором профиле мы их не писали, и из первого ничего не перетекло.

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

![[wiki-anim-video03-s04-dry-three-profiles.svg|Три профиля в режиме разбора]]

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


## Профиль no_action, пропуск, имя и шаблон

Что будет с пакетом, которому не нашлось подходящего профиля? И три мелкие опции вокруг разделителя: пропуск, имя и шаблон. Начнём с первого.

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

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

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

Посмотрим на опыте. Протокол соединения мы подробно разберём в сцене про содержимое; пока достаточно, что у защищённого соединения он называется tls. В нашем тесте профиль один имел условие «протокол http», а соединение было защищённое, и ни один пакет профилю один не подошёл. Профиль искали на первом пакете и ещё раз на приветствии, и оба раза журнал писал: «подходит профиль ноль, no_action». На остальных восьми пакетах программа брала выбор из памяти о соединении. А на каждом из десяти пакетов журнал писал: «в этом профиле нет функций Lua». Запрос дошёл до сервера целым куском в двести пятьдесят байт. Пакетов в этом опыте десять, а не одиннадцать, как в опыте с разрезом: когда запрос идёт целым куском, сервер подтверждает его одним пакетом, а не двумя. К этому мы вернёмся в конце выпуска.

Значит, трафик, для которого профиль не нашёлся, программа не трогает: он достаётся пустому профилю и проходит как есть. По коду, пакет не блокируется тем, что ему не нашлось места.

![[wiki-anim-video03-s05-no-action-last.svg|Пустой профиль no action в конце списка]]

Теперь опция skip. По названию кажется, что она отключает следующий профиль. На самом деле она отключает тот, где стоит. Сама опция только ставит флаг, а на следующем разделителе код стирает текущую запись и использует её заново, с тем же номером. Дыр в нумерации не остаётся.

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

Проверь себя. После профиля один идёт разделитель с именем gone, внутри него опция skip, потом разделитель с именем kept. Какой номер получит профиль kept?

Номер два. Профиль gone получил было номер два, но на следующем разделителе его запись стёрта, счётчик не вырос, и kept занял тот же номер. Так выходит по коду.

Имя профиля задают значением у разделителя, new, знак равно, имя, или отдельной опцией name. Оно видно в журнале вместо «noname» и доходит до скриптов на Lua, но на выбор профиля не влияет.

Есть ловушка: имя через пробел после разделителя игнорируется. В нашем режиме разбора это подтвердилось.

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

Опция import копирует образец в текущий профиль: вызовы вместе с их диапазонами, фильтры порта, списки. Импортировать можно только образец, который уже закрыт разделителем. Сам импорт текущие настройки вызова не меняет.

В нашем режиме разбора профиль real с импортом шаблона получил номер один: шаблон в число профилей не вошёл.

![[wiki-anim-video03-s05-skip-name-template.svg|Пропуск, имя и шаблон профиля]]

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


## Стратегия это функция, найденная по имени

Мы разобрали, из чего состоит строка: профили и три вида опций. Теперь главное слово выпуска. Что такое «стратегия» с точки зрения кода? Ответ проще, чем можно ожидать.

Сначала договоримся о словах, потому что «стратегия» в нашей серии значит две близкие вещи. Функция-стратегия это одна функция на языке Lua, например pass: она определена в одном из файлов с кодом на Lua. Вызов это одна опция lua-desync, которая называет такую функцию. А стратегия в широком смысле, как в выпуске про письмо в сортировочном центре, это все вызовы профиля по порядку: целый сценарий. В этой сцене речь про функцию-стратегию, а про сценарий мы заговорим дальше.

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

Теперь настоящее. Возьмём самую простую стратегию из библиотеки. Это «пустышка»: в библиотеке она называется pass. В Lua функцию объявляют словом function, дальше имя и два параметра в скобках. Внутри одна строка: записать в журнал слово pass. Ответа нет вообще. Два параметра по соглашению называются ctx и desync: через них программа передаёт функции сведения о пакете и служебные данные. Что именно в них лежит, расскажет выпуск «Lua внутри программы», а нам сейчас важно одно: у стратегии ровно два параметра.

В нашем тесте у такой функции в журнале одна строка со словами «LUA» и «pass».

Где хранится стратегия? В Lua имя функции это обычная глобальная переменная, ящик с подписью, в котором лежит функция. Определить функцию значит положить её в ящик с таким именем. Списка стратегий нигде нет: годится любая глобальная функция. Опция lua-desync просто называет ящик.

Откуда функции берутся? Из файлов, подключённых опцией lua-init. Программа выполняет каждый файл один раз при запуске, и функции остаются в памяти. По коду, правка Lua-файла вступает в силу только после перезапуска.

Наши две функции лежат в разных файлах. Pcap, самописец, в своём маленьком файле. Multisplit, разрезальщик, в файле готовых стратегий: там их двадцать пять, среди них drop, который выбрасывает пакет, fake, который добавляет поддельные пакеты, и сам multisplit. Остальные назовём позже. А pass лежит в библиотеке помощников.

![[wiki-anim-video03-s06-pass-and-drawers.svg|Стратегия это функция с именем]]

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

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

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

![[wiki-anim-video03-s06-checks-and-override.svg|Две проверки имени и замена функции]]

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

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

Итак, функция-стратегия это глобальная Lua-функция с двумя параметрами. Программа проверяет её имя при запуске и перед каждым вызовом, а позднее определение заменяет раннее.


## Строка вызова, порядок и имена вызовов

Как опция lua-desync превращается в имя функции и список аргументов? И в каком порядке идут вызовы, если их несколько? Разберём по шагам.

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

Настоящая опция устроена так же. Lua-desync, равно, имя функции, например multisplit, двоеточие, имя аргумента pos, знак «равно» и значение midsld. Midsld это метка места разреза, о ней расскажет выпуск «Где резать: метки». А вызов самописца выглядит так: pcap, двоеточие, file, равно, имя файла, куда писать.

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

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

Каждый кусок делится по первому знаку «равно». Левая часть это имя аргумента, оно тоже должно быть идентификатором. Правая это значение. Знаков «равно» в значении может быть сколько угодно, делится только по первому. Если знака нет, значения нет вовсе.

Пройдём по нашей строке. Имя multisplit подходит. Остаток «pos равно midsld» один кусок: слева pos, справа midsld. Во втором вызове имя pcap и один аргумент file со значением nfqws_seen.pcap.

Ошибка формы останавливает запуск ещё до первого пакета. В нашем режиме разбора имя функции с дефисом, аргумент без имени и аргумент, имя которого начинается с цифры, дали сообщение «неверный вызов Lua-функции» и код возврата один. А значение с защищённым двоеточием осталось одним значением, и аргумент без значения принимается.

![[wiki-anim-video03-s07-parse-call.svg|Разбор опции lua-desync]]

Ещё строка для тренировки: pass, двоеточие, «эй», равно, один, двоеточие, «би». Первый аргумент: имя «эй», значение «один». Второй: имя «би», значения нет, это голый флаг.

Проверь себя. В опции написано pass, двоеточие, «кей», равно, «икс», равно, «игрек». Что станет значением аргумента «кей»?

Строка «икс равно игрек» целиком, вместе со вторым знаком «равно». Делится только по первому.

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

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

Теперь имя экземпляра: имя функции, номер профиля и позиция вызова в профиле, считая с единицы. На экране части соединены подчёркиваниями, а мы читаем их по порядку.

Возьмём опыт, где после multisplit добавлена пустышка pass. В нашем тесте в профиле один три вызова. Первый, pcap: профиль один, позиция один. Второй, multisplit: профиль один, позиция два. Третий, pass: профиль один, позиция три. В опыте, который мы разберём в конце выпуска, pass нет, там два экземпляра.

На пакетах, где проверяются оба вызова, журнал сначала пишет про pcap, потом про multisplit: так же, как они стоят в команде. Почему на некоторых пакетах строк про multisplit нет вовсе, увидим на пакете номер пять.

В другом профиле нумерация начнётся заново. А позиция считается у каждого вызова профиля, даже у пропущенного на этом пакете: так имя не меняется от пакета к пакету.

Проверь себя. В профиле номер три стоят три вызова: pass, drop, multisplit. На этом пакете drop пропущен. Как называется multisplit?

Multisplit, профиль три, позиция три. Позиция считается и у пропущенных вызовов. Так следует из кода, опыт с профилем три мы не запускали.

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

![[wiki-anim-video03-s07-order-and-names.svg|Порядок вызовов и имена экземпляров]]

Итак, имя функции это всё до первого двоеточия, дальше аргументы, у каждого имя и, возможно, значение после первого «равно». Экземпляры идут в порядке командной строки, и имя экземпляра это функция, профиль и позиция.


## Аргументы: текст, блобы и повторная запись

Строка опции разрезана на пары «имя, значение». Что получит функция? И что будет, если одно имя записано дважды?

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

В нашем тесте тестовая функция печатала в журнал всё, что получила. Аргумент «эй равно один» пришёл строкой из одного символа, цифрой «один», а не числом. Если стратегии нужно число, она переводит текст сама.

Аргумент без знака «равно», просто «би», и аргумент со знаком «равно» и пустым местом, «си»: в обоих случаях функция получила пустую строку.

По общим сведениям, в Lua «нет» означают только два значения, nil, «ничего», и false. Пустая строка «нет» не означает. Поэтому голое имя работает как включённый флаг: стратегия проверяет, есть ли аргумент. У самописца так устроен флаг keep: если он есть, файл не затирается, а дополняется.

Теперь блоб: заранее заготовленный набор байтов с именем. В нашем тесте блоб создала сама команда, общей опцией blob: имя myblob и четыре байта, эй-эй, би-би, си-си, ди-ди.

Если значение начинается со знака процента, программа подставит вместо него сам блоб: запись «процент myblob» дала именно эти четыре байта.

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

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

![[wiki-anim-video03-s08-args-as-text.svg|Что получает функция: аргументы текстом]]

Проверь себя. Запись «икс равно решётка fake_default_tls». Это готовый блоб, его размер шестьсот восемьдесят байт. Что получит функция?

Строку из трёх цифр: шесть, восемь, ноль. Это текст, а не число и не сам блоб. Так следует из кода, с этим блобом мы не запускали.

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

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

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

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

![[wiki-anim-video03-s08-first-wins.svg|Повторный аргумент: побеждает первый]]

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

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

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


## Условия по транспорту: фильтры tcp и udp

Со стратегией разобрались. Теперь вопрос, какие пакеты вообще достаются профилю. Начнём с самого частого условия, порта.

Фильтр порта сравнивается не с портом отправителя и не с портом получателя, а с портом сервера. В обычном режиме клиента сервер это удалённый конец соединения, и сравнение идёт в обе стороны: и для исходящих пакетов, и для входящих. Поэтому «фильтр tcp равно четыреста сорок три» работает и для запроса, и для ответа. В режиме сервера, опция server, направления меняются местами, а порт для сравнения остаётся портом сервера; этот режим мы в выпуске не разбираем. Журнал перед каждым поиском профиля печатает эту цель: протокол TCP, адрес сервера, порт четыреста сорок три.

Условий по транспорту четыре: фильтры tcp, udp, icmp и фильтр по номеру протокола IP. Мы возьмём порты.

Два слова поясним. ICMP, по общим сведениям, это протокол служебных сообщений сети; по нему, например, ходит ping. А режим сервера, опция server, по коду нужен для входящих соединений, то есть когда программа стоит не на клиенте, а на самом сервере.

Как записывается порт? Просто «четыреста сорок три» подходит порту четыреста сорок три и только ему. «От восьмидесяти до девяноста через тире» подходит любому порту от восьмидесяти до девяноста включительно, например восьмидесяти, восьмидесяти пяти и девяноста. Так читается код функции, которая сравнивает порт с записью.

Тильда это отрицание: «тильда четыреста сорок три» подходит всем портам, кроме этого. Звёздочка значит все порты. Записи с перевёрнутыми границами, с числом больше шестидесяти пяти тысяч пятисот тридцати пяти или с буквами программа отвергает, и запуск останавливается.

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

![[wiki-anim-video03-s09-port-filter.svg|Фильтр порта: порт сервера]]

В нашем тесте к лабораторному «tcp равно четыреста сорок три» добавили «tcp равно тильда четыреста сорок три». Профиль сработал на всех пакетах, и запрос был разрезан на сто четыре и сто сорок шесть байт. Это согласуется с правилом, но «или» не доказывает: лабораторная запись подходит приветствию и одна.

Проверь себя. Запись «тильда восемьдесят». Подойдёт ли порт восемьдесят? А порт четыреста сорок три?

Восемьдесят нет: сама запись называет этот порт, а отрицание переворачивает ответ. Четыреста сорок три да: в записи он не назван, и отрицание превращает «нет» в «да».

Теперь правило, на котором спотыкаются. Если в профиле написан хотя бы один из четырёх фильтров транспорта, остальные три не принимают никого. Написали «tcp равно четыреста сорок три»: профиль возьмёт TCP на этот порт, а UDP, ICMP и остальные протоколы IP не примет. Не написали ни одного: профиль принимает всё. Так сказано и в справке программы.

Внутри программы «отключено» устроено хитро: пустой список заменяется записью «ноль», которую порт не проходит никогда. В нашем тесте профиль только с фильтром tcp на UDP-пакеты ни разу не сработал, а профиль без фильтров сработал и на UDP.

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

![[wiki-anim-video03-s09-transport-defaults.svg|Фильтры транспорта по умолчанию]]

Проверь себя. Профиль, где написаны оба фильтра, tcp и udp, оба на порт четыреста сорок три. Примет ли он пакет ICMP?

Нет. Хотя бы один из четырёх фильтров указан, поэтому ICMP, который мы не писали, не принимается. Профиль примет TCP и UDP на порт четыреста сорок три. Это следует из кода.

Итак, порт сравнивается с портом сервера в обе стороны. Записи порта работают как «или», тильда переворачивает ответ. Хотя бы один фильтр транспорта отключает остальные, а если ни одного, профиль принимает всё.


## Условия по содержимому: фильтр протокола и фильтр вида данных

Порт говорит, куда идёт пакет. А можно ли отбирать пакеты по тому, что внутри?

Для этого есть две разные опции, и путать их не стоит. Первая, фильтр протокола, в коде filter-l7, читается «фильтр эл семь»: это условие профиля, оно про всё соединение. Вторая, фильтр вида данных, payload, мы уже видели: это настройка вызова, она про один пакет.

Что значит «протокол». Программа смотрит на данные в пакете и пытается опознать, что это: TLS, HTTP, QUIC и другие. Пока данных нет, протокол «неизвестен». Как она это определяет, рассказывает выпуск «Тип нагрузки и имя сайта», нам хватит результата.

Два слова поясним. QUIC, по общим сведениям, это протокол, который работает поверх UDP, и браузеры часто открывают по нему сайты; подробно о нём расскажет выпуск «Другие слои». А «эл семь» в названии фильтра, по общим сведениям, значит седьмой, прикладной уровень: то, что лежит поверх TCP и UDP, то есть сам протокол вроде TLS или HTTP. Нумеровать остальные уровни мы не будем.

Игрушка, выдуманная. У соединения есть папка, а у каждого письма в ней свой штамп. На обложке папки пишут, о чём переписка, но пока первое письмо не вскрыто, там стоит «неизвестно». Дверь «только про TLS» смотрит на обложку папки. А штамп письма, «пусто», «приветствие» или «неизвестно», смотрит уже вызов.

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

Значения фильтра протокола: тринадцать настоящих протоколов, среди них tls, http и quic, и три служебных слова. All значит «любой». Unknown значит «пока неизвестно». Known значит «любой известный». Несколько значений можно перечислить через запятую.

У фильтра вида данных свои имена для тех же вещей: протоколу tls соответствует вид tls_client_hello, протоколу quic вид quic_initial, протоколу http вид http_req. Два словаря, и путать их не нужно.

![[wiki-anim-video03-s10-two-filters.svg|Два фильтра: протокол и вид данных]]

Отсюда следствие для первого пакета, SYN. Данных нет, протокол неизвестен, и профиль с условием «протокол tls» к нему подойти не может. В нашем тесте профиль с условием «протокол http» на защищённом соединении не подошёл ни на SYN, ни на приветствии: запрос прошёл нетронутым, двести пятьдесят байт одним куском. Это тот же опыт, что в сцене про профиль no_action.

А профиль с условием unknown подходит как раз на SYN, и перестаёт подходить, когда выяснится, что протокол tls.

Фильтр вида данных устроен иначе. Он принадлежит вызову и смотрит на вид данных у конкретного пакета, а не у соединения. «Пусто» у пакета без данных, например у SYN или у подтверждения, ACK. «Неизвестно» у пакета с данными, которые не опознаны. Дальше «клиентское приветствие» и другие. Значение «любые» подходит всему, tls_client_hello только приветствию, а «известный» любому опознанному виду.

В нашем тесте вызов multisplit стоял после фильтра на клиентское приветствие. На SYN и на пустых подтверждениях программа его не зовёт: журнал пишет «вид пусто не удовлетворяет фильтру» и идёт дальше. Функция работала только на пакете с приветствием.

![[wiki-anim-video03-s10-cover-stamp.svg|Обложка соединения и штамп пакета]]

Проверь себя. Профиль с условием «протокол tls» и в нём вызов с любыми данными. На какой пакет вызов будет позван первым: на SYN или на приветствие?

На приветствие. На SYN протокол неизвестен, профиль целиком не подходит, и его вызовы не звали. Так по коду. Тем же способом known, «любой известный протокол», не подойдёт на SYN и подойдёт на приветствии.

Итак, у условий по содержимому два слоя. Фильтр протокола принадлежит профилю и ждёт, пока протокол опознан, поэтому к SYN не подходит. Фильтр вида данных принадлежит вызову и смотрит на конкретный пакет.


## Списки имён и адресов

Остались условия про сайт и про адрес: список имён и список адресов. Покажем их одной картинкой.

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

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

Как имя сайта сравнивается со списком. Берём имя из запроса, www.example.com. Сначала ищем его целиком, потом без самой левой части, example.com, потом com. Запись подходит, если равна любому из этих хвостов. Значит, запись example.com закрывает и www.example.com, и любые другие поддомены.

Запись со знаком крышки в начале, крышка example.com, подходит только целому имени. А шаблонов нет: запись «звёздочка, точка, example.com» ни с каким обычным именем не совпадёт.

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

С записью example.org все три пробы, www.example.com, example.com и com, дали «нет». Профиль не подошёл, запрос прошёл целым куском в двести пятьдесят байт, а пакеты получил пустой профиль. Чужое имя не приводит к выбрасыванию пакетов: пакет достаётся профилю без вызовов. Это про обычный список, автоматические мы не разбираем.

Если списков несколько, исключающий сильнее включающего: имя нашлось в исключениях, ответ «нет». Два включающих списка работают как «или».

Файл без записей означает «списка нет», а не «никого не пускать». По коду, изменённый файл списка перечитывается без перезапуска.

![[wiki-anim-video03-s11-hostlist-walk.svg|Список имён: как сравнивается имя]]

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

Теперь важное следствие для рукопожатия. Адрес известен уже на SYN, поэтому профиль со списком адресов решает сразу: в нашем тесте профиль с подсетью, куда сервер входит, подошёл на SYN. А имя сайта в SYN взять неоткуда, оно появляется только в приветствии. Поэтому профиль со списком имён к SYN не подходит, и рукопожатие ведёт пустой профиль. Это верно и для исключающего списка: ему тоже нужно имя.

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

В нашем тесте это видно по файлу самописца. У профиля со списком имён первым записанным пакетом оказалось приветствие, а трёх пакетов рукопожатия, SYN, SYN-ACK (ответа сервера на SYN) и первого подтверждения, в файле нет. У профиля со списком адресов первый записанный пакет это SYN.

![[wiki-anim-video03-s11-from-which-packet.svg|С какого пакета профиль видит соединение]]

Проверь себя. В списке запись «крышка example.com», а клиент идёт на www.example.com. Подойдёт ли профиль?

Нет. Запись со знаком крышки подходит только целому имени, а целое имя здесь www.example.com. В нашем тесте журнал так и написал: точная запись не совпала.

Итак, список имён сравнивается по меткам слева направо, исключающий сильнее включающего, пустой список означает «списка нет». Список адресов проверяет сервер и работает уже на SYN. Профиль со списком имён получает пакеты с приветствия.


## Первое совпадение выигрывает: порядок профилей это приоритет

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

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

Так работает и программа. Список профилей просматривается от первого к последнему, и выбирается первый, у которого прошли все условия. Остальные, даже подходящие, до дела не доходят.

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

Опыт. В нашем тесте два профиля, и оба подходят нашему запросу. Заготовка лаборатории, то есть порт четыреста сорок три и самописец, попала в профиль один, а профиль два начался пустым. В первом разрез на позиции два, то есть после двух первых байтов, во втором на метке midsld. Сервер получил запрос кусками в два байта и двести сорок восемь. Разрез сделал первый профиль, а второй не понадобился. Если поменять профили местами, по коду сработал бы разрез по midsld и куски были бы сто четыре и сто сорок шесть; такого опыта у нас нет.

![[wiki-anim-video03-s12-first-match.svg|Первое совпадение выигрывает]]

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

Опыт с таким поворотом. Профиль один: список имён, в котором нашего имени нет, и разрез на позиции два. Профиль два: условие «протокол tls» и разрез на метке midsld. На SYN не подходит ни тот, ни другой, программа берёт пустой no_action. То же на SYN-ACK и на подтверждении: выбор берётся из памяти.

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

![[wiki-anim-video03-s12-profile-switch.svg|Выбор профиля и его повтор]]

Проверь себя. Тот же опыт, но в списке профиля один стоит example.com. Клиент идёт на www.example.com. Какой профиль победит на приветствии?

Профиль один: список находит example.com, и по правилу первого совпадения он выигрывает у профиля два. Разрез будет на позиции два. Это вывод из кода, такого запуска у нас нет.

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


## Диапазоны: когда звать вызов

Профиль выбран. Значит ли это, что каждый его вызов сработает на каждом пакете? Нет: у каждого вызова есть свои настройки, и главные из них диапазоны.

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

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

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

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

Теперь как записать диапазон. Граница это буква и число, например «эн три». Две границы соединяет разделитель: тире включает верхнюю границу, знак «меньше» её исключает. «Эн три, тире, эн пять» это пакеты три, четыре и пять. «Эн три, знак меньше, эн пять» это три и четыре.

Две буквы обходятся без числа: «эй» значит «всегда», «икс» значит «никогда». Начальные значения вы знаете: входящие «икс», исходящие «эй».

Теперь ловушка. Одна граница без разделителя, просто «эн три», это ошибка, и программа остановится. Ровно третий пакет записывается как «эн три, тире, эн три». А «начиная с третьего» это «эн три» и тире после него.

![[wiki-anim-video03-s13-counters-grammar.svg|Диапазон: счётчики и граница]]

Проверь себя. Запись «знак меньше, эн четыре». Какие клиентские пакеты в неё попадут?

Первый, второй и третий. Нижней границы нет, а четвёртый пакет исключён знаком «меньше».

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

Настоящий опыт нашей лаборатории. Диапазон исходящих «ди один, тире, ди один», фильтр на клиентское приветствие, дальше вызов multisplit. Идём по исходящим пакетам клиента.

SYN: данных нет, ди ноль, вне диапазона. Подтверждение рукопожатия: тоже ди ноль, вне диапазона. Лампа не горит, вызов не зовут.

Приветствие: ди один, в диапазоне. Вид данных подходит. Вызов зовут, и запрос разрезан на сто четыре и сто сорок шесть байт.

А дальше интересное. Чистое подтверждение, потом пакет FIN, которым сторона сообщает, что закончила передачу, потом ещё подтверждение. У каждого ди по-прежнему один, потому что пакеты без данных счётчик не увеличивают. Диапазон всё ещё верен. Но вызов не зовут: у этих пакетов вид данных «пусто», а фильтр на клиентское приветствие их не пропускает. Журнал так и пишет: «в диапазоне», а следом «вид пусто не удовлетворяет фильтру».

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

![[wiki-anim-video03-s13-d1-lamp-run.svg|Диапазон d1-d1 и лампа]]

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

Проверь себя. Уберём из опыта фильтр вида данных, и он станет «любые». Позовут ли вызов на чистом подтверждении после приветствия?

По коду да: диапазон верен, а вид данных подходит любой. Так мы этот опыт не запускали.

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


## Шесть шагов чтения строки

Теперь соберём всё в способ чтения. Любую строку zapret2 можно прочитать за шесть шагов.

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

Шаг второй: найти разделители профилей. Слов «новый профиль» нет, значит, профиль один. Есть, значит, профилей на один больше, чем разделителей, за вычетом профилей с опцией skip и образцов с опцией template. А в самом конце программа сама добавит пустой no_action.

Шаг третий: в каждом профиле выписать условия. Фильтры транспорта, помня, что один из них отключает остальные. Фильтр протокола. Списки имён и адресов. И помнить, что порядок профилей это приоритет.

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

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

Шаг шестой, проверка: запустите свою строку с опциями dry-run и debug. Программа напечатает профили и вызовы с диапазонами и видом данных, и вы сверите это с прочитанным. Условия профиля она при этом не печатает, их сверяйте по самой строке.

Применим к нашей строке. Служебное отложили. Разделителей нет, профиль один. Условие одно: фильтр tcp, порт четыреста сорок три, а значит, UDP и остальное профиль не принимает. Идём слева направо. Диапазон входящих «эй», первый вызов pcap с аргументом file: входящие «эй», исходящие «эй», данные любые. Дальше диапазон входящих «икс», фильтр на клиентское приветствие и второй вызов, multisplit с аргументом pos равно midsld: входящие «икс», исходящие «эй», данные «клиентское приветствие».

Программа при старте печатает ровно то, что мы вывели: один профиль и две строки про вызовы, у pcap входящие «эй», у multisplit «икс» и клиентское приветствие.

Теперь предсказание до опыта. Pcap получает все пакеты TCP на порт четыреста сорок три из тех, что отдало ядро, в обе стороны. Multisplit только исходящие, и только те, у которых вид данных клиентское приветствие. Значит, в нашем запросе он сработает один раз. Проверим на настоящих пакетах.

![[wiki-anim-video03-s14-apply-to-line.svg|Шесть шагов на нашей строке]]

Проверь себя. Пришёл пакет UDP на порт четыреста сорок три, например QUIC. Что с ним сделает эта строка?

Ничего. Фильтр tcp отключил UDP, пакет достанется пустому профилю no_action и пройдёт нетронутым. Это следует из кода и из нашего теста с похожей строкой, где пакет QUIC по четвёртой версии IP достался no_action.

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

Разделителя два, значит три профиля, и режим разбора это подтвердил. Профиль один: TCP на порт восемьдесят, протокол http, вид данных запрос http. Профиль два: TCP на порт четыреста сорок три, протокол tls, вид данных клиентское приветствие. Профиль три: UDP на порт четыреста сорок три, протокол quic, вид данных начальный пакет quic.

Вызовы называются fake, multisplit и multidisorder: это приёмы следующих выпусков. Заметьте главное: в каждом профиле фильтр протокола и фильтр вида данных стоят вместе, потому что первый про соединение, а второй про пакет.

![[wiki-anim-video03-s14-default-line.svg|Строка по умолчанию]]

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


## Одиннадцать пакетов опыта

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

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

Пакет номер один: SYN от клиента, шестьдесят байт. Условие «TCP, порт четыреста сорок три» выполнено, подходит профиль один. Первым зовут pcap, он записывает пакет в файл.

Вторым программа рассматривает multisplit: диапазон исходящих его пускает, но у SYN вид данных «пусто», а фильтр требует клиентское приветствие. Журнал пишет: «вид пусто не удовлетворяет фильтру», и функцию программа не запускает. Вердикт, то есть ответ программы ядру: пропустить без изменений.

Пакет номер два: SYN-ACK от сервера, тоже шестьдесят байт. Профиль программа берёт из памяти о соединении: профиль один. Pcap записывает пакет. У multisplit диапазон входящих «икс», «никогда», и журнал пишет «вне диапазона». Пропустить.

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

Пакет номер четыре: приветствие, триста два байта. Двадцать байтов заголовка IP и тридцать два заголовка TCP дают пятьдесят два, как у любого подтверждения. Остальное данные, двести пятьдесят.

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

Программа узнаёт приветствие, достаёт имя www.example.com и повторяет выбор профиля: снова профиль один. Pcap записывает пакет таким, каким его отдало ядро.

Теперь multisplit: диапазон «эй», вид данных клиентское приветствие, оба условия выполнены, и функцию зовут. Она вычисляет позицию midsld: сто четыре. И отправляет два пакета: в первом сто четыре байта данных, во втором сто сорок шесть.

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

![[wiki-anim-video03-s15-packet-four.svg|Приветствие клиента: пакет четыре]]

Проверь себя. Почему самописец записал неразрезанное приветствие, хотя разрезает его multisplit?

Потому что pcap записывает пакет в том виде, в каком его отдало ядро, а multisplit исходный пакет не правит: он отправляет две новые части и выбрасывает оригинал. Порядок в строке влияет на порядок строк в журнале, но не на содержимое файла. Так следует из кода, с перестановкой мы не запускали.

Продолжим. Пакет номер пять: подтверждение сервера на первую часть запроса, пятьдесят два байта. Профиль берётся из памяти, pcap записывает пакет. А про multisplit журнал молчит. Причина в самом приветствии: перед разрезом multisplit сам сообщил программе, что входящие пакеты ему больше не нужны. В журнале это строка «instance cutoff», отказ вызова от направления, с пометкой: флаг входящих поднят, исходящих нет. Как это устроено, расскажет выпуск «Lua внутри программы».

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

Пакеты восемь, девять и одиннадцать клиентские и без данных: подтверждение, пакет FIN, ещё подтверждение. Pcap записывает каждый. Multisplit программа рассматривает: диапазон исходящих «эй» его пускает, но вид данных «пусто», фильтр отказывает, и сама функция не запускается. Пакет десять, FIN от сервера, входящий: только pcap.

Подведём итог. Самописец получил все одиннадцать пакетов, среди них неразрезанное приветствие на триста два байта. Журнал упоминает multisplit на семи пакетах, на четырёх молчит, а сработал он на одном, на приветствии.

![[wiki-anim-video03-s15-eleven-table.svg|Одиннадцать пакетов опыта]]

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

Сервер собрал запрос целиком: сто четыре и сто сорок шесть, всего двести пятьдесят байт.

Наша строка меняет один пакет, приветствие, а смотрит на все одиннадцать. Это ответ на первый вопрос выпуска.

Свяжем слова строки со следом в журнале. Фильтр tcp: все одиннадцать пакетов в профиле один. Диапазон «эй» перед pcap: самописец видит и входящие. Диапазон «икс» после него: multisplit входящих не получает. Фильтр на приветствие: multisplit не вызван на пяти пустых клиентских пакетах. Аргумент pos равно midsld дал место разреза.

Проверь себя. После пакета одиннадцать клиент отправил ещё одно подтверждение без данных. Запустится ли на нём multisplit?

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

Итак, из одиннадцати пакетов самописец записал все, multisplit программа рассматривала на семи и запустила на одном, на четвёртом.


## Меняем один аргумент и итог выпуска

Теперь главное упражнение: меняем одну цифру или одно условие и заранее говорим, что изменится. Три опыта.

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

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

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

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

Опыт второй: меняем уже не аргумент, а условие профиля. Добавим «протокол http». Прогноз по тому, что мы знаем: протокол нашего соединения tls, профиль не подойдёт никогда, и все пакеты достанутся no_action.

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

Опыт третий, на будущее: заменим midsld на цифру. В нашем тесте с аргументом pos равным один сервер получил запрос кусками в один байт и двести сорок девять. Без аргумента pos программа режет после второго байта: два и двести сорок восемь. Меняется одна цифра, и место разреза сдвигается. Что ещё принимает pos, расскажет выпуск «Где резать: метки».

![[wiki-anim-video03-s16-three-experiments.svg|Три опыта с прогнозом]]

Проверь себя. Диапазон «би двести пятьдесят, тире, би триста». Разрежет ли он приветствие?

Разрежет: на приветствии би двести пятьдесят, это нижняя граница, и она включается. Так по коду, такого запуска у нас нет.

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

Командная строка zapret2 делится на профили. Профиль номер один существует ещё до первой опции, разделитель new начинает новый пустой профиль и ничего не копирует, а в конце программа сама добавляет пустой no_action, и трафик без профиля идёт нетронутым.

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

Стратегия это вызовы функций: опция lua-desync называет функцию, дальше через двоеточие аргументы. Вызовы идут в порядке записи, у каждого своё имя из функции, профиля и позиции. Аргументы приходят текстом.

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

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

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

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

В нашей строке осталось слово, которое мы только произносили: midsld. Для нашего имени оно даёт сто четыре, мы считали это в выпуске «Что видит наблюдатель». Но мы не знаем, какие ещё бывают метки, например host и endhost, и можно ли резать запрос в нескольких местах сразу. Об этом выпуск «Где резать: метки». А дальше пойдут сами приёмы: разрез на части, перестановка, подделка.

![[wiki-anim-video03-s16-recap-orders.svg|Итог выпуска: чтение строки]]

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

## См. также

- [[02-chto-vidit-nablyudatel|Что видит наблюдатель]] — предыдущее видео серии
- [[04-gde-rezat-metki|Где резать: метки]] — следующее видео
- [[profile]] · [[preset]] — профили и пресеты в Zapret 2
- [[последовательность аргументов]] — как выстраивается цепочка инстансов

---

> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git2.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/video/03-chitaem-komandnuyu-stroku-zapret2.md) · [скачать весь репозиторий одним zip-архивом](https://git2.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).
