О чём заметка

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

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

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

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

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

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

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

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

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

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

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

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

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

Команда лаборатории: заготовка и конец Настоящая команда запуска программы в нашей лаборатории (пути к файлам сокращены) разделена на две части: заготовка скрипта лаборатории (служебные опции, условие «tcp, порт 443», запись всех пакетов в файл и две настройки диапазонов) стоит спереди в каждом опыте, а в конец скрипт дописывает опции самого опыта: фильтр на клиентское приветствие и вызов multisplit; в других опытах заготовка та же, конец другой. настоящая команда нашей лаборатории, пути к файлам сокращены nfqws2 --qnum=200 --debug=@nfqws.log --writable=. --lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua --lua-init=@zapret-pcap.lua --filter-tcp=443 --in-range=a --lua-desync=pcap:file=nfqws_seen.pcap --in-range=x --payload=tls_client_hello --lua-desync=multisplit:pos=midsld заготовка скрипта лаборатории конец опыта в заготовке служебные опции условие: tcp, порт 443 запись всех пакетов в файл две настройки диапазонов в конце: что делает именно этот опыт фильтр на клиентское приветствие + вызов, который режет запрос в других опытах заготовка та же, конец другой, например: --payload=tls_client_hello --lua-desync=multidisorder:pos=midsld

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

Итог опыта: одиннадцать пакетов Итог опыта заранее: программа nfqws2 стоит на клиенте, на пути его пакетов в сеть, и видит одиннадцать пакетов соединения; десять она пропускает без изменений, а четвёртый, запрос в 250 байт, выбрасывает и сама отправляет вместо него два новых пакета в 104 и 146 байт, из которых сервер собирает обратно 250 байт; к этим одиннадцати пакетам выпуск вернётся в конце. Клиентотправляет запросРоутерлабораторныйСерверпринимает запрос nfqws2: стоит здесьна пути пакетов клиента в сеть одиннадцать пакетов опыта, как их видит программа→ от клиента ← к клиенту 1 →S60 2 ←SA60 3 →A52 4 →AP302 5 ←A52 6 ←AP66 7 ←A52 8 →A52 9 →AF52 10 ←AF52 11 →A52 длина IP-пакета, байт выброшензапрос, 250 байт пропущены без изменений: 7 пропущены: 3 программа сама отправляет в сеть вместо запроса две части 104146 байт: 104 + 146 = 250 сервер собрал 250 байт ровно тот же запрос, как и был Десять пакетов пропущено, один выброшен, две части отправлены взамен. К этим одиннадцати пакетам мы вернёмся в конце выпуска.

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

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

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

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

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

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

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

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

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

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

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

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

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

Строка команды по кускам Командная строка лаборатории разобрана на куски и раскрашена по виду опций: серое служебное (номер очереди, файл журнала, папка для файлов, три файла Lua), синее условие профиля (--filter-tcp=443), золотое настройки вызова (два диапазона --in-range и фильтр --payload=tls_client_hello) и зелёное сами вызовы (--lua-desync с функциями pcap и multisplit); слов --new в строке нет, поэтому профиль один. команда опыта, разрезанная на куски по виду опций nfqws2 --qnum=200 --qnum=200 --debug=@nfqws.log --debug=@nfqws.log --writable=. --writable=. --lua-init=@zapret-lib.lua --lua-init=@zapret-lib.lua --lua-init=@zapret-antidpi.lua --lua-init=@zapret-antidpi.lua --lua-init=@zapret-pcap.lua --lua-init=@zapret-pcap.lua --filter-tcp=443 --filter-tcp=443 --in-range=a --in-range=a --lua-desync=pcap:file=nfqws_seen.pcap --lua-desync=pcap:file=nfqws_seen.pcap --in-range=x --in-range=x --payload=tls_client_hello --payload=tls_client_hello --lua-desync=multisplit:pos=midsld --lua-desync=multisplit:pos=midsld ← nfqws2: название программы, а не опция серое: служебноенастройки программы, отбор пакетов не затрагивают синее: условие профилякакие пакеты нужны: только TCP, порт 443 золотое: настройка вызовасама ничего не вызывает, настраивает вызовы правее себя зелёное: вызоводна опция --lua-desync: «вызови функцию с такими настройками» слов --new в строке нет, поэтому профиль один Названия цветов (условие профиля, настройка вызова, вызов) мы будем держать до конца выпуска.

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

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

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

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

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

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

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

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

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

Профиль один существует с самого начала Профиль номер один создаётся ещё до цикла разбора опций, пока ни одной опции не прочитано: три строки кода nfq2/nfqws.c:2288, 2293 и 2294 добавляют в список пустую запись, запоминают указатель dp на неё и ставят ей номер, счётчик профилей меняется с 0 на 1; журнал опыта подтверждает: «we have 1 user defined desync profile(s)»; всё, что написано до первого --new, относится к профилю один. nfq2/nfqws.c: до цикла разбора опций что получается 2288if (!(dpl = dp_list_add(&params.desync_profiles))) 2293dp = &dpl->dp; 2294dp->n = ++desync_profile_count; 1в список профилей добавлена пустая запись 2dp: запомнили, где она лежит 3записи присвоен номер: счётчик растёт и только потом начинается цикл, который читает опции по одной 2314while ((v = getopt_long_only(...)) != -1) desync_profile_count 0 1 пустая запись условия: нетвызовы: нет 1 dpпрофиль, которыйстроится сейчас журнал опыта при старте программы we have 1 user defined desync profile(s) and default low priority profile 0 Всё, что написано до первого --new, относится к профилю один. В нашей команде --new нет.

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

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

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

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

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

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

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

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

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

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

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

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

Три вида опций командной строки Опции командной строки бывают трёх видов: общие (--qnum, --debug, --writable, --lua-init, --blob, --ctrack-disable, --server) настраивают программу целиком; профильные (--filter-tcp и другие фильтры, списки имён и адресов, --name, --template, --import, --cookie, --lua-desync) записываются в профиль, который строится сейчас; «ручка» (--payload, --in-range, --out-range) не записывается в профиль, а хранится в трёх переменных программы (nfq2/nfqws.c:2255-2259); условия профиля по коду можно писать в любом месте профиля, а порядок важен только для ручки. общие опциианалогия: доска объявлений --qnum--debug--writable--lua-init--blob--ctrack-disable--server paramsнастройки программы целиком профильные опциианалогия: записи на странице --filter-tcp/udp/icmp/ipp/l7--hostlist*, --ipset*--name, --template--import, --cookie--lua-desync dpпрофиль, который строится сейчас «ручка»аналогия: цвет ручки --payload--in-range--out-rangeв документации это«фильтры внутри профиля» payload_type range_in range_outтри переменные, nfqws.c:2255-2259 условия профиля: в любом месте профиля (по коду) ручка: действует на вызовы правее себя

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

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

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

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

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

Ручка копируется в каждый вызов Три настройки «ручки» программа копирует в каждый вызов в момент его разбора: в опыте лаборатории --in-range=a стоит перед вызовом pcap, поэтому у pcap входящие a, затем --in-range=x и --payload=tls_client_hello, поэтому у multisplit и pass входящие x, исходящие a и вид данных tls_client_hello; строки журнала при старте профиля один печатают эти значения как range_in=a0-a0 и x0-x0, где a значит «всегда», x «никогда», а число ноль. хвост команды опыта: разбор слева направо --in-range=a --lua-desync=pcap:file=nfqws_seen.pcap --in-range=x --payload=tls_client_hello --lua-desync=multisplit:pos=midsld --lua-desync=pass ручка: три настройки, которые программа помнит в этот момент вид данныхвходящиеисходящие любые tls_client_hello x (никогда) a (всегда) x (никогда) a (всегда) строки журнала при старте: что получил каждый вызов profile 1 (noname) lua pcap(file="nfqws_seen.pcap" range_in=a0-a0 range_out=a0-a0 payload_type= all) profile 1 (noname) lua multisplit(pos="midsld" range_in=x0-x0 range_out=a0-a0 payload_type= tls_client_hello) profile 1 (noname) lua pass( range_in=x0-x0 range_out=a0-a0 payload_type= tls_client_hello) У pcap входящие «a», у двух других «x»: значения копируются в вызов в момент его разбора. a0-a0 читается как «от a до a»: у «a» (всегда) и «x» (никогда) числа нет, печатается 0. Условия профиля (например, порт) в этих строках не печатаются.

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

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

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

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

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

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

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

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

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

Разделитель --new шаг за шагом Разделитель --new в коде (nfq2/nfqws.c:2746-2765) делает пять шагов: увеличивает счётчик профилей с 1 до 2, добавляет в конец списка новую пустую запись, присваивает ей номер 2, записывает имя, если оно задано, и сбрасывает три настройки вызова к начальным значениям (данные любые, входящие x, исходящие a); ни одна строка не копирует прошлый профиль, поэтому новый профиль пуст: нет условий, нет вызовов, и фильтр порта придётся написать в каждом профиле. nfq2/nfqws.c: ветка разделителя --new что получается 2746desync_profile_count++;2748if (!(dpl = dp_list_add(&params.desync_profiles)))2754dp->n = desync_profile_count;2756if (optarg && !(dp->name = strdup(optarg)))2761anon_hl = anon_hl_exclude = NULL;2762anon_ips = anon_ips_exclude = NULL;2763payload_type = 0;2764range_in = PACKET_RANGE_NEVER;2765range_out = PACKET_RANGE_ALWAYS; 1 2 3 4 5 1прежний профиль закрыт, счётчик профилей +1 2новая запись добавлена в конец списка 3записи присвоен номер, равный счётчику 4имя профиля, если оно стоит после --new 5сброс трёх настроек вызова к начальным список профилей, порядок как в строке профиль 1как был desync_profile_count 1 2 новая запись 2 имя: если задано условия: нетвызовы: нет ручка: три настройки вызова до (пример)после --new вид данныхtls_client_hello→ любые входящиеa→ x исходящиеx→ a Новая запись создаётся пустой: нет условий, нет вызовов, нет имени. Ни одна строка не копирует прошлый профиль: фильтр порта, нужный в двух профилях, пишут в каждом.

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

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

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

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

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

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

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

Три профиля в режиме разбора Запуск программы в режиме разбора с отладкой, без пакетов: команда из трёх частей, разделённых двумя --new, даёт три профиля, заданных пользователем; профиль 1 получил то, что написано (входящие a, исходящие x, данные tls_client_hello), профиль 2 только начальные значения (входящие x, исходящие a, данные любые), профиль 3 напечатан двумя строками по числу вызовов pass: входящие a записаны раньше обоих вызовов, остальное начальное; настройка копируется в вызов при его разборе, а --new возвращает её к начальной. запуск в режиме разбора с отладкой, без пакетов; служебные опции опущены --payload=tls_client_hello--in-range=a--out-range=x--lua-desync=pass→ профиль 1 --new--lua-desync=pass→ профиль 2 --new--in-range=a--lua-desync=pass--lua-desync=pass→ профиль 3 что напечатала программа we have 3 user defined desync profile(s)два разделителя, три профиля profile 1 (noname) lua pass( range_in=a0-a0 range_out=x0-x0 payload_type= tls_client_hello) всё, как написали profile 2 (noname) lua pass( range_in=x0-x0 range_out=a0-a0 payload_type= all) значения начальные profile 3 (noname) lua pass( range_in=a0-a0 range_out=a0-a0 payload_type= all) a: до обоих pass profile 3 (noname) lua pass( range_in=a0-a0 range_out=a0-a0 payload_type= all) записано наминачальное значение: мы его не писали Настройка копируется в вызов в момент его разбора, а --new возвращает её к начальной. Если бы pass стоял раньше диапазона, он получил бы начальный «x».

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

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

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

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

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

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

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

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

Пустой профиль no_action в конце списка Профиль no_action: после разбора командной строки программа сама подшивает в конец списка пустой профиль с номером 0 (nfqws.c:3192-3201), и пакет, не подошедший профилю 1, достаётся ему; в опыте с условием «протокол http» на соединении TLS профиль искали на первом пакете и на приветствии, остальные восемь из десяти пакетов взяли его из памяти, функций Lua не было, сервер получил 250 байт одним куском. в конец списка профилей программа сама подшивает пустой профиль профиль 1--filter-l7=httpнужен протокол http, у нас tls профили 2, 3 и другиеесли они есть 3192DLOG("adding low-priority default empty desync profile\n");3194if (!(dpl = dp_list_add(&params.desync_profiles)) || !(dpl->dp.name=strdup("no_action")))3200DLOG_CONDUP("we have %u user defined desync profile(s) and default low priority profile 0\n", …); при запуске журнал пишет: добавляется пустой профиль по умолчанию, с низким приоритетом профиль подшивается В КОНЕЦ списка, его имя no_action у последнего профиля номер 0 (память обнулена): в число «своих» профилей он не входит профиль 0: no_actionпустой: условий и вызовов нетдобавлен программой в конец пакет не подошёл принимает всё опыт лаборатории: профиль 1 с условием http, соединение TLS, десять пакетов 1поискSYN 2память 3память 4поискзапрос 250 байт 5память 6память 7память 8память 9память 10память на каждом из десяти пакетов журнал пишет:no lua functions in this profilepacket: id=N pass unmodified клиент: сервер получил 250 байтодним куском, запрос не тронут Пакет без подходящего профиля не трогают: его принимает пустой профиль (по коду).

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

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

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

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

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

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

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

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

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

Пропуск, имя и шаблон профиля Три мелкие опции: skip убирает тот профиль, где стоит, и следующий занимает его номер (из четырёх частей получается три профиля); имя задаётся значением у new или опцией name, а имя после new через пробел игнорируется; template уносит профиль на полку шаблонов, import копирует его вызовы в текущий профиль. три мелкие опции вокруг разделителя --new --skipвыключает тот профиль, где стоит 1 · без имени№ 1 2 · second№ 2 3 · skipped + --skip№ 3 3 · skipped: запись стёрта 4 · fourth№ 3 (тот же) в журнале 3 профиля:noname, second, fourth nfqws.c:2769-2771: поднят флагnfqws.c:2725-2731: на следующем --newзапись стёрта, номер не растёт опечатка в пропущенном профилевсё равно останавливает запуск:bad identifier 'pa-ss', выход 1 имя профиляв журнале вместо «noname» --new=secondprofile 2 (second) --name=fourthprofile 3 (fourth) --new secondprofile 2 (noname)имя через пробел игнорируется Имя доходит до Luaкак desync.profile_name(desync.c:996-997), а выбор профиляимя не использует. --template, --importобразец отдельно от профилей полка шаблонов, вне выбораT: passn1-n3 · tls_client_hello --import=T: копия профиль 1 (real)passскопировано из Tdropсвоё: после импорта журнал: 1 профиль и 1 шаблоншаблон в число профилей не входит образец уже закрыт разделителем;имя и номер не копируются Skip убирает профиль, где стоит. Имя идёт в Lua, шаблон лежит отдельно, импорт копирует.

Итак, в конец списка программа сама добавляет пустой профиль 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 лежит в библиотеке помощников.

Стратегия это функция с именем Стратегия это глобальная Lua-функция с двумя параметрами: pass из lua/zapret-lib.lua:43-45 имеет имя pass, параметры ctx и desync и одну строку записи в журнал; опция --lua-desync=pass называет ящик с этим именем, а функции лежат в файлах zapret-lib.lua (pass), zapret-antidpi.lua (25 готовых стратегий, среди них multisplit, drop и fake) и zapret-pcap.lua (pcap), которые выполняются один раз при запуске. стратегия: функция Lua, найденная по имени внутри ящика pass (lua/zapret-lib.lua:43-45) 43function pass(ctx, desync)44DLOG("pass")45end имя два параметра ← записать в журнал в журнале опыта одна строка:LUA: passОтвета нет вообще: это «пустышка». в команде--lua-desync=passопция называет ящик с таким именем zapret-lib.luaбиблиотека помощниковpassдругие найден по имени zapret-pcap.luapcapсамописец zapret-antidpi.luaготовые стратегии: 25 multisplitразрезает dropвыбрасывает fakeподделки ещё 22 стратегии: назовём позже Файлы выполняются один раз при запуске, функции остаются в памяти.Правка Lua-файла действует только после перезапуска (по коду). Списка стратегий нигде нет: годится любая глобальная функция Lua с двумя параметрами.

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

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

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

Две проверки имени и замена функции Программа проверяет имя функции дважды: при запуске (nfq2/lua.c:4080-4100; несуществующая функция останавливает Lua, но клиент всё равно получает ответ из-за bypass в правиле очереди) и перед каждым вызовом (nfq2/desync.c:1055-1061); файлы выполняются по порядку, поэтому drop из четвёртого файла затирает готовую drop, и запрос в опыте дошёл до сервера целиком. имя функции проверяется дважды, а позднее определение заменяет раннее замок 1: при запускеnfq2/lua.c:4080-4100 4089lua_getglobal(params.L, func->func);4090if (!lua_isfunction(params.L,-1))4093DLOG_ERR("desync function '%s' does not exist\n", …) для каждого вызова: нет функции — сообщение и остановкарежим разбора Lua не создаёт, ту же строку принимает (код 0) опыт: --lua-desync=nonexistent_fndesync function 'nonexistent_fn'does not existLUA SHUTDOWNклиент всё равно получил ответ: в правилеочереди лаборатории стоит bypass замок 2: перед каждым вызовомnfq2/desync.c:1055-1061снова берёт имя и проверяет, что это функция файлы выполняются по порядку: позднее определение затирает раннее zapret-lib.luapass zapret-antidpi.luadropготовая zapret-pcap.luapcap свой файлdropсвоя затёрта: ящик drop теперь с новой функцией В опыте своя drop ничего не отвечает: запрос, который настоящий drop выбросил бы,дошёл до сервера целиком (250 байт). Функция-стратегия: глобальная функция Lua; имя проверяется при запуске и перед вызовом.

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

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

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

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

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

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

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

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

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

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

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

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

Разбор опции lua-desync Разбор опции lua-desync: имя функции это всё до первого двоеточия (multisplit, pcap), дальше куски через двоеточие делятся по первому знаку равно на имя аргумента и значение (pos и midsld, file и nfqws_seen.pcap); имена должны быть идентификаторами, а в нашем режиме разбора имя с дефисом, аргумент без имени и имя аргумента с цифры в начале дают ошибку, значение с защищённым двоеточием, аргумент без значения и значение x=y принимаются. как опция lua-desync разбирается на имя и аргументы --lua-desync= multisplit : pos = midsld имя функциидо первого «:» имя аргументадо первого «=» значениепосле «=» --lua-desync= pcap : file = nfqws_seen.pcap имя и имя аргумента: идентификаторыпервый символ: латинская буква или _дальше: буквы, цифры, _ни дефиса, ни точки, ни цифры в началеnfq2/helpers.c:102-110 что вышло в нашем режиме разбора (без запуска пакетов) pa-ssbad identifier 'pa-ss'имя функции с дефисом, выход 1 pass:=3invalid lua function callаргумент без имени pass:1a=2invalid lua function callимя аргумента начинается с цифры pass:a=x\:y:b=2a = "x:y", b = "2"защищённое «\:» осталось в значении pass:a:b=:a = "", b = ""аргумент без значения принимается pass:k=x=yk = "x=y"делится только по первому «=» Ошибка формы останавливает запуск до первого пакета. Значение: всё после первого «=»; без «=» значения нет, голое имя принимается.

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

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

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

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

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

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

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

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

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

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

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

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

Порядок вызовов и имена экземпляров Порядок вызовов и имя экземпляра: вызовы профиля стоят в порядке командной строки (pcap, multisplit, pass), имя экземпляра это функция, номер профиля и позиция (pcap_1_1, multisplit_1_2, pass_1_3), позиция считается и у пропущенного на этом пакете вызова (multisplit_3_3 в примере), а ответы вызовов складываются: побеждает сильнейший из PASS, MODIFY, DROP, ошибка вызова останавливает остальные и пакет проходит без изменений. вызовы идут в порядке команды, у каждого своё имя имя экземпляра = функция _ профиль _ позиция 1pcap:file=nfqws_seen.pcappcap_1_1 2multisplit:pos=midsldmultisplit_1_2 3passpass_1_3 профиль 1 (опыт с пустышкой pass); слева позиция журнал опыта, пакет с запросом:* lua 'pcap_1_1' : …* lua 'multisplit_1_2' : … порядок строк = порядок вызовов в команде пример (по коду, не запускали): профиль 3 1passpass_3_1 2dropdrop_3_2пропущен 3multisplitmultisplit_3_3 позиция считается и у пропущенных вызововтак имя не меняется от пакета к пакету (desync.c:1102) ответы вызовов одного пакета складываются PASS< MODIFY< DROP побеждает сильнейший (desync.c:875-888) при ошибке вызова остальные не запускаются вызов 1 ошибка вызов 3 вызов 4 итог: PASS, пакет без изменений (desync.c:1142-1146) Оба правила про сложение и ошибкувзяты из кода, опытом не проверялись.Имя экземпляра: функция, профиль, позиция.

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

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

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

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

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

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

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

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

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

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

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

Что получает функция: аргументы текстом Что получает функция: всё записанное в опции приходит в Lua текстом; в лабораторном опыте a=1 пришло строкой «1», аргумент без знака равно и с пустым значением пустой строкой, d=%myblob четырьмя байтами блоба AA BB CC DD, e=#myblob строкой «4» (длиной блоба), а косая черта перед процентом или решёткой отключила подстановку; если блоба с таким именем нет, программа пишет, что блоб недоступен, и пакет проходит без изменений (nfq2/lua.c:1788-1849). аргументы вызова приходят в Lua текстом в нашем тесте блоб создала команда:--blob=myblob:0xAABBCCDD AABBCCDD блоб myblob: четыре байта записано в опциифункция получилапочему a=1"1"строка из одного символа, не число b""нет знака «=»: пустая строка c=""знак «=» и пустое значение: пустая строка d=%myblobAA BB CC DD«%» подставляет сам блоб: четыре байта e=#myblob"4"«#» подставляет длину блоба, тоже текстом f=\%myblob"%myblob"косая черта отключила подстановку и ушла g=\#myblob"#myblob"то же для «#» x=%nopeблоба с таким именем нет:blob 'nope' unavailabledesync ERROR. passing packet unmodified.функция не запускается, остальные вызовы этого пакета тоже, пакет проходит без изменений Три готовых блоба есть в программе всегда: они нужны стратегиям с подделкой пакетов. Всё приходит текстом: «%» подставляет блоб, «#» его длину, «\» отключает подстановку.

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

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

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

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

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

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

Повторный аргумент: побеждает первый Если один аргумент записан дважды, побеждает записанный первым: список аргументов хранится развёрнутым (вставка в начало, nfq2/pools.c:213), а при записи в таблицу для Lua каждое значение затирает прежнее, поэтому из a=1:a=2:a=3 остаётся 1; в опыте multisplit с pos=2:pos=midsld разрезал запрос на 2 и 248 байт, а с pos=midsld:pos=2 на 104 и 146 байт. один аргумент записан дважды --lua-desync=pass:a=1:a=2:a=3 1. добавление в списокpools.c:209-215: новый аргумент встаёт в начало после a=1a=1 после a=2a=2a=1 после a=3a=3a=2a=1 режим разбора печатает список развёрнутым:pass(a="3",a="2",a="1" …) 2. запись в таблицу для Lualua.c:1805: идём с начала списка, пишем по имени a ← "3" a ← "2" a ← "1" a = "1" каждая запись затирает прежнюю: остаётся записанноепоследним, то есть стоявшее в команде первым опыт: multisplit получил pos дважды multisplit:pos=2:pos=midsldLUA: multisplit: split pos: 22248 multisplit:pos=midsld:pos=2LUA: multisplit: split pos: midsld104146 первый опыт: сервер получил 2 + 248 = 250 байтвторой опыт: 104 + 146 = 250 байт, снова первый Особого правила «первый побеждает» в коде нет:это следствие вставки в начало и записи по имени. По общим сведениям, во многих программах побеждает последнее значение, а здесь наоборот.Если аргумент записан дважды, побеждает написанный первым.

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

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

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

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

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

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

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

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

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

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

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

Фильтр порта: порт сервера Фильтр порта сравнивается с портом сервера в обе стороны: и у запроса клиента 10.1.0.1, и у ответа сервера 10.2.0.2 это порт 443, журнал печатает цель поиска «tcp ip=10.2.0.2 port=443»; записи порта: 443 это только 443, 80-90 это диапазон включительно, ~443 это все порты кроме 443, * это все порты, записи с перевёрнутыми границами, числом больше 65535 или буквами отвергаются, а несколько записей работают как «или». фильтр порта сравнивается с портом сервера Клиент10.1.0.1Сервер10.2.0.2 запрос клиента: порт назначения порт 443 ответ сервера: порт отправителя порт 443 цель = удалённый конец: порт сервера, в обе стороны режим --server меняет направления местами (порт всё равно серверный): в выпуске не разбираем журнал перед каждым поиском профиля (строка из лаборатории) desync profile search for tcp ip=10.2.0.2 port=443icmp=255:255 l7proto=unknown ssid='' mark=0x00000000 hostname='' ← протокол, адрес сервера, порт сервера ← условное значение для TCP и UDP как записывается порт (по коду: pf_parse и pf_match, nfq2/filter.c) 443только порт 443 80-90любой порт от 80 до 90 включительно: 80, 85, 90 подходят, 79 и 91 нет ~443тильда это отрицание: подходит любой порт, кроме 443 *звёздочка: все порты, от 1 до 65535 ~1-1023отрицание диапазона: порты 1024 и выше 443-8065536abcотказ: запуск останавливается («Invalid port filter») несколько записей (через запятую или повтором опции) работают как «или»: достаточно однойопыт лаборатории: к «--filter-tcp=443» добавили «--filter-tcp=~443», профиль сработал, куски 104 + 146 байт.Это согласуется с «или», но один этот опыт «или» не доказывает: запись 443 подходит приветствию и сама.

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

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

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

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

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

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

Фильтры транспорта по умолчанию Таблица фильтров транспорта: если в профиле не написан ни один из фильтров tcp, udp, icmp и ipp, он принимает TCP, UDP, ICMP и прочие протоколы IP; если написан хотя бы один, остальные не принимают никого, поэтому пара --filter-tcp=443 и --filter-udp=443 примет TCP и UDP на порт 443, но не ICMP; внутри программы пустой фильтр заменяется записью «0», которая не пропускает ни один порт. фильтры транспорта: что принимает профиль TCP--filter-tcpUDP--filter-udpICMP--filter-icmpпрочие IP--filter-ipp в профиле не написано ничегоопыт: сработал и на UDPда, любойда, любойда, любойда, любой --filter-tcp=443опыт: на UDP-пакеты не сработалда: порт 443нетнетнет --filter-udp=443по кодунетда: порт 443нетнет --filter-tcp=443 --filter-udp=443следует из кодада: порт 443да: порт 443нетнет в коде: filter_defaults, nfq2/nfqws.c:1543-1557 1546 if (!dp->b_filter_l3) dp->filter_ipv4 = dp->filter_ipv6 = true;версия IP не указана: обе 1549 if (!LIST_EMPTY(&dp->pf_tcp) || !LIST_EMPTY(&dp->pf_udp) || …)написан хотя бы один из четырёх 1551 return port_filters_deny_if_empty(&dp->pf_tcp) && …пустые получают «запрет» как «отключено» устроено внутри (nfq2/filter.c:8-11, 44-45) пустой список заменяется записью «0»: from = 0, to = 0, neg = 1 порт 443: 443 && ((1) ^ 1) = 0ложь: такая запись не пропускает ни один порт Чтобы стратегия работала и для TCP, и для UDP: напишите оба фильтра или не пишите ни одного.

Проверь себя. Профиль, где написаны оба фильтра, 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. Два словаря, и путать их не нужно.

Два фильтра: протокол и вид данных Две разные опции: filter-l7 это условие профиля про всё соединение (13 настоящих протоколов и слова all, unknown, known), а payload это настройка вызова про один пакет (виды данных empty, unknown, tls_client_hello, quic_initial, http_req и другие); у них разные словари: протоколу tls соответствует вид tls_client_hello, quic это quic_initial, http это http_req. два разных условия: фильтр протокола и фильтр вида данных filter-l7читается «фильтр эл семь»условие ПРОФИЛЯпро всё соединение: протокол tls, http, quic… payloadфильтр вида данныхнастройка ВЫЗОВАпро один пакет: вид данных tls_client_hello… значения filter-l7 httptlsdtlsquicwireguarddhtdiscordstunxmppdnsmtprotobtutp_bt 13 настоящих протоколов и три служебных слова: allлюбой unknownпока неизвестно knownлюбой известный значения payload emptyпакет без данных (SYN, ACK) unknownданные есть, но не опознаны tls_client_helloклиентское приветствие TLS quic_initialhttp_reqи другие виды allлюбые knownлюбой опознанный вид два словаря: протокол соединения и вид данных пакета tlstls_client_hello quicquic_initial httphttp_req Названия видов данных: файл config.default, строки 88-90. Несколько значений пишут через запятую. Два словаря, и путать их не нужно.

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

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

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

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

Обложка соединения и штамп пакета Игрушка про обложку папки и штамп письма на настоящих пакетах лаборатории: обложка (протокол соединения) остаётся unknown на SYN, SYN-ACK и первом ACK и становится tls с приветствия, а штамп (вид данных пакета) у пакетов без данных empty, у приветствия tls_client_hello, у ответа сервера unknown; вызов с payload=tls_client_hello запускается только на приветствии, а профиль с filter-l7=tls к SYN не подходит. игрушка (выдумана): обложка папки и штамп письма на настоящих пакетах пакетиз журнала лабораторииобложкаconnection_protoпротокол соединенияштампpayload_typeвид данных пакета 1 → SYNunknownempty 2 ← SYN-ACKunknownempty 3 → ACKunknownempty 4 → приветствиеtlstls_client_hello 5 ← ACKtlsempty 6 ← ответ сервераtlsunknown 7–11 ещё пять пакетовtlsempty дверь профиляfilter-l7=tlsпо коду не подходитне подходитне подходитподходитподходитподходитподходит вызов multisplitpayload=tls_client_helloкак в лаборатории не зовут: пустоне зовут: пустоне зовут: пустоЗОВУТне зовут: пустоне зовут: неизвестноне зовут: пусто * lua 'multisplit_1_2' : payload_type 'empty' does not satisfy filterSYN, ACK: не запускают* lua 'multisplit_1_2' : payload_type 'tls_client_hello' satisfy filterприветствие: работает опыт: filter-l7=http на соединении TLSпрофиль не подошёл ни на SYN, ни на приветствии:запрос ушёл одним куском, 250 байт опыт: filter-l7=unknown, затем профиль с tlsна SYN подошёл первый профиль, на приветствиивыбор переехал на второй: куски 104 + 146 байт

Проверь себя. Профиль с условием «протокол 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, дали «нет». Профиль не подошёл, запрос прошёл целым куском в двести пятьдесят байт, а пакеты получил пустой профиль. Чужое имя не приводит к выбрасыванию пакетов: пакет достаётся профилю без вызовов. Это про обычный список, автоматические мы не разбираем.

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

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

Список имён: как сравнивается имя Список имён: в файле комментарии и пустые строки пропускаются, имя читается до первого пробела; имя из запроса www.example.com проверяется целиком, потом без левой метки (example.com), потом com; запись example.com подошла на второй пробе и запрос разрезан на 104 и 146 байт, запись example.org не подошла ни на одной из трёх проб и запрос ушёл целым в 250 байт; запись со знаком крышки подходит только целому имени, шаблона со звёздочкой нет; исключающий список сильнее включающего, два включающих работают как «или», пустой файл означает «списка нет». список имён (hostlist): файл и сравнение имени пример файла списка # commentпропуск: комментарий example.comзапись: имя до пробела ^strict.example.org«крышка»: целое имя (пустая строка)пропуск: пустая строка как подключить --hostlist=файлвключающий --hostlist-exclude=файлисключающий --hostlist-domains=a,bвключающий, в команде --hostlist-exclude-domains=a,bисключающий, в команде как имя из запроса сравнивается со списком (по коду: SearchHostList, nfq2/hostlist.c:195-227) www.example.comexample.comcom имя из запроса на каждом шаге убирается самая левая метка; запись подходит, если равна любому из этих хвостов запись example.com negative positive не нужно подошёл: куски 104 + 146 запись example.org negative negative negative не подошёлзапрос целым, 250 байт что подходит под имя www.example.com (опыты лаборатории) example.comдахвост совпал example.orgнеттри пробы negative ^example.comнетstrict_mismatch ^www.example.comдацелое имя совпало *.example.comнетшаблонов нет исключающий списоксильнее включающего два включающихработают как «или» файл без записей= «списка нет» изменённый файлперечитан сам (по коду) Чужое имя не приводит к выбрасыванию пакетов: пакет достаётся пустому профилю без вызовов, запрос идёт целым.

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

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

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

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

С какого пакета профиль видит соединение С какого пакета профиль видит соединение: список адресов 10.2.0.0/24 проверяет первые 24 бита адреса сервера 10.2.0.2 и подходит уже на SYN (в файле самописца 11 пакетов, первый в 60 байт), а список имён ждёт приветствия, где появляется имя (в файле 10 пакетов, первый в 302 байта, трёх пакетов рукопожатия нет), и до этого пакеты получает пустой профиль no_action. с какого пакета профиль видит соединение 10200подсеть 10.2.0.0/24 10202сервер 10.2.0.2 первые 24бита равныподходит список адресов (ipset): проверяется адрес удалённого конца, в обычном режиме клиента это сервер список имёнимя только в приветствии SYNno_action SYN-ACKno_action ACKno_action приветствиесвой профиль и дальшесвой профиль здесь профиль впервые видит соединение список адресовадрес известен уже на SYN SYNсвой профиль SYN-ACKсвой профиль ACKсвой профиль приветствиесвой профиль и дальшесвой профиль видит с самого первого пакета файл самописца, список имён10 пакетов, первый это приветствие: 302 байтаSYN, SYN-ACK и первого ACK в файле нет файл самописца, список адресов11 пакетов, первый это SYN: 60 байтподсеть 10.2.0.0/24 содержит адрес сервера исключающий список тоже ждёт имени:на SYN его проверять не по чему опция запоминания имён по адресам это меняет;в лаборатории её не включали

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

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

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

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

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

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

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

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

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

Первое совпадение выигрывает Правило «первый подошёл»: в игрушке с тремя дверями подряд пакет на порт 443 входит во вторую дверь, а пакет на порт 8080 в третью, у которой нет условий; так же программа просматривает профили от первого к последнему, и в опыте лаборатории с двумя подходящими профилями разрез сделал первый (куски 2 и 248 байт, всего 250), а при обратном порядке по коду были бы куски 104 и 146. первый подошедший профиль побеждает игрушка (выдумана): три двери подряд дверь 1только порт 80 дверь 2только порт 443 дверь 3без условий порт 443 порт 443: входит во вторую дверь, до третьей не доходит порт 8080 порт 8080: первые две двери не подошли, входит в третью: условий у неё нет так работает и программа: профили просматриваются по порядку командной строки профиль 1multisplit:pos=2заготовка лаборатории: порт 443 профиль 2multisplit:pos=midsldусловий нет (после --new пусто) no_actionпустой, без вызововвсегда стоит последним ← подошёл первым,остальные не нужны ← тоже подходит, но непонадобился опыт лаборатории: сервер получил запрос кусками 2 байта: разрез сделал профиль 1248 байт 2 + 248 = 250 байт Если поменять профили местами, по коду разрез был бы по midsld: куски 104 + 146 (такого опыта у нас нет).

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

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

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

Выбор профиля и его повтор Профиль выбирается на первом пакете и запоминается: на SYN не подходят ни профиль 1 (список имён), ни профиль 2 (протокол tls), берётся пустой no_action, на SYN-ACK и ACK выбор берётся из памяти, а на приветствии, когда выяснились протокол tls и имя, выбор повторяется: профиль 1 отвечает «нет», профиль 2 подходит, журнал пишет «desync profile changed by revealed l7 protocol or hostname !» и сервер получает куски 104 и 146 байт. когда выбирается профиль и когда выбор повторяется профиль 1список имёнпрофиль 2протокол tlsвыбрандля этого пакета SYN протокол и имянеизвестны нетнужно имя, его нет нетпротокол неизвестен no_actionвыбор запоминается SYN-ACK протокол и имянеизвестны не проверяетсявыбор из памяти не проверяетсявыбор из памяти no_actionвзят из памяти ACK протокол и имянеизвестны не проверяетсявыбор из памяти не проверяетсявыбор из памяти no_actionвзят из памяти приветствие протокол tls,имя www.example.com неттри пробы negative подходитпротокол tls профиль 2выбор повторён desync profile changed by revealed l7 protocol or hostname !в журнале: профиль изменился из-за открывшегося протокола или имени; дальше соединение ведёт профиль 2 сервер получил запрос кусками 104 байта146 байт 104 + 146 = 250 байт Профиль, которому нужно имя или протокол, видит соединение только с приветствия.

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

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

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

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

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

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

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

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

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

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

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

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

Диапазон: счётчики и граница Диапазон строится на счётчиках: из пяти букв нам нужны три (n пакет по счёту, d пакет с данными по счёту, b байты данных; s и p смотрят на номера TCP и не разбираются, x значит «никогда», a «всегда»); на клиентском приветствии нашего опыта n3, d1, b250 (в журнале это пакет номер 4, потому что журнал считает оба направления); счётчик растёт до проверки, у направлений счётчики свои, считается только то, что увидела программа; запись границ: n3-n5 даёт пакеты 3, 4, 5, n3<n5 даёт 3 и 4, n3-n3 только третий, n3- начиная с третьего, <n4 пакеты 1, 2, 3, а одна граница n3 без разделителя это ошибка. диапазон: счётчики и запись границ nпакет по счёту(считая текущий) dпакет с даннымипо счёту bбайты данных(считая текущий) s, pномера TCP:не разбираем xникогдаaвсегда букв счётчиков пять, нам нужны три: n, d, b числа нашего запроса: клиентские пакеты по порядку SYNn 1 d 0 b 0 ACKn 2 d 0 b 0 приветствиеn 3 d 1 b 250 в журнале этот пакет под № 4:журнал считает оба направления,а n только клиента 1. растут до проверкитекущий пакет ужепосчитан 2. у направлений своив режиме клиента: исходящиесмотрят на счётчики клиента,входящие на счётчики сервера 3. считается увиденноев лаборатории программеотдают первые 12 пакетовкаждого направления как записать диапазон (разбор: nfq2/filter.c:126-171) пакеты клиента по счёту n: n3-n5123456тире включает верхнюю границу: пакеты 3, 4, 5 n3<n5123456«меньше» исключает верхнюю границу: пакеты 3, 4 n3-n3123456ровно третий пакет n3-123456начиная с третьего: верхней границы нет <n4123456нижней границы нет, четвёртый исключён: пакеты 1, 2, 3 n3n3: нельзяошибка: одна граница без разделителя, программа остановится вместо числа: a значит «всегда», x «никогда»; начальные значения: входящие x, исходящие aрежим разбора печатает n3-, -n3, <n3 как n3-a0, a0-n3, a0<n3

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

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

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

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

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

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

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

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

Диапазон d1-d1 и лампа Диапазон d1-d1 значит «пока счётчик d равен единице», а не «ровно один пакет»: в игрушке с лампой над дверью лампа горит, пока счётчик писем с вложением равен единице, и гаснет только на втором письме с вложением; в опыте лаборатории SYN и первое подтверждение (d0) вне диапазона, приветствие (d1) проходит оба затвора и multisplit режет запрос на 104 и 146 байт, а пустые подтверждение, FIN и подтверждение (d1) проходят диапазон, но фильтр вида данных их останавливает и вызов не запускается. диапазон d1-d1: «пока счётчик d равен единице» игрушка (выдуманный пример): лампа над дверью горит, пока счётчик писем с вложением равен единице до писемсчётчик 0не горит письмос вложениемсчётчик 1горит пустоеписьмосчётчик 1горит ещё однопустоесчётчик 1горит второе письмос вложениемсчётчик 2гаснет опыт лаборатории: шесть исходящих пакетов клиента --payload=tls_client_hello --out-range=d1-d1 --lua-desync=multisplit:pos=midsld пакетdзатвор 1: диапазон d1-d1затвор 2: вид данныхитог № 1 · SYNd0 вне диапазона не проверяется вызов не зовут № 3 · ACKd0 вне диапазона не проверяется вызов не зовут № 4 · приветствиеd1 в диапазоне подходит зовут: 104 + 146 № 8 · ACKd1 в диапазоне вид «пусто»: нет вызов не зовут № 9 · FINd1 в диапазоне вид «пусто»: нет вызов не зовут № 11 · ACKd1 в диапазоне вид «пусто»: нет вызов не зовут входящие пакеты вызова не касаются: диапазон входящих у него x (журнал: in pos x0 x0 out of range x0-x0) * lua 'multisplit_1_2' : out pos d0 d0 out of range d1-d1← № 1 и № 3 * lua 'multisplit_1_2' : out pos d1 d1 in range d1-d1← № 4, 8, 9, 11: диапазон верен * lua 'multisplit_1_2' : payload_type 'empty' does not satisfy filter← № 8, 9, 11: второй затвор «d1-d1» значит «пока счётчик d равен единице», а не «ровно один пакет»пакеты без данных счётчик d не меняют; их останавливает второй затвор, фильтр вида данных сравнение с буквой n: она растёт на каждом клиентском пакете, n = 1, 2, 3, 4, 5, 6 по коду n3-n3 верно ровно на приветствии: такой запуск мы не делали в нашем опыте n4<n4 не пропустил ни одного пакета: n1-n3 ниже границы, n4 исключён знаком «<», n5-n6 выше

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Шесть шагов на нашей строке Шесть шагов чтения применены к лабораторной строке: служебное отложено, слов --new нет и профиль один, условие одно (TCP, порт 443), затем по ленте слева направо ведутся три настройки (вид данных, входящие, исходящие): pcap получает входящие a, исходящие a и любые данные, multisplit получает входящие x, исходящие a и клиентское приветствие; программа при старте печатает ровно это (один профиль и две строки про вызовы), а предсказание до опыта такое: pcap получит все пакеты TCP:443 в обе стороны, multisplit только исходящее приветствие, то есть один раз. шаг 1отложитьслужебное шаг 2найтипрофили шаг 3выписатьусловия шаг 4вести тринастройки шаг 5вызовы иаргументы шаг 6сверитьс журналом шесть шагов чтения, применённые к строке нашей лаборатории служебное: --qnum, --debug, --writable, --lua-init ×3 шаг 2: слов --new нет, профиль один --filter-tcp=443 шаг 3: только TCP, порт 443 три настройки по ходу чтения (записываются в каждый вызов) вид данных входящие исходящие любыеxa --in-range=a a --lua-desync=pcap:file=nfqws_seen.pcap вх a · исх a · любые ← --in-range=x x --payload=tls_client_hello tls_client_hello --lua-desync=multisplit:pos=midsld вх x · исх a · tls_client_hello ← исключения шага 1: --server и --ctrack-disable меняют выбор профиля; мы их не разбирали аргументы: имя=значение аргумент pos шаг 6: программа при старте напечатала то же самое (журнал опыта) we have 1 user defined desync profile(s) and default low priority profile 0 profile 1 (noname) lua pcap(file="nfqws_seen.pcap" range_in=a0-a0 range_out=a0-a0 payload_type= all) profile 1 (noname) lua multisplit(pos="midsld" range_in=x0-x0 range_out=a0-a0 payload_type= tls_client_hello) один профиль и две строки про вызовы: совпало; условия профиля программа не печатает, их сверяют по самой строке предсказание до опыта: какие из одиннадцати пакетов получит вызов pcap1234567891011все пакеты TCP:443 от ядра, в обе стороны multisplit1234567891011только исходящее приветствие: один раз пакеты пронумерованы как в журнале опыта; настоящий проход по ним в следующей сцене

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

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

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

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

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

Строка по умолчанию Настоящая строка по умолчанию из config.default разделена двумя --new на три профиля: TCP 80, протокол http, запрос http; TCP 443, протокол tls, клиентское приветствие; UDP 443, протокол quic, начальный пакет quic; условия профиля синие, вид данных золотой, вызовы fake, multisplit и multidisorder зелёные, аргументы после двоеточия серые (их разберут следующие выпуски), в конце стоит пустой no_action, а режим разбора подтверждает три профиля и пять вызовов. настройка по умолчанию из config.default (строки 87-91) служебные опции перед ней добавляет скрипт запуска; слова <HOSTLIST> здесь опущены профиль 1TCP:80 --filter-tcp=80--filter-l7=http--payload=http_req--lua-desync=fake:blob=fake_default_http:tcp_md5--lua-desync=multisplit:pos=method+2 профиль 2TCP:443 --new--filter-tcp=443--filter-l7=tls--payload=tls_client_hello--lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000--lua-desync=multidisorder:pos=1,midsld профиль 3UDP:443 --new--filter-udp=443--filter-l7=quic--payload=quic_initial--lua-desync=fake:blob=fake_default_quic:repeats=6 no_action: пустой профиль в самом конце, его добавляет программа --filter-tcp=80--filter-l7=http--filter-tcp=443--filter-l7=tls--filter-udp=443--filter-l7=quic условие профилятранспорт, порт, протокол: какие соединения нужны --payload=http_req--payload=tls_client_hello--payload=quic_initial ручкавид данных: какой пакет нужен; диапазоны не заданы, действуют x и a --lua-desync=fake:blob=fake_default_http:tcp_md5--lua-desync=multisplit:pos=method+2--lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000--lua-desync=multidisorder:pos=1,midsld--lua-desync=fake:blob=fake_default_quic:repeats=6 вызовыаргументы после двоеточий серые: их разберут следующие выпуски режим разбора подтвердил: три профиля и пять вызовов (вывод сокращён) we have 3 user defined desync profile(s) profile 1 (noname) lua fake(tcp_md5="",blob="fake_default_http" ... payload_type= http_req) profile 1 (noname) lua multisplit(pos="method+2" ...) profile 2 (noname) lua fake(tcp_seq="-10000",tcp_md5="",blob="fake_default_tls" ...) profile 2 (noname) lua multidisorder(pos="1,midsld" ...) profile 3 (noname) lua fake(repeats="6",blob="fake_default_quic" ... payload_type= quic_initial) в каждом профиле фильтр протокола и фильтр вида данных стоят вместепервый (--filter-l7) про соединение, второй (--payload) про пакет

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

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

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

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

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

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

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

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

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

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

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

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

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

Приветствие клиента: пакет четыре Путь приветствия клиента (пакет номер 4, 302 байта: 20 IP, 32 TCP и 250 данных, без заголовка Ethernet): программа узнаёт приветствие и имя www.example.com и снова выбирает профиль один, pcap записывает пакет таким, как его отдало ядро, multisplit проходит оба условия, вычисляет позицию midsld, равную 104, и сама отправляет две новые части (104 и 146 байт данных, пакеты в 156 и 198 байт), а вердикт для исходного пакета «выбросить», поэтому вместо одного пакета в сети два, а в журнале номера идут от 1 до 11 и новых частей среди них нет. пакет № 4: приветствие клиента, путь через профиль 2032данные клиента: 250 байт IPTCPзаголовки IP и TCP: 20 + 32 = 52 байта 302 байта в прошлых выпусках 316 байт: с заголовком Ethernet в 14 байт; программа получает пакет без него профильузнали приветствие,имя www.example.com;выбор снова: профиль 1 pcap (вызов 1)записывает пакеткак отдало ядро:неразрезанный, 302 multisplit (вызов 2)диапазон a: верновид данных: подходитпозиция midsld = 104 вердикт ядрувыбросить оригинал;две части шлётсама программа оригинал выброшен multisplit отправляет две новые части часть 1: 156 байт52данные 104 sending part 1 0-103 len=104rawsend_dissect size=156 часть 2: 198 байт52данные 146 sending part 2 104-249 len=146rawsend_dissect size=198 104 + 146 = 250 байт данных: сервер соберёт запрос целиком части уходят мимо очереди: в журнале номера идут от 1 до 11, новых частей среди них нетвместо одного пакета в сети два: оригинал выброшен, две части отправила сама программа

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

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

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

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

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

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

Одиннадцать пакетов опыта Таблица одиннадцати пакетов, которые увидела программа в опыте: профиль один подходит им всем, pcap записывает все одиннадцать, журнал упоминает multisplit на семи (на пяти пустых клиентских пакетах он не запускается из-за вида данных «пусто», на входящем втором диапазон входящих x, на четырёх входящих пакетах после приветствия вызов молчит, потому что сам отказался от входящих) и запускает его только на приветствии, которое он режет на 104 и 146 байт; в захвате клиента двенадцать пакетов: 11 минус выброшенный оригинал плюс две отправленные части, а файл самописца содержит одиннадцать пакетов с неразрезанным приветствием в 302 байта. одиннадцать пакетов, которые увидела программа (размеры без заголовка Ethernet) №пакетбайтпрофильpcapmultisplitвердикт 1→ SYN60профиль 1записалне звали: вид «пусто»пропустить 2← SYN-ACK60из памятизаписалвне диапазона (x)пропустить 3→ ACK52из памятизаписалне звали: вид «пусто»пропустить 4→ приветствие302выбор повторёнзаписалразрезал: 104 + 146выбросить 5← ACK на часть 152из памятизаписалмолчитпропустить 6← данные сервера66из памятизаписалмолчитпропустить 7← ACK на часть 252из памятизаписалмолчитпропустить 8→ ACK52из памятизаписалне звали: вид «пусто»пропустить 9→ FIN52из памятизаписалне звали: вид «пусто»пропустить 10← FIN52из памятизаписалмолчитпропустить 11→ ACK52из памятизаписалне звали: вид «пусто»пропустить молчит на № 5, 6, 7, 10: на приветствии multisplit сам отказался от входящих пакетовinstance cutoff for 'multisplit_1_2' in=1 out=0 11пакетов записалсамописец (pcap) 7пакетов, где журналупоминает multisplit 1пакет разрезан:приветствие, № 4 на остальных четырёх (№ 5, 6, 7, 10) журнал про multisplit молчит захват клиента (запись на сетевой карте клиента): 12 пакетов 123456789101112 часть 1часть 2104 байта146 байт 11 − 1 + 2 = 12: убрали оригинал, добавили две отправленные части подтверждение 105 (= 1 + 104) относится к части 1, подтверждение 251 (= 105 + 146) к части 2 файл самописца: 11 пакетов, четвёртый 302 байта: приветствие в нём целиком, как его отдало ядро слово строки и след в журнале --filter-tcp=443→ все одиннадцать пакетов подошли профилю один --in-range=a→ перед pcap: самописец видит и входящие --in-range=x→ после него: multisplit входящих не получает --payload=tls_client_hello→ multisplit не вызван на пяти пустых клиентских пакетах pos=midsld→ место разреза: 104

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Три опыта с прогнозом Три опыта с прогнозом до результата: в первом диапазон b0-b249 не пропускает приветствие (байтов данных 250 больше 249), запрос идёт целым в 250 байт и в захвате клиента 10 пакетов, а диапазон b0-b250 пропускает, запрос режется на 104 и 146 байт и в захвате 12 пакетов; во втором добавленное условие --filter-l7=http не подходит соединению tls, на каждом из десяти пакетов журнал пишет «no lua functions in this profile», pcap ни разу не звали и файла нет; в третьем при pos=1 куски 1 и 249, без pos по умолчанию 2 и 248. три опыта: меняем одну цифру или условие, прогноз записан до результата опыт 1. Меняем цифру: диапазон исходящих перед multisplit, два запуска --out-range=b0-b249 прогноз до опытаb на приветствии = 250; 250 > 249:вне диапазона, не сработает: запрос целым журналout pos b250 b250 out of range b0-b249packet: id=4 pass unmodified сервер получил запрос250 байт в захвате клиента 10 пакетовпрогноз сошёлся --out-range=b0-b250 прогноз до опытаb = 250; 250 ≤ 250: в диапазоне,сработает: запрос будет разрезан журналout pos b250 b250 in range b0-b250packet: id=4 drop сервер получил запрос104146 в захвате клиента 12 пакетовпрогноз сошёлся одна цифра изменила результат: 249 не режет, 250 режет опыт 2. Меняем условие профиля, не аргумент: добавлен фильтр протокола --filter-l7=httpстоит правее самописца, но условия профиля действуют, где бы они ни стояли прогноз: протокол соединения tls, профиль не подойдёт никогда, все пакеты достанутся no_action журнал на каждом из десяти пакетов:no lua functions in this profile запрос дошёл целым (250 байт), в захвате клиента 10 пакетовпрогноз сошёлся файла nfqws_seen.pcap нет вообще: pcap ни разу не звали опыт 3, на будущее. Меняем midsld на цифру и убираем аргумент pos pos=1249 байткуски 1 и 249: 1 + 249 = 250 без pos248 байткуски 2 и 248: 2 + 248 = 250; по умолчанию pos равен 2 (lua/zapret-antidpi.lua:497) что ещё принимает pos, расскажет выпуск «Где резать: метки»

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

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

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

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

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

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

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

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

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

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

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

Итог выпуска: чтение строки Итог выпуска: шесть карточек (профили, три вида опций, стратегия как вызовы функций, условия профиля, диапазоны, способ чтения), три вида порядка (вызовы профиля идут все по порядку записи, из повторных аргументов побеждает записанный первым, из подходящих профилей побеждает первый подошедший), опыт на одиннадцати пакетах (самописец записал все, multisplit сработал один раз на приветствии) и вопрос к следующему выпуску про метки в аргументе pos=midsld. итог выпуска: что мы научились читать в командной строке профилипрофиль 1 есть до первой опции;--new начинает чистый лист;в конце пустой no_action три вида опцийобщие, профильные и «ручка»;ручка копируется в вызов правее:порядок слов важен стратегия: вызовы функцийвызовы идут в порядке записи;у каждого своё имя; аргументыприходят текстом условия профиляпорт сервера, протокол, спискиимён и адресов; один фильтртранспорта отключает остальные диапазоныn, d, b: «-» включает верхнююграницу, «<» исключает;d1-d1: пока счётчик равен 1 способ чтенияшесть шагов: служебное, профили,условия, слева направо, вызовы,сверка с журналом порядок встречается в строке трижды вызовы внутри профиляидут в порядке записи, всеpcapmultisplitоба повторный аргументпобеждает записанный первымpos=2pos=midsld→ 2 + 248 подходящие профилипобеждает первый подошедшийпрофиль 1профиль 2→ 2 + 248 опыт на одиннадцати пакетах 1234567891011самописец записал все одиннадцать;разрезальщик сработал один раз, на приветствии синее: pcap записал; зелёное: multisplit разрезал пакет № 4 за кадром остались автоматические списки имён и режим сервера; устройство программы разбирают другие выпуски в нашей строке осталось слово, которое мы только произносили pos=midsldкакие ещё бывают метки, например host и endhost? можно ли резать запрос в нескольких местах сразу? следующий выпуск: «Где резать: метки» дальше пойдут сами приёмы:разрез на частиперестановкаподделка настоящее оборудование на пути пакетов не обсуждалось: только код программы и наша лаборатория

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

См. также


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

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