О чём заметка

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

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

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

Повтор и вопрос: как сказать программе «режь здесь»

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

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

В выпуске «Что видит наблюдатель» мы разобрали по байтам первое сообщение защищённого соединения, приветствие ClientHello. В нашей лаборатории оно занимает двести пятьдесят байт, и имя сайта www.example.com лежит в нём с девяносто седьмого байта по сто одиннадцатый. Здесь и дальше мы считаем с нуля: самый первый байт данных имеет номер ноль.

А в самом начале серии это приветствие разрезали посередине слова example. Вместо одного пакета ушли два: сто четыре байта и сто сорок шесть. Первый кончается буквами «www точка exa», второй начинается с «mple точка com». В нашем тесте разрезанное приветствие прошло мимо игрушечного инспектора.

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

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

Метка вместо числа Строка стратегии multisplit:pos=midsld: слово midsld это метка, слово вместо числа. Программа сама находит для неё номер байта 104 в приветствии на 250 байт (имя www.example.com лежит на байтах 97–111, счёт с нуля), и в нашем тесте приветствие уходит двумя частями: 104 байта (до «www.exa») и 146 байт (от «mple.com»). метка: слово вместо числа --lua-desync=multisplit:pos=midsld имя стратегииаргумент метка приветствие TLS, 250 байт0249 ← имя сайта www.example.com, байты 97–111 программа сама находит номер байта 104 104 байта (0–103)146 байт (104–249) …www.example.com… Мы не писали «104»: мы написали слово, а программа превратила его в номер байта.В нашем тесте приветствие ушло на провод двумя пакетами: 104 и 146 байт.

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

Курьер и два письма Выдуманный пример: курьер режет два письма. Просьба «разрежь на четвёртом слове» подходит только первому письму («Добрый день, дорогая Анна»): во втором письме четвёртое слово «госпожа», разрез не у имени. Просьба «разрежь перед именем получателя» находит имя Анна в обоих письмах. Метка в настройке такая же просьба по смыслу. курьер режет письмавыдуманный пример Просьба 1: «разрежь на четвёртом слове» письмо 1Добрыйдень,дорогаяАнна,пишувам письмо 2Добрыйдень,уважаемаягоспожаАнна,пишувам ножницы у имени ножницы не у имени Просьба 2: «разрежь перед именем получателя» письмо 1Добрыйдень,дорогаяАнна,пишувам письмо 2Добрыйдень,уважаемаягоспожаАнна,пишувам ножницы у имени ножницы у имени Метка в настройке: просьба второго вида, «перед именем», а не «на четвёртом слове».

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

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

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

Позиция это номер байта: счёт с нуля, разрез перед байтом

Начнём с самого простого способа сказать «режь здесь»: числом. Вопрос сцены: что такое позиция и как по ней получаются части?

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

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

Сколько байт в первой части? Столько, какой номер у разреза: четыре. Число байт в первой части равно позиции.

А вторая часть это остаток: размер минус позиция. Восемь минус четыре, тоже четыре.

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

Позиция это номер байта Выдуманный пример: строка «To: Anna» из восьми байт с номерами 0–7. Разрез на позиции 4 ставится перед байтом с номером 4 и делит строку на «To: » и «Anna»: слева 4 байта, то есть столько, какой номер у разреза, справа 8 − 4 = 4 байта. Позиция −4 считается от конца (8 − 4 = 4) и даёт тот же разрез, а −1 это самый последний байт. позиция — номер байта, перед которым разрезвыдуманный пример T0o1:23 A4n5n6a7 режь на позиции 4 слева 4 байта = позиция справа 8 − 4 = 4 байта −8−7−6−5 −4−3−2−1 «минус четыре»: 8 − 4 = 4, тот же разрез, перед именем «минус один»: самый последний байт позицияномер байта, перед которымразрез. Часть до негокончается байтом раньше.Число в настройке:абсолютная позиция. Обычное число в настройке, например pos=4, зависит только от места в данных, а не от их содержимого.

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

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

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

Позиция двести сорок девять даёт два пакета наоборот: двести сорок девять байт и один, последний.

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

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

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

Проверь себя. Позиция минус один на нашем приветствии. Какие два пакета уйдут в сеть?

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

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

А если позицию не написать совсем, multisplit по коду возьмёт два. В нашем тесте без позиции на провод ушли два байта и двести сорок восемь.

Куда ляжет числовой разрез Куда ляжет разрез при числовой позиции на настоящем приветствии в 250 байт (в нашем тесте): pos=1 даёт части 1 и 249 байт, pos=249 даёт 249 и 1, pos=250 не меньше размера и разреза нет, остаётся один пакет в 250 байт; pos=-10 это 250 − 10 = 240 и части 240 и 10, pos=-1 даёт 249 и 1, без pos по умолчанию позиция 2 и части 2 и 248. На запросе HTTP в 81 байт pos=32 даёт 32 и 49 байт, а pos=104 лежит за краем запроса и разреза нет. приветствие TLS, 250 байт: куда ляжет разрез (в нашем тесте) pos=1две части: 1 байт и 249 байт249 pos=249две части: 249 байт и 1 байт249 pos=250не меньше размера: разреза нет, один пакет в 250 байтжурнал: no valid split positions pos=-10250 − 10 = 240: части 240 и 10 байт24010 pos=-1250 − 1 = 249: части 249 и 1 байт249 без posпо умолчанию позиция 2: части 2 и 248 байт248 запрос HTTP, 81 байт, тот же масштаб (в нашем тесте) pos=32перед именем сайта: части 32 и 49 байт (81 − 32 = 49)3249 pos=104104 не меньше 81: за краем запроса, разреза нетзапрос, 81 байт

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

Где в приветствии лежит имя: ходьба по длинам

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

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

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

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

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

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

На семьдесят шестом байте два байта длины списка наборов шифров, там шесть. Семьдесят шесть, плюс два, плюс шесть: восемьдесят четыре.

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

На восемьдесят шестом байте лежат два байта длины всех расширений: сто шестьдесят два. Это первое важное место. Его имя, то есть метка, extlen.

Ходьба по длинам до extlen Как программа идёт по приветствию в 250 байт: заголовки 5 + 4 + 2 + 32 = 43 байта; на байте 43 длина идентификатора сессии 32 даёт прыжок 43 + 1 + 32 = 76; на байтах 76–77 длина наборов шифров 6 даёт 76 + 2 + 6 = 84; на байте 84 длина способов сжатия 1 даёт 84 + 1 + 1 = 86; на байтах 86–87 длина расширений 162, это метка extlen = 86. Сверху выдуманная игрушка: лента, где перед каждым блоком записано, сколько в нём байт. ходьба по длинам игрушка: выдуманный пример 45 читаем число (4), прыгаем через столько байт, читаем следующее (5) приветствие TLS: байты 0…111, счёт с нуля, 8 пикселей на байт заголовки · 43 байтаидентификатор сессии · 32расширения 88…249 поле длины: читаемперепрыгиваемостальные байтыв байтах числа шестнадцатеричные 1на байтах 0–42 лежат заголовки: 5 + 4 + 2 + 32 = 43 2на байте 43 длина идентификатора сессии: байт 20 = 32, прыжок 43 + 1 + 32 = 76 3на байтах 76–77 длина наборов шифров: 00 06 = 6, прыжок 76 + 2 + 6 = 84 4на байте 84 длина способов сжатия: 01 = 1, прыжок 84 + 1 + 1 = 86 5на байтах 86–87 длина всех расширений: 00 a2 = 162, это метка extlen = 86 43+1+32 76+2+6 84+1+1 86extlen = 86 По коду перепрыгиваются три части: идентификатор сессии, наборы шифров и способы сжатия.

Дальше сами расширения, блоки «тип, длина, данные». Программа перепрыгивает блоки по длинам, пока не найдёт тип ноль, «имя сервера». В нашем приветствии он первый, на восемьдесят восьмом байте: два байта типа, два байта длины, а данные начинаются с девяносто второго. Это второе место, метка sniext.

Внутри данных идут три поля: длина списка, восемнадцать; один байт типа имени, ноль; и два байта длины имени, пятнадцать. Имя начинается на девяносто седьмом байте. Это третье место, метка host.

Поля до имени сайта Расширение «имя сервера» в приветствии, байты 86–111, по полям: длина всех расширений 00 a2 = 162 на 86–87 (метка extlen = 86); тип расширения 00 00 = 0 и длина 00 14 = 20 на 88–91; данные расширения начинаются с 92 (метка sniext = 92): длина списка 00 12 = 18, тип имени 00, длина имени 00 0f = 15; имя www.example.com на байтах 97–111 (метка host = 97). В большом приветствии на 1484 байта те же 86, 92 и 97. Если бы идентификатор сессии был пустым, по счёту всё сдвинулось бы на 32 байта: extlen 54, sniext 60, host 65. расширение «имя сервера»: байты 86–111 приветствия 860087a2 extlen = 86 86–87длина всех расширений: 00 a2 = 162метка extlen = 86 8800890090009114 88–89тип расширения: 00 00 = 0, это «имя сервера» 90–91длина расширения: 00 14 = 20, данные идут с байта 92 92009312 sniext = 92 92–93длина списка имён: 00 12 = 18метка sniext = 92 94009500960f 94тип имени: 00 (один байт) 95–96длина имени: 00 0f = 15 9777w9877w9977w1002e.10165e10278x10361a1046dm10570p1066cl10765e1082e.10963c1106fo1116dm host = 97 97–111имя: www.example.com, 15 байтметка host = 97 по счёту: длина расширения 20 = 2 + 1 + 2 + 15, длина списка 18 = 1 + 2 + 15 большое приветствие, 1484 байтате же 86, 92 и 97: до имени всё той же длины если идентификатор сессии пустой (по счёту)всё после него на 32 байта левее:extlen 54, sniext 60, host 97 − 32 = 65

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

Это верно для наших двух приветствий. По общим сведениям, у других клиентов расширения могут идти в другом порядке, и тогда имя окажется дальше: sniext и host сдвинутся, а extlen останется на месте, пока не изменятся поля до него. Вот почему в настройках пишут метку, а не число.

Проверь себя. Допустим, всё то же, но идентификатор сессии пустой: длина ноль вместо тридцати двух. Куда сдвинется начало имени?

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

Итак, программа находит имя ходьбой по длинам и по пути проходит три места: extlen на восемьдесят шестом байте, sniext на девяносто втором и host на девяносто седьмом.

А в запросе HTTP: другая упаковка, другое число, и девять имён

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

Наш запрос, восемьдесят один байт. Первая строка: «GET, косая черта, index точка html, пробел, HTTP, косая черта, один, точка, один». Потом строка «Host, двоеточие, пробел, www точка example точка com», а за ней ещё две строки заголовков, User-Agent и Accept.

Сначала как программа узнаёт запрос. По коду, пакет считается запросом HTTP, если он начинается с одного из девяти слов, заглавными буквами: GET, POST, HEAD, OPTIONS, PUT, DELETE, CONNECT, TRACE или PATCH. Дальше должны идти пробел, адрес и слова «HTTP, косая черта, один, точка».

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

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

Для программы это две разные задачи. В приветствии нужно прыгать по длинам. В запросе нужно найти слово Host с двоеточием: длин там нет, есть строки текста. А результат один и тот же: номер байта, с которого начинается имя.

Одно имя в двух местах Одно и то же имя www.example.com, 15 байт, лежит в разных местах: в приветствии TLS на 250 байт на байтах 97–111, в запросе HTTP на 81 байт на байтах 32–46. Ниже запрос по строкам: GET /index.html HTTP/1.1 на байтах 0–23, слово Host с двоеточием на 26–30, пробел на 31, имя на 32–46, затем строки User-Agent и Accept. В приветствии имя находят ходьбой по длинам (начало на 97), в запросе поиском строки Host (начало на 32): два разбора, один результат, номер байта. одно имя, два места: приветствие TLS и запрос HTTP, один масштаб приветствие TLS, 250 байт имя: 97–111 запрос HTTP, 81 байт имя: 32–46 те же 15 байт, другое место: 97 или 32? запрос HTTP крупнее, по строкам, с номерами байтов GET /index.html HTTP/1.1Host:www.example.comUser-Agent: labAccept: */* 0263247496679 host = 32 конец строки (0d 0a)Host: байты 26–30, пробел 31имя: байты 32–46 в приветствии: ходьба по длинамв байтах записано, сколько пропустить;результат: имя начинается на 97 в запросе: поиск строки Host:длин нет, есть строки текста;результат: имя начинается на 32 Два разных разбора, один результат: номер байта, с которого начинается имя.

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

Вернёмся к игрушке со строкой «То, двоеточие, пробел, Анна». Поставим впереди слово подлиннее: «Hello, двоеточие, пробел, Anna». Теперь имя начинается не на четвёртом байте, а на седьмом. Число изменилось, а просьба «перед именем» осталась верной.

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

Имён у меток девять, список будет на экране. Первое, abs, это просто число, абсолютная позиция, мы её уже видели. Начало имени сайта называется host, а первый байт сразу после имени, endhost.

Ещё три имени про второе справа слово имени сайта: в адресе www точка example точка com это слово example. Его начало называется sld, середина midsld, конец endsld.

Последние три привязаны к типу пакета. Method это начало слова-метода в запросе HTTP, например слова GET. Extlen и sniext вы уже встретили в приветствии.

Девять имён меток Девять имён меток. Верхняя схема на имени www.example.com: host это начало имени, endhost первый байт сразу после имени; sld, midsld и endsld это начало, середина и конец слова example (второго справа слова имени). Три карточки: abs это просто число; sld, midsld, endsld про слово example; method, extlen и sniext зависят от типа пакета. Внизу девять слов-методов запроса HTTP (GET, POST, HEAD, OPTIONS, PUT, DELETE, CONNECT, TRACE, PATCH, заглавными, после слова пробел); «get» маленькими буквами не из девяти, пакет получит ярлык «неизвестно» и для него работают только числа. девять имён меток на имени сайтасписок имён: nfq2/protocol.c:112 www.example.com97111 ← имя сайта, 15 байт hostendhost sldmidsldendsldслово example: второе справа слово имени abs, host, endhostabs: просто числоhost: начало имениendhost: первый байт сразупосле имени sld, midsld, endsldsld: начало слова examplemidsld: его серединаendsld: его конецчисла для них: дальше в серии method, extlen, sniextзависят от типа пакета:method: начало слова GET и т. п.в запросе HTTP;extlen, sniext: в приветствии TLS метод в запросе HTTP: девять слов, заглавными, после слова пробел GETPOSTHEADOPTIONSPUTDELETECONNECTTRACEPATCH getне из девяти Запрос с «get» маленькими буквами получит ярлык «неизвестно»:для такого пакета работают только числа, слова-метки не понимаются (по коду: nfq2/protocol.c:251).

Проверь себя. Запрос начинается не с «GET», а с маленьких букв «get». Признает ли программа такой пакет запросом HTTP?

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

Итак, в приветствии имя находят ходьбой по длинам, в запросе поиском строки Host. Места разные, а метки одни. Метка позиции это слово, которое превращается в номер байта, и таких слов девять.

Метки на приветствии: все флажки на одной ленте

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

Метки на приветствии в 250 байт Приветствие в 250 байт и семь меток на нём: extlen 86 (поле длины расширений, байты 86–87), sniext 92 (первый байт данных расширения, байты 88–91 — заголовок), host 97 (первая буква имени), sld 101, midsld 104, endsld 108 и endhost 112 (первый байт после имени, имя www.example.com занимает 97–111). В нашем тесте разрез на host даёт части 97 и 153 байта (имя целиком во второй), разрез на endhost — 112 и 138 (имя целиком в первой). Приветствие из лаборатории: 250 байт, номера с нуля 0249 00850086a287008800890090149100921293009400950f9677w9777w9877w992e.10065e10178x10261a1036dm10470p1056cl10665e1072e.10863c1096fo1106dm111001120b113 extlen 86 host 97 midsld 104 endhost 112 sniext 92 sld 101 endsld 108 97..111: имя, 15 байт 86..87: поле длины расширений 88..91: заголовок расширения host 97: первая буква имени, w. endhost 112: первый байт сразу после имени. sniext 92: первый байт данных расширения. Само расширение началось раньше, с байта 88. В нашем тесте, pos=host: 97 + 153 байта, имя ушло в начало второй части97 байт153 байта: имя целиком в начале второй части pos=endhost: 112 + 138 байт, имя осталось целиком в первой части112 байт: имя целиком в первой части138 байт

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

Метка endhost, первый байт сразу после имени: сто двенадцатый. Разрез на host оставляет имя целиком во второй части, разрез на endhost оставляет его целиком в первой.

В нашем тесте разрез по host дал два пакета: девяносто семь байт и сто пятьдесят три. Второй пакет начинается с самого имени, «www точка example точка com».

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

Две метки ведут внутрь приветствия. Extlen, поле длины расширений: восемьдесят шестой байт, первый байт двухбайтового поля.

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

Три метки про слово example. Оно занимает байты с сто первого по сто седьмой, семь букв. Sld, начало слова, сто один; midsld, середина, сто четыре; endsld, первый байт после слова, сто восемь. Откуда эти числа, разберём через две сцены, а пока возьмём их из таблицы.

В нашем тесте мы проверили таблицу настоящим запуском. Список из семи имён мы написали в таком порядке: host, sld, midsld, endsld, endhost, sniext, extlen. Программа записала в журнал числа по возрастанию: восемьдесят шесть, девяносто два, девяносто семь, сто один, сто четыре, сто восемь, сто двенадцать. Все семь совпали с таблицей.

Список меток встал по возрастанию В нашем тесте список из семи меток, написанный в порядке host, sld, midsld, endsld, endhost, sniext, extlen, программа записала в журнал числами по возрастанию: 86 92 97 101 104 108 112, и все семь совпали с таблицей. На большом приветствии (1484 байта) тот же список в другом порядке дал те же семь чисел. На проводе приветствие в 250 байт ушло восемью кусками 86, 6, 5, 4, 3, 4, 4 и 138, имя www.example.com разошлось по кускам 4, 3, 4 и 4 байта. В нашем тесте: список меток в том порядке, как мы его написали hostsldmidsldendsldendhostsniextextlen программа переводит метки в числа и сортирует extlen86 sniext92 host97 sld101 midsld104 endsld108 endhost112 приветствие в 250 байтresolved split pos: 86 92 97 101 104 108 112✓ совпало с таблицей большое, 1484 байтаresolved split pos: 86 92 97 101 104 108 112✓ те же числа У большого приветствия другой порядок в списке: sniext, extlen, sld, endsld, host, endhost, midsld На проводе (в нашем тесте): восемь кусков 8665138 86 + 6 + 5 + 4 + 3 + 4 + 4 + 138 = 250 увеличено: имя, байты 97..111 www.4 байтаexa3 байтаmple4 байта.com4 байта

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

Проверь себя. Разрез на метке sniext. Сколько байт в первой части и чем она кончается?

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

Итак, на нашем приветствии: extlen восемьдесят шесть, sniext девяносто два, host девяносто семь, sld сто один, midsld сто четыре, endsld сто восемь, endhost сто двенадцать. Метка method на приветствии не находится.

Метки на запросе HTTP, метка method и две ловушки

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

Метки на запросе HTTP в 81 байт Запрос HTTP в 81 байт и шесть меток: method 0 (начало слова GET), host 32 (первая буква имени), sld 36, midsld 39, endsld 43 и endhost 47 (первый байт конца строки после имени www.example.com, имя занимает 32–46). В нашем тесте список из этих шести меток записан в журнал числами 0 32 36 39 43 47, а на проводе запрос ушёл кусками 32, 4, 3, 4, 4 и 34 байта. Запрос HTTP из лаборатории: 81 байт, номера с нуля GET·/index.html·HTTP/1.1↓Host:·www.example.com↓User-Agent:·lab↓Accept:·*/*↓↓ method 0 :3a30·2031w7732w7733w7734.2e35e6536x7837a6138m6d39p7040l6c41e6542.2e43c6344o6f45m6d460d47↓0a48U5549s7350 host 32 sld 36 midsld 39 endsld 43 endhost 47 32..46: имя, 15 байт 47..48: конец строки¶ = CR (0d), ↓ = LF (0a), · = пробел 0..2: слово GET, method 0 — его начало В нашем тесте, список метокresolved split pos: 0 32 36 39 43 47✓ по возрастанию На проводе (в нашем тесте): шесть кусков, 81 байт GET /index.html HTTP/1.1¶↓Host:·32www.4exa3mple4.com4¶↓User-Agent:·lab¶↓Accept:·*/*¶↓¶↓34 32 + 4 + 3 + 4 + 4 + 34 = 81Метка method 0 разреза не даёт: пустого куска нет

Method, начало слова GET: ноль. Host: тридцать два. Sld, начало слова example: тридцать шесть.

Midsld: тридцать девять. Endsld: сорок три. Endhost: сорок семь.

В нашем тесте настоящий запуск со списком из этих шести меток записал в журнал именно эти числа, по возрастанию.

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

Посмотрим на midsld. В приветствии сто четыре, в запросе тридцать девять. Числа разные, а место одно: и там и там разрез падает между «а» и «эм», «exa» и «mple». Метка находит нужное место в любом из двух пакетов, и поэтому она удобнее числа.

В нашем тесте со списком «host, midsld» запрос HTTP ушёл тремя пакетами. Тридцать два байта: первая строка и «Host, двоеточие, пробел». Потом семь байт «www точка exa» и сорок два байта, начиная с «mple точка com».

Метка method нужна, когда разрез хочется поставить в самом слове-методе. Это начало слова GET, POST или другого из девяти методов, когда пакет уже признан запросом. Method плюс два это разрез после двух букв: в нашем тесте запрос разошёлся на «GE» и ещё семьдесят девять байт.

Имя в запросе на сорок шестом байте кончается, а endhost равен сорока семи: это уже конец строки, возврат каретки, первый байт после имени. Разрез на нём оставляет имя целиком в первой части.

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

Три ловушки меток на запросе HTTP Три ловушки на подготовленных нами запросах (в нашем тесте). Два пробела после Host: метка host встаёт на второй пробел (32), а имя начинается с 33. Порт :8080 или два пробела после имени попадают в «имя», и endhost уезжает за них: 52 и 49 вместо 47. После Host: меньше трёх байт (Host:ww) ни одна метка про имя не находится, а на Host:www находится только host (31). В нашем тесте, на запросах, которые мы подготовили сами Host:··www.example.com↓host 32Ловушка 1: два пробела после Host:host встал на второй пробел, имя начинается с 33. Метка пропускает один пробел, поиск имени все. Host:·www.example.com:8080↓endhost 52Ловушка 2: порт после имениendhost 52 стоит за портом :8080 (без порта было бы 47): порт попал в «имя». Host:·www.example.com··↓endhost 49Ловушка 2: два пробела после имениendhost 49 стоит за пробелами (без них было бы 47): считается до конца строки. Host:wwЛовушка 3: после Host: два байта, без пробелани host, ни endhost, ни sld, ни midsld, ни endsld не находятся. Host:wwwhost 31Ловушка 3: после Host: три байта, без пробелаhost находится (31), остальным нужны точка или конец строки. · = пробел, ¶ = CR, ↓ = LF. Красным отмечены байты, которые сбивают метку.

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

Вторая. Endhost считается до конца строки. Если за именем стоят пробелы или порт, они попадают в «имя», и endhost уезжает за них: на запросе с портом «двоеточие восемь ноль восемь ноль» он стоит после порта.

И третья. Для всех меток про имя в запросе после слова Host с двоеточием должно идти хотя бы три байта, считая пробел. Поиск имени справится и с двумя, а метка нет.

Проверь себя. На нашем запросе написали pos равно endhost. Сколько байт во второй части и чем она начинается?

Тридцать четыре: восемьдесят один минус сорок семь. Вторая часть начинается с конца строки: возврат каретки и перевод строки, потом заголовок User-Agent. В нашем тесте вышло именно так.

Итак, на запросе HTTP: method ноль, host тридцать два, sld тридцать шесть, midsld тридцать девять, endsld сорок три, endhost сорок семь. Midsld даёт то же место, что в приветствии, только число другое. Помните ловушки: host пропускает один пробел, endhost считается до конца строки, а после Host с двоеточием нужны три байта.

Второе справа слово: правило точки и середина слова

Остаётся самая интересная метка, midsld, середина второго справа слова. Начнём с самого слова. Вопрос сцены: как программа его находит?

В имени www точка example точка com слова разделены точками. Справа налево это com, example, www. Второе справа example. В программе его называют sld, от английского second-level domain, «домен второго уровня». По общим сведениям, доменом второго уровня строго называют example точка com целиком, но sld мы называем слово example, как в программе.

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

В программе это делает «поиск слова номер эн справа», в коде FindNLD. Мы называем его просто поиском. Ему дают имя и номер слова справа, у нас два, а он отвечает, где слово начинается и сколько в нём байт.

Как найти второе справа слово Как программа находит второе справа слово в имени www.example.com (15 байт, номера внутри имени с нуля): слова справа налево — com, example, www. Правый указатель стартует за концом имени (15) и шагает 14, 13, 12, 11, пока не встретит точку; левый стартует с 10 и шагает до точки на 3. Слово example начинается на байт правее точки, 4, длина 11 − 4 = 7. Это функция FindNLD из zapret2, номера внутри имени, не в пакете. Имя www.example.com: 15 байт, номера внутри имени с нуля www.example.com0123456789101112131415 за концом com: 1-е справа example: 2-е справа, это sld www: 3-е справа правый левый правый указатель: 15, 14, 13, 12, 11: стоп на точке 11 левый указатель: 10, 9, 8, 7, 6, 5, 4, 3: стоп на точке 3 начало: 3 + 1 = 4длина: 11 − 4 = 7слово example Итог: начало 4, длина 7. Все эти номера внутри имени, не в пакете. Так работает функция FindNLD в zapret2 (nfq2/protocol.c:15-33): ей дают имя и номер слова справа, у нас 2.

Теперь по клеткам. Имя состоит из пятнадцати байт, номера внутри имени с нуля. «www» занимает нулевой, первый и второй байты. Точка стоит на третьем. Слово example идёт с четвёртого по десятый. Вторая точка на одиннадцатом, а com с двенадцатого по четырнадцатый.

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

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

Из слова в номера пакета От слова к номерам в пакете: начало имени в приветствии 97, поиск слова вернул начало 4 и длину 7, поэтому sld = 97 + 4 = 101, endsld = 101 + 7 = 108, midsld = 101 + 7 / 2 = 101 + 3 = 104 (деление нацело, остаток отбрасывается); в запросе HTTP имя с байта 32 даёт 36, 43 и 39. Разрез на 104 ставит «…www.exa» (104 байта) и «mple.com…» (146 байт); в запросе тот же разрез между «exa» и «mple» стоит на 39. Слово из одной буквы — исключение: midsld = начало + 1. Номер внутри имени плюс начало имени в пакете 0f96w7797w7798w7799.2e100e65101x78102a61103m6d104p70105l6c106e65107.2e108c63109o6f110m6d11100112 начало имени 97 sld 101 midsld 104 endsld 108 слово example: байты 101..107 Приветствие: имя с байта 97, поиск слова дал начало 4 и длину 7 sld = 97 + 4 = 101 endsld = 101 + 7 = 108 midsld = 101 + 7 / 2 = 101 + 3 = 104 деление нацело: остаток отбрасывается, 7 / 2 = 3 Запрос HTTP: имя с байта 32, те же начало 4 и длина 7 sld = 32 + 4 = 36 endsld = 36 + 7 = 43 midsld = 36 + 7 / 2 = 36 + 3 = 39 Разрез на midsld в нашем тесте (приветствие, 250 байт): 104 + 146104 байта: …www.exa146 байт: mple.com… приветствие: байты 101..107e101x102a103m104p105l106e107разрез на 104 запрос HTTP: байты 36..42e36x37a38m39p40l41e42разрез на 39 Числа разные, а место одно: между «exa» и «mple» Исключение: у слова из одной буквы midsld = начало + 1 (выдуманный пример: www.x.com с байта 32, 36 + 1 = 37)

Теперь в пакет. К номеру внутри имени прибавляется начало имени. В нашем приветствии оно на девяносто седьмом байте. Метка sld это начало слова: девяносто семь плюс четыре, сто один. Метка endsld это конец слова: сто один плюс семь, сто восемь.

А midsld это начало плюс длина, поделённая нацело на два. Деление нацело значит, что остаток отбрасывается: семь на два это три. Сто один плюс три, сто четыре.

Одно исключение. Если в слове одна буква, программа берёт начало плюс один, чтобы разрез встал после этой буквы.

Для запроса HTTP формулы те же, имя начинается на тридцать втором байте: sld тридцать шесть, endsld сорок три, midsld тридцать девять.

Посмотрим на сами байты приветствия. На сто четвёртом стоит буква «эм», а перед ней буква «а»: слово разрезано как «exa» и «mple». На сто первом байте слово начинается, на сто восьмом стоит точка перед com.

Разрез на сто четвёртом даёт первую часть в сто четыре байта, она кончается на «www точка exa». Вторая часть, сто сорок шесть байт, начинается с «mple точка com». Это те самые сто четыре и сто сорок шесть из выпуска «Письмо в сортировочном центре», и теперь мы знаем, откуда они.

Проверь себя. Имя www точка икс точка com начинается на тридцать втором байте. Чему равны sld, midsld и endsld?

Sld тридцать шесть: имя начинается на тридцать втором, потом «www» и точка. Endsld тридцать семь, слово из одной буквы. И midsld тоже тридцать семь: это то самое исключение, начало плюс один. Это счёт, запуска с таким именем у нас не было.

Итак, поиск находит слово по двум точкам: у нашего имени начало четыре и длина семь. Метки sld, endsld и midsld складывают эти числа с началом имени: для приветствия получается сто один, сто восемь и сто четыре. У слова из одной буквы midsld это начало плюс один.

Правило точки на необычных именах: co.uk, адрес IP, localhost

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

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

Сначала напомним: поиск умеет искать слово любого номера справа. У нашего имени первое справа com, второе example, третье www, а четвёртого нет. Но метки sld, midsld и endsld всегда просят второе.

Второе справа слово у необычных имён Прогон функций (настоящий код поиска на готовых строках, не запуск nfqws2): второе справа слово в www.example.com — example (начало 4, длина 7), в example.com — example (0, 7), в st.vk.com — vk (3, 2), в a.b.co.uk — co (4, 2, списка суффиксов в коде нет), в адресе 93.184.216.34 — число 216 (7, 3), в www.x.com — x (4, 1); у localhost слова не находятся, и метки sld, midsld, endsld пропадают. Красная черта — разрез midsld по формуле кода. Имя сайта и второе справа слово между точками прогон функций: настоящий код поиска на готовых строках, не запуск nfqws2 www.example.comexample: начало 4, длина 7 example.comexample: начало 0, длина 7слева ничего нет, поиск идёт до начала имени st.vk.comvk: начало 3, длина 2 a.b.co.ukco: начало 4, длина 2списка суффиксов вроде co.uk в коде нет 93.184.216.34216: начало 7, длина 3адрес IP: числа между точками тоже слова localhostне нашлось: sld, midsld, endsld не находятсяточек нет, второго справа слова нет www.x.comx: начало 4, длина 1слово из одной буквы: midsld = начало + 1 разрез midsld: начало + длина / 2 нацело (у слова из одной буквы начало + 1), расчёт по формуле кода Определяет ли настоящее оборудование второе слово так же, мы не знаем: описано только правило zapret2.

Имя example точка com, без www. Слово example начинается на нулевом байте имени, длина семь. Слева от него ничего нет, и это не мешает: поиск идёт до начала имени.

Имя st точка vk точка com. Второе справа слово vk, начало три, длина два.

Слово из одной буквы, как «икс» в имени www точка икс точка com, мы уже видели: у него midsld равна началу плюс один.

Имя «а» точка «бэ» точка co точка uk. Списка суффиксов вроде «co точка uk» в программе нет, по коду. Она просто берёт второе справа слово: co.

Адрес IP версии четыре в строке Host: четыре числа между точками. Для программы это тоже слова, и второе справа это третье по счёту число. Если в нём три цифры, midsld встанет после первой цифры. Запроса с таким адресом в нашей лаборатории не было, это прогон функций.

У имени localhost точек нет, и второго справа слова нет. Метки sld, midsld и endsld не находятся, разрез по ним молча пропадёт. Метки host и endhost с таким именем работают.

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

Слово номер N справа Слово любого номера справа в www.example.com: 1-е com, 2-е example, 3-е www, 4-го нет; метки sld, midsld и endsld всегда просят второе. Если после com стоит лишняя точка (www.example.com.), пустое слово после неё считается первым справа, и второе справа становится com. У имени localhost точек нет: в настройке «host, midsld» остаётся одна позиция, host, а midsld молча выпадает (по результатам прогона функций). Прогон функций: слово любого номера справа, лишняя точка и имя без точек Номер слова считается справа налево www.example.com 1: com 2: example 3: www 4: нет метки всегдапросят второесправа Лишняя точка в конце имени: www.example.com. www.example.com. 1: пусто 2: com пустое слово после последнейточки считается первым справа,счёт сдвигается на одно слово Имя без точек: Host: localhost, в настройке «host, midsld» localhost host: находится midsld: не находится Остаётся одна позиция, host: разрез один, перед именем. Midsld молча выпадает. Метки sld, midsld и endsld у такого имени не находятся, метки host и endhost работают. Список «host, midsld» с этим именем целиком не гоняли: метки посчитаны по отдельности, запуска не было.

Проверь себя. Имя «а» точка «бэ» точка co точка uk. Внутри какого слова окажется разрез по midsld и где именно?

Внутри слова co, после его первой буквы. Midsld это начало слова плюс половина длины нацело: длина два, половина один. Это счёт по результату прогона; запуска с таким именем не было.

Проверь себя ещё раз. Строка Host: localhost, а в настройке «host, midsld», без пробела после запятой. Какие позиции останутся?

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

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

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

Метка плюс число, и как программа читает запись

К метке можно прибавить число. Вопрос сцены: что это даёт и как программа читает такую запись?

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

Запись «host плюс один» значит: взять метку host и сдвинуть разрез на один байт вправо. «Endhost минус один» это разрез перед последним байтом имени. Метка, знак, число, без пробелов.

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

Ещё запуск: «host плюс три» и «endhost минус два». Журнал показал сто и сто десять, и на проводе ушли три пакета: сто байт, десять байт «точка, example, точка, цэ» и сто сорок байт.

Метка плюс число: разрез сдвигается на байты Приветствие в увеличенном виде, байты 94–113: метка host стоит на 97, и host+1 сдвигает разрез на 98; метка endhost стоит на 112, и endhost-1 даёт 111, перед последней буквой имени «m». Внизу настоящий запуск с host+3 и endhost-2: журнал «100 110», на проводе три пакета по 100, 10 и 140 байт, в середине «.example.c». приветствие, байты 94–113 крупно (в нашем тесте) 009400950f96w7797w7798w7799.2e100e65101x78102a61103m6d104p70105l6c106e65107.2e108c63109o6f110m6d111001120b113 номер байта host · 97 host+1 · 98host+197 + 1 = 98: разрез на байт правее endhost · 112 endhost-1 · 111endhost-1112 − 1 = 111: перед последней буквой, «m» тот же запрос целиком: pos=host+3,endhost-2 (в нашем тесте) байты 0–99: 100 байт байты 110–249: 140 байт host+3 · 100 endhost-2 · 110 10 байт: «.example.c», байты 100–109 журнал: resolved split pos: 100 110на проводе: три пакета, 100 + 10 + 140 = 250 байт

И «method плюс два» мы уже видели на запросе: два.

Остальные записи тоже запускали в нашем тесте. На приветствии: «host минус один» девяносто шесть, «midsld плюс один» сто пять, «sld плюс два» сто три.

На запросе: «host плюс один» тридцать три, «host минус один» тридцать один, «midsld плюс один» сорок, «sld плюс два» тридцать восемь.

Проверь себя. На запросе HTTP чему равна запись «endhost плюс два» и что стоит в этом месте?

Сорок девять: сорок семь плюс два. Это начало строки User-Agent, потому что два байта до неё занимают возврат каретки и перевод строки. Первая часть кончается вместе со строкой Host. Это счёт.

Теперь как программа читает такую запись. Сначала пробует прочитать всё как число. Получилось: это абсолютная позиция. Не получилось: она берёт слово до первого плюса или минуса, сверяет его с девятью именами и читает остаток как поправку. Нет остатка, поправка ноль.

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

Зато число читается очень снисходительно: цифры, пока они идут, а хвост молча отбрасывается. Цифра пять и буква икс читается как пять. Два, точка, пять читается как два. А ноль, буква икс, один, ноль, шестнадцатеричная запись, читается как ноль: шестнадцатеричных чисел здесь не понимают.

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

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

Как читается запись метки Как программа читает запись метки: сначала пробует число; если не число, берёт слово до первого плюса или минуса, сверяет его с таблицей из девяти имён и читает остаток как поправку. Справа примеры: midsld, method+2, midsld-1 читаются; число читается снисходительно (5x даёт 5, 2.5 даёт 2, 0x10 даёт 0); foo, HOST с большими буквами, пробел после запятой и число 32768 дают отказ. вход: запись из настройкичто получается из разных записей host+1 метка и поправка без пробелов 1вся запись это число? нет 2слово до первого + или − host 3есть в таблице из девяти? да 4остаток читается как поправка +1 метка host, поправка +1адрес: 97 + 1 = 98 нет остатка: поправка 0; регистр важен читается: по коду и прогону функций midsldmidsld, поправка 0 method+2method, поправка +2 midsld-1midsld, поправка −1 host+host, поправка 0 число читается снисходительно 5x5: хвост «x» отброшен 2.52: хвост «.5» отброшен 0x100: запись 0x не понимается отказ: весь список отвергается fooнет в таблице: плохой список меток HOSTрегистр: имени нет в таблице host, midsldпробел после запятой 32768не влезает в int16; −32768 можно в нашем тесте: 5x и 0x10 дали в журнале 5 и 0,ошибки не было; с foo приветствие ушло целым, 250 байт.

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

Списки меток: порядок, повторы, выпавшие метки

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

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

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

Список принимают стратегии multisplit и multidisorder. Остальные стратегии с позициями берут одну метку или диапазон от метки до метки. И все они, по коду, работают только с пакетами TCP.

Размер средней части это разность двух соседних позиций. Возьмём список из четырёх: «два, host, midsld, endhost». Это числа два, девяносто семь, сто четыре и сто двенадцать.

Первая часть два байта, потому что позиция два. Вторая: девяносто семь минус два, девяносто пять.

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

Размеры частей на проводе Размер части равен разности соседних позиций. Для списка 2,host,midsld,endhost на приветствии в 250 байт позиции 2, 97, 104 и 112 дают части 2, 95 (97 минус 2), 7 (104 минус 97), 8 (112 минус 104) и 138 (250 минус 112), вместе 250 байт. Для списка endhost,host,host числа 97 и 112 дают три части 97, 15 и 138, в середине имя www.example.com. приветствие 250 байт: список 2,host,midsld,endhost журнал: resolved split pos: 2 97 104 112в нашем тесте 2байт 0байт 249 97 104 112 часть 12байты 0–1до первой позиции часть 297 − 2 = 95байты 2–96 часть 3104 − 97 = 7байты 97–103 часть 4112 − 104 = 8байты 104–111 часть 5250 − 112 = 138байты 112–249остаток до конца 2 + 95 + 7 + 8 + 138 = 250: ровно размер приветствия список endhost,host,hostчисла 97 112, три части (в нашем тесте) 97 байт138 байт 15 = 112 − 97: имя «www.example.com»

Теперь список с перестановкой и повторами: «midsld, два, midsld, host, два». В нашем тесте в журнале стояли два, девяносто семь и сто четыре: повторы склеились, и на проводе ушли четыре пакета, два байта, девяносто пять, семь и сто сорок шесть.

Прогон функций, ещё три списка. «Sniext, extlen» даёт восемьдесят шесть и девяносто два: extlen идёт первым, хотя написан вторым. «Endhost, host» даёт девяносто семь и сто двенадцать, порядок исправлен. «Host, host» даёт одно число, девяносто семь.

Метки, которые не нашлись, пропадают молча. «Два, пятьсот, минус один» даёт два и двести сорок девять: пятьсот за границей. А «midsld плюс двести» уходит за границу целиком, и не остаётся ничего.

В нашем тесте с «method, midsld» на приветствии метка method выпала, осталась одна midsld, и приветствие ушло двумя пакетами, сто четыре и сто сорок шесть. Ошибки нет.

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

Как программа обрабатывает список меток Список меток читается целиком: каждая метка переводится в номер байта, не нашедшиеся выбрасываются, остальные сортируются по возрастанию, повторы склеиваются. Запись midsld,2,midsld,host,2 даёт 2, 97 и 104 и части 2, 95, 7 и 146. Запись method,midsld теряет method и даёт одну позицию 104. Запись 500,-251,method не находит ни одной позиции, и пакет в 250 байт уходит без изменений. список midsld,2,midsld,host,2метка выпала, и список пуств нашем тесте, приветствие 250 байт записано midsld2midsldhost2 в номера 1042104972 не нашлось ничего не выпало по возрастанию 2297104104 без повторов 297104 на проводе 2957146 сумма частей: 2 + 95 + 7 + 146 = 250 ещё списки: прогон функций sniext,extlen86 92extlen идёт первым endhost,host97 112порядок исправлен host,host97повтор склеен 2,500,-12 249500 за границей method,midsldв нашем тесте записано methodmidsld в номера нет104 после выброса 104 на проводе 104146 ошибки нет 500,-251,methodв нашем тесте записано500-251method в номеранетнетнет журнал: no valid split positionsпакет идёт без изменений: один кусок, 250 байт одна ошибка (2,,3 или 2,bad) ломает весь список

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

Проверь себя. Список «endhost, host, host». Какие числа получатся и сколько частей уйдёт?

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

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

Где метки не работают: чужой тип, QUIC, мало байт, склеенное приветствие

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

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

Это как ключ от чужой двери. Он не подошёл, дверь цела, она просто не открылась.

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

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

Теперь начальный пакет QUIC. По общим сведениям, QUIC это протокол поверх UDP, и в его начальном пакете приветствие зашифровано. В нашей записи такого пакета, тысяча двести байт, слов «www» и «google» нет вообще: имя под шифром.

Программа nfqws2 расшифровывает такой пакет своим кодом, и в нашем тесте она прочитала из него имя www точка google точка com. Но метки по этому имени не работают.

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

Числа считаются по пакету целиком, а не по расшифрованному приветствию. Вывод: переводчик меток на QUIC отдаёт только числа. Но стратегии с позициями работают только с TCP, поэтому резать QUIC по позициям они не будут. Мы проверяли только сам переводчик.

Метки и тип пакета, пакет QUIC Тип нагрузки выбирает правила: для запроса HTTP и приветствия TLS работают слова-метки и числа, для остальных типов (unknown, quic_initial) только числа. Чужая метка не вызывает ошибку, а просто не находится: на приветствии pos=method ушёл целым пакетом в 250 байт. Начальный пакет QUIC в 1200 байт зашифрован, программа читает имя www.google.com сама, но пробная функция нашла на нём только числа 0, 2, 100 и минус один, то есть 1199. тип нагрузки выбирает правила http_reqслова запроса и числа tls_client_helloслова приветствия и числа unknownquic_initial и др.только числа прогон функций: список host,method,2 приветствие, тип http_reqтолько 2 запрос, тип tls_client_helloтолько 2 приветствие, тип unknownтолько 2 приветствие, тип quic_initialтолько 2 тип задан вручную; в программе он берётся по байтам в нашем тесте: pos=method на приветствии один пакет, 250 байтmethod не нашлась журнал: no valid split positions начальный пакет QUIC (в нашем тесте) 1200 байтимя под шифром слов www и google в этих 1200 байтах нет nfqws2 расшифровал сам: имя www.google.com пробная функция: что нашлось hostendhostsldmidsldendsldsniextextlenmethod все слова: не нашлись 0 → 02 → 2100 → 100-1 → 1199 числа считаются по пакету целиком, 1200 байт стратегии с позициями работают только с TCPпо коду: пакет QUIC они резать не будут

Проверь себя. Переводчик меток получил на начальном пакете QUIC список «host, сто». Что он вернёт?

Одно число, сто: host не находится, сто находится. Так вёл себя наш запуск с пробной функцией. Резать по нему всё равно нечем: стратегии с позициями на UDP ничего не делают.

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

Меньше одиннадцати байт приветствие вообще не узнаётся. Ровно на одиннадцати узнаётся, но ни одно слово-метка не находится, находятся только числа.

С восьмидесяти восьми байт находится extlen: ему нужно только поле длины на восемьдесят шестом и восемьдесят седьмом байтах. И так до ста одиннадцати находится только он.

Со ста двенадцати байт добавляются host, sld, midsld, endsld и sniext: расширение с именем кончается на сто одиннадцатом байте и должно прийти целиком. Endhost ещё нет: он равен ста двенадцати, а позиция должна быть меньше размера. Он появляется со ста тринадцати.

Значит, метки появляются тремя ступенями: extlen, потом сразу пять, и последним endhost. «Узнать тип» и «найти имя» это два разных вопроса: одиннадцать байт против ста двенадцати.

Метки на коротком куске приветствия Что находится, если стратегия получила только первые n байт приветствия: до 10 байт тип не узнаётся, на 11 тип узнаётся, но слов-меток нет, на 87 их всё ещё нет, с 88 находится extlen (86), до 111 только он, со 112 добавляются sniext, host, sld, midsld и endsld, endhost появляется со 113. Три ступени: extlen, сразу пять меток, endhost. сколько байт приветствия получила стратегия в нашем тесте: сборка приветствия выключена, стратегия получила первые n байт метки на ленте: extlen 86 · sniext 92 · host 97 · sld 101 · midsld 104 · endsld 108 · endhost 112 10 байт тип не узнаётся 11 байт тип узнаётся, слов-меток нет 87 байт слов-меток нет 88 байт находится extlen 111 байт по-прежнему только extlen 112 байт есть всё, кроме endhost 113 байт есть все слова, и endhost сплошная черта: метка находится; пунктир: ещё нет три ступени extlenс 88 байт пять сразус 112 байт endhostс 113 байт тип и имя это два вопроса: 11 байт против 112при обычных настройках неполное приветствие придерживается, и стратегия его не видит

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

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

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

Три настоящих запуска: слово, список и ловушка

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

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

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

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

Запуск pos=midsld: настройка, журнал, провод Первый настоящий запуск, pos=midsld: метка даёт правило точки, 101 плюс 7 пополам, 104. Журнал: resolved split pos 104. На проводе два пакета: 104 байта с номера 1, кончаются на www.exa, и 146 байт с номера 105, начинаются с mple.com. Сервер собрал те же 250 байт. запуск 1: pos=midsld (в нашем тесте) меткаmidsld правило точки: example, начало 101, длина 7101 + 7 / 2 = 101 + 3 = 104 разрезперед байтом 104 журнал: resolved split pos: 104 байты 0–103: 104 байта байты 104–249: 146 байт 104 левая часть кончается «www.exa»правая начинается «mple.com» на проводе пакет 1: 104 байта, с номера 1 следом пакет 2: 146 байт, с номера 105 (104 + 1) сервер собрал по номерам: 250 байт, равные оригиналу

Проверь себя перед вторым запуском. Список «host, midsld, endhost» на нашем приветствии. Сколько пакетов уйдёт на провод и какого размера?

Три разреза, четыре пакета. Позиции девяносто семь, сто четыре и сто двенадцать. Размеры: девяносто семь, потом сто четыре минус девяносто семь, семь, потом сто двенадцать минус сто четыре, восемь, и остаток, сто тридцать восемь. Это счёт.

Второй запуск, настоящий: все семь имён подряд, host, sld, midsld, endsld, endhost, sniext, extlen. Журнал записал семь чисел по возрастанию, и на проводе были восемь пакетов: восемьдесят шесть байт, шесть, пять, четыре, три, четыре, четыре и сто тридцать восемь. Вместе двести пятьдесят.

Посмотрим на середину. Четыре байта «www точка», три байта «exa», четыре байта «mple» и четыре байта «точка com». Имя сайта разложено по четырём пакетам, и в каждом свой кусочек.

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

Запуск с семью метками: восемь пакетов Второй настоящий запуск, все семь имён подряд: журнал записал 86 92 97 101 104 108 112 по возрастанию, и на проводе восемь пакетов по 86, 6, 5, 4, 3, 4, 4 и 138 байт, вместе 250. Середина разложена по четырём пакетам «www.», «exa», «mple» и «.com», а начало по границам полей: до длины расширений, длина расширений с заголовком имени, длина списка с типом и длиной имени. запуск 2: семь меток подряд (в нашем тесте) записано: hostsldmidsldendsldendhostsniextextlen журнал: resolved split pos: 86 92 97 101 104 108 112по возрастанию extlen86sniext92host97sld101midsld104endsld108endhost112 86 байт: до длины расширений138 байт: остаток семь разрезов, восемь пакетов 6 байтдлина расширенийи заголовок имени 5 байтдлина списка, типи длина имени 4 байтаwww. 3 байтаexa 4 байтаmple 4 байта.com 86 + 6 + 5 + 4 + 3 + 4 + 4 + 138 = 250 имя сайта разложено по четырём пакетам, а каждый разрез попал на границу поля или слова сервер получил 250 байт, равные оригиналу

Третий запуск, ловушка: pos равно ноль. Разрез перед самым первым байтом. Журнал говорит: «позиции разреза: ноль». Позиция есть. Но следующая строка: «нет допустимых позиций», и на провод ушёл один пакет, все двести пятьдесят байт, без изменений.

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

А если рядом есть ещё метка, ноль ничему не мешает. «Ноль, midsld» тоже пишет в журнал «ноль, сто четыре», но отправляет две части, сто четыре и сто сорок шесть, без пустой первой. Так же ведёт себя «host минус девяносто семь»: это тоже ноль.

Урок ловушки: журнал показывает то, что программа прочитала, а не то, что она отправила. Читать нужно вместе с размерами пакетов на проводе.

Ловушка pos=0: журнал и провод Третий настоящий запуск, ловушка pos=0: журнал пишет resolved split pos 0, следом строка no valid split positions, и на провод уходит один пакет в 250 байт без изменений. Журнал печатается раньше, чем программа вычёркивает позицию, с которой разрез невозможен. Рядом с меткой midsld ноль не мешает: pos=0,midsld и pos=host-97,midsld дают в журнале 0 104, а на проводе два пакета по 104 и 146 байт. запуск 3: pos=0 (в нашем тесте) настройкаpos=0 журнал 1: позиция естьresolved split pos: 0 журнал 2: позиция вычеркнутаno valid split positions на проводе один пакет: 250 байт, без изменений разрез перед байтом 0 дал бы пустую первую часть журнал печатается раньше, чем программа вычёркивает позицию, с которой разрез невозможен код: lua/zapret-antidpi.lua:501-502 и lua/zapret-lib.lua:587-597, затем строка на :529 если рядом есть ещё метка, ноль ничему не мешает pos=0,midsldжурнал: 0 104 pos=host-97,midsldжурнал: 0 104 104 байта146 байтдва пакета, пустого нет 104 байта146 байтhost-97 = 97 − 97 = 0: тоже позиция ноль журнал показывает то, что программа прочитала, а не то, что она отправилачитать его нужно вместе с размерами пакетов на проводе

Итак, три запуска. Метка midsld даёт сто четыре и сто сорок шесть. Семь меток дают восемь пакетов, и по ним видны границы полей и слов имени. А число ноль в журнале стоит, но разреза нет.

В коде: переводчик метки и переводчик списка

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

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

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

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

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

Что происходит внутри разборов. Разбор приветствия находит имя ходьбой по длинам, разбор запроса находит строку Host. Дальше оба зовут одну общую функцию, «позицию по имени сайта», в коде HostPos. Host это начало имени плюс поправка, endhost это конец имени плюс поправка. Для sld, midsld и endsld она зовёт поиск второго слова справа, который мы разбирали. Поэтому формулы у приветствия и запроса одни и те же.

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

Переводчик метки: выбор по типу и проверка границ Переводчик метки ResolvePos получает данные пакета, их размер, тип нагрузки и метку, а возвращает номер байта или −1 («не нашлось»). Один выбор по типу: запрос HTTP идёт в HttpPos, приветствие TLS в TLSPos, любой другой тип в AnyProtoPos, которая понимает только числа, и для слова там нет ошибки, только «не нашлось». Для слов HttpPos и TLSPos зовут общую HostPos, а любой путь заканчивается проверкой границ CheckPos: номер не меньше нуля и меньше размера, иначе −1. Отрицательное число сначала прибавляется к размеру: −10 при размере 250 даёт 240. ResolvePos · nfq2/protocol.c:225-236 данные пакета размер данных тип нагрузки метка и число ResolvePos(data, sz, l7payload, marker)результат: номер байта или -1 («не нашлось») switch по типу нагрузки, строки 227-234 http_req → HttpPosстрока Host, метка methodслова и числа tls_client_hello → TLSPosимя ходьбой по длинамслова и числа default → AnyProtoPosтолько числаслово даёт «не нашлось» все остальные типы строка 437строка 716HostPos (198-224): host, endhost, sld, midsld, endsld pos<0 ? sz+pos : posпример: размер 250, метка -10250 + (-10) = 240 CheckPos(sz, offset): offset>=0 && offset<szиначе -1 (строки 182-185) байт 0байт 2490…249: годится, номер остаётся250 и дальше: -1 метка не того типа или за границей молча пропадает: ошибки нет

Второе место: переводчик списка, в коде ResolveMultiPos. Цикл по меткам: каждая идёт через переводчик метки, и результат записывается в массив. Если нашлось, счётчик растёт на единицу. Если нет, следующая метка затирает «не нашлось» на том же месте.

После цикла две строки. Первая сортирует массив по возрастанию. Вторая склеивает повторы.

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

Проследим список «два, host, host, пятьсот» на нашем приветствии. Два находится, host даёт девяносто семь, второй host тоже. Пятьсот за границей и в массив не остаётся. Сортировка и склейка повторов: два и девяносто семь.

А в списке «method, midsld» на приветствии method уходит в функцию для чисел и не находится, остаётся одно число, сто четыре.

Переводчик списка: цикл, сортировка, склейка Переводчик списка ResolveMultiPos: цикл переводит каждую метку через переводчик метки и пишет результат в массив, а счётчик растёт, только если метка нашлась; потом массив сортируется и повторы склеиваются. Список 2,host,host,500 на нашем приветствии в 250 байт даёт 2, 97, 97 и «не нашлось» (500 не меньше 250), после сортировки и склейки остаются 2 и 97, в журнале «resolved split pos: 2 97». Список method,midsld теряет method (на приветствии это не число и не находится) и даёт одно число 104. Метки переводит Си, а стратегии на Lua просят номера через мост resolve_multi_pos и режут пакет сами. ResolveMultiPos · nfq2/protocol.c:237-248список 2,host,host,500 (приветствие) 240for (i=j=0; i<marker_count; i++)242pos[j] = ResolvePos(…, marker+i);243if (pos[j]!=POS_NOT_FOUND) j++;245qsort_ssize_t(pos, j);246j = unique_ssize_t(pos, j);247*pos_count = j; 2 2j=1 host 97j=2 host 97j=3 500 -1j=3 -1 не считается: j не вырос, слот затрёт следующая метка сортировка по возрастанию (строка 245)29797 склейка повторов (строка 246)297 *pos_count = 2 в нашем тесте, журнал стратегии:resolved split pos: 2 97 лента приветствия, 250 байт: что нашлосьбайт 0байт 249 2 host 97 500500 ≥ 250: не нашлось список method,midsld на приветствии method → -1 (не нашлось) midsld → 104остаётся одно число: 104 Lua: скриптовый язык. Стратегии написаны на нём, программа на Си вызывает их функции.Метки переводит Си: Lua только просит номера через мост resolve_multi_pos и режет пакет сама.

Проверь себя. Метка host, а тип нагрузки unknown. Что вернёт переводчик метки?

«Не нашлось». Для этого типа работает только функция для чисел, а host для неё не число. Ошибки нет.

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

В коде: плюс один на мосту в Lua, журнал и части

Остался сдвиг на единицу. Вопрос сцены: почему в журнале «позиции разреза» стоит сто четыре, а внутри стратегии то же место называется сто пять?

Две линейки. Си считает байты с нуля, а Lua, по общим сведениям, с единицы. Один и тот же байт у Си имеет номер сто четыре, у Lua сто пять.

Мост между ними стоит в Си, это функция привязки resolve_multi_pos. Она принимает три вещи: данные, имя типа нагрузки и строку меток. Потом зовёт переводчик списка.

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

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

Мост может выдать ошибку, но из multisplit достижима одна: плохой список меток. Мы видели её с pos равно foo.

Мост в Lua: сдвиг на единицу Две линейки: Си считает байты с нуля, а Lua, по общим сведениям, с единицы, поэтому один и тот же байт «m» в «mple.com» (байт 104 приветствия) у Си имеет номер 104, а у Lua 105. Мост resolve_multi_pos принимает данные, имя типа нагрузки и строку меток, зовёт переводчик списка и к каждому числу прибавляет единицу: список 2 97 104 112 становится списком 3 98 105 113. Из ошибок моста из multisplit достижима одна, «bad marker list» (например, при pos=foo). Зачем единица, в коде не сказано; видимо, чтобы брать части обычной функцией Lua для кусков строки, это предположение. две линейки: байты 100-108 приветствия . e x a m p l e . Си: с нуля100101102103104105106107108 Lua: с единицы101102103104105106107108109 разрез midsld один и тот же байт «m»: у Си номер 104, у Lua номер 105 Lua считает с единицы (по общим сведениям) мост resolve_multi_pos · nfq2/lua.c:3321-3350 данныетри входа, строки 3330-3332 имя типа строка меток ResolveMultiPosстрока 3344 pos[i] + 1строка 3347 таблица Си, из переводчикаLua, из моста 2 +13 97 +198 104 +1105 112 +1113 ошибки моста3342-3343: bad marker listдостижима: pos=foo в нашем тесте3335-3337: bad payload typeиз multisplit недостижима Зачем +1: видимо, чтобы брать части функцией Lua для кусков строки (там счёт с единицы).Это наше предположение: в коде об этом не сказано.

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

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

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

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

Наш список Lua: три, девяносто восемь, сто пять, сто тринадцать. Первая часть от первого байта до второго, два байта. Вторая от третьего до девяносто седьмого, девяносто пять. Третья от девяносто восьмого до сто четвёртого, семь.

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

Крайние значения. Один даёт Lua-позицию два, её не вычёркивают: в нашем тесте с pos равно один на провод ушли пакеты в один байт и в двести сорок девять. Ноль даёт Lua-позицию один, её вычёркивают: «ноль, midsld» остаётся с одним разрезом, сто четыре.

Журнал, вычёркивание единицы и части Что делает multisplit со списком от моста 3 98 105 113 (номера Lua): в режиме отладки печатает журнал, вычитая единицу (2 97 104 112), потом вычёркивает позицию 1, то есть байт 0, и режет данные на части функцией string.sub, которых на одну больше, чем разрезов. Части 1–2, 3–97, 98–104, 105–112 и 113–250 в номерах Lua дают 2, 95, 7, 8 и 138 байт, вместе 250; на проводе в нашем тесте были именно эти пять пакетов. Крайние значения: pos=1 даёт Lua-позицию 2, она остаётся (части 1 и 249); pos=0 даёт Lua-позицию 1, её вычёркивают, список пуст, и пакет в 250 байт уходит без разреза; в списке 0,midsld ноль вычёркивается, а остаётся один разрез: части 104 и 146. стратегия multisplit · lua/zapret-antidpi.lua:500-507 список от моста3 98 105 113 отладка, журнал: минус 1 (501)2 97 104 112 строка 502: убрать 1в нашем списке нет строка 504: цикл5 частей части: string.sub(данные, начало, конец), номера Lua байт 0байт 249 часть 1sub(1, 2)байты 0–1 у Си2 байтапервая: до позиции 3 часть 2sub(3, 97)байты 2–9695 байт часть 3sub(98, 104)байты 97–1037 байт часть 4sub(105, 112)байты 104–1118 байт часть 5sub(113, 250)байты 112–249138 байтпоследняя: до конца данных 2 + 95 + 7 + 8 + 138 = 250: ровно размер данных на проводе в нашем тесте (с какого байта, сколько): (0, 2) (2, 95) (97, 7) (104, 8) (112, 138) крайние значения в нашем тесте pos=1журнал: 1Lua-позиция 2: остаётсячасти 1 и 249 байт pos=0журнал: 0 (до вычёркивания)Lua-позиция 1 вычёркиваетсясписок пуст: разреза нет,пакет в 250 байт без изменений pos=0,midsldжурнал: 0 104ноль вычёркнут, остаётся 105части 104 и 146 байт

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

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

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

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

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

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

Второе: метки. Их девять: abs, host, endhost, sld, midsld, endsld, method, extlen и sniext. Числа лежат на экране в таблице, а запомнить стоит порядок. На приветствии сначала extlen и sniext, потом host, потом три метки слова example и endhost последним. На запросе method первым, потом host, три метки слова и endhost. Midsld на приветствии сто четыре, на запросе тридцать девять.

Шпаргалка: числа меток на двух лентах Шпаргалка чисел на двух лентах. На приветствии в 250 байт метки стоят по возрастанию: extlen 86, sniext 92, host 97, sld 101, midsld 104, endsld 108, endhost 112; имя www.example.com занимает байты 97–111. В запросе HTTP в 81 байт: method 0, host 32, sld 36, midsld 39, endsld 43, endhost 47; имя занимает байты 32–46. На обеих лентах метка midsld режет имя на «www.exa» и «mple.com»: 104 на приветствии и 39 на запросе. приветствие в нашем тесте: 250 байтбайт 0байт 249 extlen 86 sniext 92 host 97 sld 101 midsld 104 endsld 108 endhost 112 имя www.example.com, байты 97–111www.exa|mple.com порядок на приветствии:extlen, sniext, host,sld, midsld, endsld, endhostmidsld = 104 запрос HTTP в нашем тесте: 81 байтбайт 0байт 80 method 0 host 32 sld 36 midsld 39 endsld 43 endhost 47 имя www.example.com, байты 32–46www.exa|mple.com порядок на запросе:method, host, sld, midsld,endsld, endhostmidsld = 39

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

Четвёртое: границы. Метки-слова понимают только приветствие TLS и запрос HTTP, а на QUIC переводчик меток понимает только числа. Если стратегия получила мало данных, метки появляются тремя ступенями: extlen, потом сразу пять, и endhost последним. А в журнале число может стоять и без разреза, как с нулём.

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

И выбор места важен. В нашей игрушке разрез по endhost оставил имя на прежнем месте, а разрез по host передвинул его во второй пакет.

Отсюда практический совет: проверяйте не только журнал, но и размеры пакетов на проводе. Журнал показывает прочитанное, провод показывает отправленное.

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

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

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

Три запуска и что дальше Три настоящих запуска на приветствии в 250 байт. pos=midsld: в журнале 104, на проводе два пакета в 104 и 146 байт, имя разрезано посередине. Семь имён списком (host, sld, midsld, endsld, endhost, sniext, extlen): в журнале 86 92 97 101 104 108 112, на проводе восемь пакетов по 86, 6, 5, 4, 3, 4, 4 и 138 байт. pos=0: в журнале 0, следом «no valid split positions», разреза нет и уходит один пакет в 250 байт: журнал показывает прочитанное, провод показывает отправленное. Следующий выпуск про multisplit: что копируется из исходного пакета, как меняются номера, в каком порядке уходят части и какие промежутки между ними. три запуска в одной строке (лаборатория, приветствие 250 байт) multisplit:pos=midsld resolved split pos: 104 104 байта146 байт два пакета: имя разрезано посередине, «www.exa» и «mple.com» multisplit:pos=host,sld,midsld,endsld,endhost,sniext,extlen resolved split pos: 86 92 97 101 104 108 112 86 байт138 байт восемь пакетов по 86, 6, 5, 4, 3, 4, 4 и 138 байт, вместе 250 multisplit:pos=0 resolved split pos: 0no valid split positions 250 байт: один пакет, без разреза журнал показывает прочитанное, провод показывает отправленное следующий выпуск: multisplit как позиции становятся пакетами на проводе что копируется изисходного пакета № 1№ ?как меняютсяномера частей 1?2порядок и промежуткимежду частями

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

См. также

  • Читаем командную строку zapret2 — предыдущее видео серии
  • multisplit — как работает сама функция разреза
  • payload — распознавание типов протоколов, от которого зависят относительные метки
  • blob — как объявлять и передавать данные

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

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