О чём заметка

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

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

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

Повтор и вопрос: страницы пришли не по порядку

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

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

Повтор про командную строку, выпуск «Читаем командную строку zapret2». Стратегия там записана как вызов Lua-функции: имя, двоеточие и аргументы через двоеточие, например pos равно midsld. Какие пакеты дойдут до вызова, решают условия профиля и лёгкие фильтры; один из них, фильтр вида данных, у нас пускает только клиентское приветствие. Сегодня мы заглянем внутрь одной такой функции: той, что режет приветствие и отправляет куски с конца.

Повтор про метки, выпуск «Где резать: метки». Место разреза это позиция: номер байта, перед которым режут, счёт с нуля. Вместо числа можно написать метку, слово, которое программа сама превращает в номер байта. Наш пример прежний: приветствие ClientHello на двести пятьдесят байт, в нём имя www.example.com с девяносто седьмого байта по сто одиннадцатый. Метка midsld, середина второго справа слова имени, указывает на байт сто четыре. Это граница между «www.exa» и «mple.com».

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

Теперь вопрос. А если отправить сначала конец, потом начало? Дойдёт ли сообщение целым? Что скажет получатель, когда пакет с более поздними байтами придёт раньше пакета с ранними?

Лента, multisplit и вопрос Приветствие в 250 байт с именем www.example.com на байтах с 97 по 111 и меткой midsld на байте 104: multisplit из прошлого выпуска отправляет сначала первые 104 байта, потом остальные 146, и игрушечный вахтёр, которого мы написали сами, не находит разорванное имя, а вопрос этого выпуска в том, что будет, если отправить сначала конец, 146 байт, и только потом начало, 104 байта. повтор: приветствие в 250 байт и метка midsld 0250байты приветствия, смещения с нуля имя www.example.com: байты 97–111 midsld: перед байтом 104 multisplit из прошлого выпуска: куски по порядку 1-й: 104 байта, смещения 0–103 2-й: 146 байт, смещения 104–249 игрушечный вахтёр смотрит в одно место каждого пакета: имя разорвано, не нашёлэто наша модель, а не оборудование а если отправить сначала конец, потом начало? 1-й: 146 байт, смещения 104–249 2-й: 104 байта, смещения 0–103 что скажет получатель, когда пакет с поздними байтами придёт раньше пакета с ранними?

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

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

Письмо из трёх страниц и два писаря Выдуманный пример: письмо из трёх страниц, на каждой в углу номер, почтальон приносит страницы в порядке три, один, два, первый писарь читает их как пришли и записывает «в шесть Встретимся у фонтана», второй сначала раскладывает по номерам и записывает «Встретимся у фонтана в шесть», а какой из них похож на настоящего наблюдателя, мы не знаем. игрушка, выдуманный пример: письмо из трёх страниц 1Встретимся 2у фонтана 3в шесть так написано, номер страницы в углу почтальон принёс страницы в порядке: три, один, два пришла 1-й:3в шесть пришла 2-й:1Встретимся пришла 3-й:2у фонтана какой из писарей похож на настоящего наблюдателя? писарь 1: читает как пришлостраницы в порядке прихода312в шесть Встретимся у фонтаназаписывает на доске:«в шесть Встретимся у фонтана» писарь 2: сначала раскладывает по номерамстраницы по номерам123Встретимся у фонтана в шестьзаписывает на доске:«Встретимся у фонтана в шесть» мы не знаем, какой из них настоящий; о настоящем ТСПУ мы ничего не утверждаемигрушечные читатели обоих видов будут в конце выпуска

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

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

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

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

Итак, пакеты с номерами можно отправить не по порядку, и писари из игрушки читают такую пачку по-разному. Кто из них настоящий, нам неизвестно. Мы смотрим на код и на то, что делает с таким порядком получатель.

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

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

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

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

Две полоски и два счёта Выдуманный пример: лента из 250 клеток разрезана перед клеткой 104 на полоску начала в 104 клетки и полоску конца в 146 клеток, обычный разрез отправил бы начало первым, а наша стратегия отправляет конец первым и начало вторым, и тот же байт имеет смещение 104, если считать с нуля, и номер 105, если считать с единицы. игрушка, выдуманный пример: лента из 250 клеток, режем перед клеткой 104 начало: 104 клетки (0–103)конец: 146 клеток (104–249)0250 разрез перед клеткой 104 обычный разрез (multisplit): начало первымсерверначало · 1041-йконец · 1462-й наша стратегия: конец первымсерверконец · 1461-йначало · 1042-й длинный конец уезжает первым, начало вторым смещение: считаем клетки с нуляметка midsld стоит на смещении 104 номер: считаем клетки с единицытот же байт имеет номер 105 смещение = номер минус один

Теперь настоящий опыт. Стратегия «резать и слать с конца» в коде называется multidisorder. Английское слово disorder значит «беспорядок», часть multi «много кусков», хотя в этом опыте их два. Аргумент pos равен midsld: режем по той же метке, что и в выпуске «multisplit: резать на части». В команде стоит ещё фильтр вида данных, та же настройка вызова, что в выпуске «Читаем командную строку zapret2»: он пускает в функцию только клиентское приветствие TLS.

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

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

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

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

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

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

Журнал и провод первого запуска Опыт с командой multidisorder и pos=midsld на приветствии в 250 байт: в журнале метка midsld стала числом 104, затем ушла часть 2 из 146 байт со смещения 104 и часть 1 из 104 байт с нуля, пакет ядра выброшен, на проводе у клиента первым прошёл пакет с номерами 105–250 и IP id 50136, вторым пакет с номерами 1–104 и IP id 50137, оба с флагами PSH и ACK, а настоящий номер первого байта 3010423859 плюс 104 даёт 3010423963, то есть номер 105, и 104 плюс 146 равно 250 без дыры и перекрытия. журнал программы в нашем опыте: --lua-desync=multidisorder:pos=midsld packet: id=4 len=302пакет ядра: 302 байта с заголовками multidisorder: resolved split pos: 104метка midsld стала числом 104 multidisorder: sending part 2 104-249 len=146 seqovl=0 …часть 2: конец, 146 байт multidisorder: sending part 1 0-103 len=104 seqovl=0 …часть 1: начало, 104 байта packet: id=4 dropпакет ядра выброшен журнал считает части с единицы, код с нуля провод у клиента, уже после программы, в порядке отправки пакетномера байтовдлинафлагиIP id пакет ядра1–250250 байтна проводе нет 1-й: конец105–250146 байтPSH+ACK50136 2-й: начало1–104104 байтаPSH+ACK50137 IP id идёт в порядке отправки, а не байтов: 50136, затем +1 флаги копируются у обработанного пакета: у обоих кусков PSH и ACK номера: настоящие и «от первого байта» настоящий номер первого байта в опыте3010423859 = номер 1+ 104 = 3010423963 = номер 105 ни дыры, ни перекрытия1–104 и 105–250104 + 146 = 250 начальный номер в каждом запуске другой

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

Вторая: флаги пакета копируются с обработанного пакета. У нашего приветствия это PSH, «протолкнуть», и ACK, и оба куска уходят с ними.

Проверь себя, на бумаге, по коду. Метка равна ста: режем по смещению сто, считая с нуля. Какие два пакета уйдут, в каком порядке и с какими номерами?

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

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

Код: ворота, позиции и цикл, идущий с конца

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

Ворота функции мы разбирали в выпуске «multisplit: резать на части», здесь только повтор. Четыре вопроса. Есть ли в пакете TCP? Есть ли данные для разреза: берётся аргумент blob, иначе собранное сообщение (так называется приветствие, склеенное из нескольких пакетов; выпуск «Сборка и повторный проигрыш»), иначе сам пакет? Направление то, что нужно, по умолчанию исходящее? Тип нагрузки известный? Хоть одно «нет», и пакет идёт дальше как был. Для нашего приветствия в одном пакете данные это те же двести пятьдесят байт.

Заодно функция делает отсечку: просит движок больше не звать этот вызов для входящих пакетов соединения. В журнале нашего опыта это видно по строке «in равно один, out равно ноль»: отсечка есть для входящих и нет для исходящих. Как устроены отсечки, объясняет выпуск «Lua внутри программы».

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

Дальше позиции, и тут всё знакомо. Строку pos переводит в числа тот же переводчик, что в выпуске «Где резать: метки». Мост к Lua прибавляет единицу: midsld даёт позицию сто пять, а в журнале стоит сто четыре. Если pos не написан, берётся строка «два». Вычёркиватель единицы, delete_pos_1, убирает позицию «единица», разрез перед самым началом; если список опустел, функция пишет «нет допустимых позиций» и ничего не отправляет.

Ворота и позиции До вызова функции multidisorder программа сама отсеивает пакеты фильтром диапазона и фильтром типа нагрузки, потом четыре ворота самой функции (TCP, есть данные, исходящее направление, известный тип), а дальше строка pos проходит пять шагов в lua/zapret-antidpi.lua: значение по умолчанию 2 (611), переводчик меток превращает midsld в позицию 105 (614), журнал вычитает единицу и пишет 104 (615), вычёркиватель единицы оставляет 105 (616), и непустой список пускает функцию в цикл (617). сначала фильтры программы, ещё до Lua диапазон пакетовin pos x0 x0 out of range x0-x0входящие по умолчанию пусто: пакет идёт как былnfq2/desync.c:1042 тип нагрузки: только клиентское приветствиеpayload_type 'empty' does not satisfy filterпустое подтверждение клиента: стоп до Luanfq2/desync.c:1051, 1090 ворота самой функции, lua/zapret-antidpi.lua:598-609 это TCP?строка 598 есть данные?#data>0, строка 609 направление?по умолчанию out, 609 тип известный?payload_check, 609 хоть одно «нет»: пакет идёт дальше как был instance cutoff for 'multidisorder_1_2' in=1 out=0отсечка (603): входящие выкл., исходящие вкл. от строки pos до списка позиций, на нашем примере midsld значение поумолчаниюarg.pos or "2"строка 611«midsld»не написан: «2» переводчикметокresolve_multi_posстрока 614«midsld» → {105}позиции с единицы строкажурналаzero_based_pos(pos)строка 615{105} → «104»журнал вычитает 1 вычёркивательединицыdelete_pos_1(pos)строка 616{105} → {105}единица не разрез есть липозицииif #pos>0 thenстрока 617дальше: циклпусто: нет разреза мост: переводчик считает с единицы (105), журнал с нуля (104) replay_first (610): для приветствия в одном пакете всегда разрешает позиции в журнале — смещения с нуля

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

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

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

Начало куска это позиция с номером i, а у куска ноль позиции нет, и начало это первый байт. Конец куска на единицу меньше следующей позиции, а у последнего куска это конец данных. Байты вырезает функция Lua string.sub.

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

Цикл с конца Выдуманная линейка с тремя засечками даёт четыре куска, а цикл for i=#pos,0,-1 в lua/zapret-antidpi.lua:559 идёт от последнего куска к первому: для позиции 105 при i равном 1 начало 105, конец 250, это 146 байт со сдвигом номера 104, при i равном 0 начало 1, конец 104, это 104 байта со сдвигом 0, после цикла функция возвращает VERDICT_DROP (585) и пакет ядра на провод не идёт. игрушка, выдуманный пример: линейка с тремя засечками i = 0 (начало)i = 1i = 2i = 3 три засечки дают четыре куска: от i = 0 до i = 3 1 2 3 4 отправляем с последнего отрезка к первому: цикл начинается с i = 3 цикл в общем отправителе частейlua/zapret-antidpi.lua 559for i=#pos,0,-1 do560local pos_start = pos[i] or 1561local pos_end = i<#pos and pos[i+1]-1 or #data562local part = string.sub(data,pos_start,pos_end)581if not rawsend_payload_segmented(desync,part,pos_start-1-ovl) then585return VERDICT_DROP i = 1: «часть 2» в журналеначало 105 = pos[1]; следующей нет, конец 250105…250 = 146 байт, сдвиг 105 − 1 − 0 = 104 i = 0: «часть 1» в журналеpos[0] нет: начало 1; конец 104 = pos[1] − 11…104 = 104 байта, сдвиг 1 − 1 − 0 = 0 после цикла: VERDICT_DROP (585), пакет ядра на 250 байт на провод не идёт если отправка куска не удалась (582): VERDICT_PASS, оригинал уходит следом позиций N, кусков N + 1

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

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

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

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

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

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

Закрепим на опытах: пять кусков и значение по умолчанию.

Проверь себя. Метки: два, host, midsld, endhost: число два, начало имени, середина и конец. Это смещения два, девяносто семь, сто четыре и сто двенадцать, считая с нуля. Сколько получится кусков?

Четыре позиции, пять кусков.

Какого размера самый первый кусок по порядку приветствия и какого самый последний?

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

Уйдут они с конца, как на экране: сто тридцать восемь байт со смещения сто двенадцать, восемь со ста четырёх, семь с девяноста семи, девяносто пять со второго, и последними два с нуля. Всё это совпало с нашим тестом. Кусок на семь байт начинается с «www.exa», кусок на восемь с «mple.com»: имя разорвано, и две части едут в обратном порядке.

Ещё проверка, на бумаге. Метки host и endhost, то есть смещения девяносто семь и сто двенадцать. Какие получатся куски, и чем это плохо для замысла?

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

Пять кусков и порядок отправки Метки 2, host, midsld и endhost дают смещения 2, 97, 104 и 112, то есть четыре разреза и пять кусков по 2, 95, 7, 8 и 138 байт с суммой 250, на проводе куски уходят с конца в порядке 138, 8, 7, 95, 2 с номерами 113–250, 105–112, 98–104, 3–97 и 1–2, кусок на 7 байт начинается с www.exa, кусок на 8 байт с mple.com, а расчёт на бумаге для меток host и endhost даёт куски 97, 15 и 138 байт, где кусок в 15 байт это имя целиком. в нашем опыте: --lua-desync=multidisorder:pos=2,host,midsld,endhost 0250 2 host · 97 midsld · 104 endhost · 112 пять кусков29578138 2 + 95 + 7 + 8 + 138 = 250 провод у клиента: куски уходят с конца 1-й138@112номера 113–250 2-й8@104номера 105–112начинается с mple.com 3-й7@97номера 98–104начинается с www.exa 4-й95@2номера 3–97 5-й2@0номера 1–2 длина@смещение: 8@104 это 8 байт, начиная со смещения 104; имя разорвано, части едут в обратном порядке проверь себя: метки host и endhost (расчёт по формуле цикла, опыта нет) 9715138 кусок в 15 байт это имя www.example.com целиком уйдут в порядке: 138, 15, 97 игрушечный читатель по одному пакету (наша модель) найдёт имя в пакете на 15 байтчтобы разорвать имя для такого читателя, разрез должен попасть внутрь имени, как midsld

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

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

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

Ноль, единица и значение по умолчанию Четыре случая для pos на приветствии и запросе: pos равен 0 и pos равен 999 не режут ничего, в журнале нет допустимых позиций и на проводе один пакет 250@0; pos равен 1,midsld даёт три пакета 146@104, 103@1 и 1@0; без pos подставляется 2, уходят 248@2 и 2@0, имя целиком остаётся внутри длинного куска; а на HTTP-запросе в 81 байт с меткой method+2 уходят 79@2 и 2@0, начало GE. четыре случая на проводе у клиента, длина@смещение в порядке отправки pos=0 и pos=999разреза нетноль: позиция 1, её вычёркивают999: за пределами данных 1-й · 250@0 один пакет 250@0, оригинал прошёл как был журнал: no valid split positions pos=1,midsldтри кускаодин: настоящий разрезпосле одного байта 1-й · 146@104 2-й · 103@1 3-й 146@104, затем 103@1, затем 1@0 pos не написанзначение по умолчанию «2»два куска: 248 и 2имя остаётся целым 1-й · 248@2 2-й 248@2, затем 2@0 имя www.example.com (97–111) целиком внутри 248@2игрушечный читатель по одному пакету (наша модель) его находит HTTP, pos=method+2запрос в 81 байт--payload=http_req, порт 80method+2 = смещение 2 1-й · 79@2 2-й 79@2, затем 2@0 начало «GE»: две первые буквы слова GET

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

Получатель: что отвечает сервер на кусок из будущего

Кусок из будущего пришёл раньше. Что делает сервер? Ответ даёт, по общим сведениям, не zapret2, а сетевой стек сервера. Посмотрим, что он сказал в нашем тесте.

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

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

Вторая фраза называется SACK, «выборочное подтверждение». Это блок «у меня лежат байты от такого-то до такого-то». Блок ничего не подтверждает, он только сообщает, что лежит в стороне.

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

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

Кладовщик, ACK и SACK Выдуманный пример: кладовщик принимает длинное письмо, на стол ложится страница с буквами 105–250, он говорит «жду первую», это ACK, кладёт страницу в отдельную коробку и добавляет «у меня лежат 105–250», это SACK, а когда ложится страница 1–104, коробка пустеет и он говорит «жду 251, всё до 250 есть»; ACK 1 значит жду байт номер один, блок от 105 до 251 значит лежат байты со 105 по 250, правая граница записана на единицу больше. игрушка, выдуманный пример: склад принимает длинное письмо 1. на стол легла страница с буквами 105–250страница 105–250 «Жду первую»ACK «И у меня лежат 105–250»SACK коробка «лежит»:страница 105–250 2. на стол легла страница 1–104страница 1–104 коробка «лежит»:пусто «Жду 251: всё до 250 есть»ACK блока нет: в стороне ничего не лежит два термина ACK 1подтверждение: жду байт номер один,по порядку ничего не получено блок [105, 251)у меня лежат байты от 105 до 250;правая граница записана на единицу больше блок ничего не подтверждает: он только сообщает, что лежит в стороне по общим сведениям, SACK это расширение TCP, и оба конца разрешают его в начале соединения в нашем рукопожатии оба конца его разрешили (опция SACKOK в SYN и SYN-ACK) числа пересчитаны от первого байта: он номер 1

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

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

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

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

Ответы сервера на куски Ответы сервера лаборатории на Linux 6.18 в нашем опыте: на пакет «конец» с байтами 105–250 пришёл ACK 1 и блок от 105 до 251, то есть по порядку ничего нет, а в стороне лежит конец, на пакет «начало» с байтами 1–104 пришёл ACK 251 без блока, затем сервер отправил 14 байт своего ответа; для сравнения целый пакет 250@0 даёт ACK 251, а multisplit по той же метке даёт после первого куска 104@0 ACK 105 без блока; измерен один получатель, у других систем ответы могут отличаться. в нашем опыте: что пришло на сервер и что он ответил, номера от первого байта 1. первым на сервер пришёл пакет «конец»: байты 105–250, 146 байт ответ сервера: ACK 1 и блок [105, 251) по порядку ничего нет, жду первый байт; в стороне лежит конец не полученолежит в стороне: байты 105–250ACK 1: жду отсюда 2. затем пришёл пакет «начало»: байты 1–104, 104 байта ответ сервера: ACK 251, блока нет вместе с лежавшими байтами закрыто всё: жду байт 251 всё до байта 250 получено по порядкуACK 251: жду отсюда следом сервер отправил свои 14 байт ответа для сравнения: два опыта без перемешивания пакет не трогали: 250@0ответ один: ACK 251 multisplit по той же метке: 104@0 первымпосле него ACK 105 и блока нет в обоих ничего из будущего в стороне не лежит измерен один получатель: сервер лаборатории, ядро Linux 6.18у других систем ответы могут отличаться, мы не проверяли

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

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

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

Итак, сервер отвечает на каждый пакет из будущего подтверждением «жду первый» и блоком «у меня лежат такие-то байты». Когда приходит начало, подтверждение одним скачком доходит до конца. Это в нашем тесте, на одном получателе.

Пять кусков: блок растёт влево, приложению хватает одного чтения

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

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

Опыт с пятью кусками, метки два, host, midsld, endhost. Первым ушёл самый последний кусок: сто тридцать восемь байт, со ста тринадцатого по двести пятидесятый. Ответ: ACK один и блок от ста тринадцатого до двести пятидесятого.

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

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

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

Опять один блок, от девяносто восьмого до двести пятидесятого. Семь байт встали вплотную слева.

Четвёртый пакет: девяносто пять байт, с третьего по девяносто седьмой. Блок от третьего до двести пятидесятого. Не хватает двух первых байт. Пятый пакет как раз два байта, с первого по второй. Они закрывают дыру, и приходит ACK двести пятьдесят один без блока.

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

Пять частей: блок растёт влево Опыт multidisorder с пятью частями приветствия в 250 байт (метки 2, host, midsld и endhost): сначала выдуманная игрушка, где пачка страниц 113-250 на складе и страницы 105-112 прилипают к ней слева, и это одна пачка, а не две, потом по шагам части на проводе и ответы сервера: 138 байт со 113-го дают ACK 1 и блок с 113 по 250, 8 байт со 105-го дают блок с 105, 7 байт с 98-го дают блок с 98 (место для проверки себя), 95 байт с 3-го дают блок с 3, а последние 2 байта с 1-го закрывают дыру, и приходит ACK 251 без блока; счётчик ждёт байт номер 1 четыре раза и 251 в конце, это один получатель в нашем тесте. пять частей: на сервере блок растёт влево, пока не придёт начало выдуманный пример склад пачка: страницы 113…250 105…112 на стол кладут: одна пачка, не две ждёт байт номер 1 251 250 байт, ширина = число байт1250 1пакет 113…250, 138 байтответ: ACK 1 блок [113,251) 2пакет 105…112, 8 байтответ: ACK 1112 и 113 рядом: слились блок [105,251) 3пакет 98…104, 7 байтответ: ACK 1проверь себя: какой блок? блок [98,251) 4пакет 3…97, 95 байтответ: ACK 1не хватает байтов 1 и 2 блок [3,251) 5пакет 1…2, 2 байтаответ: ACK 251, блока нет всё подряд с первого байта: пришло и подтверждено четыре ответа «жду первый» с блоком, растущим влево, потом один ACK на весь запросблок [a,b) = байты с a по b−1; всё это измерено на одном получателе, в нашем тесте

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

Блок от второго до двести пятидесятого: сто три байта встали вплотную слева. Первым ответом был блок от ста пятого, а после третьего куска придёт ACK двести пятьдесят один.

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

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

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

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

Второй опыт и что читает приложение Второй опыт с метками 1 и midsld и что прочитало приложение на сервере: три куска приходят с конца, 146 байт со 105-го дают блок с 105, 103 байта со 2-го дают блок со 2, последний 1 байт закрывает дыру и приходит ACK 251; приложение здесь это маленькая принимающая программа лаборатории, она читает всё, что отдало ядро, и записывает число байт и число чтений, а границы чтений протокол по общим сведениям не обещает; в опыте с двумя кусками в обратном порядке приложение прочитало 250 байт за одно чтение, а в опыте с multisplit по порядку двумя чтениями по 104 и 146 байт, и по общим сведениям сетевой стек держит байты из будущего у себя, пока не закроется дыра, это обычное поведение приёмника TCP, другие системы мы не проверяли. второй опыт, потом что видит приложение на сервере проверь себя: метки 1 и midsld, три куска, какой блок после второго? 1кусок 105…250, 146 байтответ: ACK 1 блок [105,251) 2кусок 2…104, 103 байтаответ: ACK 1 проверь себя: какой блок? блок [2,251) 3кусок 1…1, 1 байтответ: ACK 251, блока нет дыра закрыта: ACK 251 приложение: что это слово значит здесьмаленькая принимающая программа на сервере лаборатории; читает всё, что отдало ядрозаписывает число байт и число чтений: одно чтение = все байты разом, два = байты порциямипо общим сведениям: границы чтений протокол не обещает что прочитало приложение на сервере два куска в обратном порядкечтение 1: 250 байтвсего 250 байт, хэш равен multisplit, куски по порядкучтение 1: 104 байтачтение 2: 146 байт почему в обратном порядке чтение одно: стек держит байты из будущего байты 105…250 лежат в стекепока пусто приложению отдано 0 байт: первый байт ещё не пришёл пришло начало 1…104: стек отдаёт все 250 байт разом, это одно чтение по общим сведениям: обычное поведение приёмника TCP; другие системы мы не проверяли

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

Пакет ядра и nodrop: дубликат и двойной отчёт

Что если пакет ядра не выбрасывать? У стратегии есть для этого флаг nodrop, «не выбрасывать».

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

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

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

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

Вердикт пакета ядра и nodrop Последние строки multidisorder (lua/zapret-antidpi.lua:625-629): если отправка частей не удалась, функция возвращает PASS (625-627), строка 628 replay_drop_set нужна для большого приветствия и разобрана позже в выпуске, а строка 629 возвращает PASS при nodrop и DROP без него, то есть без nodrop пакет ядра уничтожается и оригинал на провод не выходит, а с nodrop он выпускается и идёт следом за кусками; куски уходят внутри вызова функции, вердикты функций профиля сводятся в один (nfq2/desync.c:1087), поэтому в нашем опыте оригинал оказывается после кусков; внизу выдуманный пример, где письмо ушло отрывками и приехало ещё целиком, кладовщик отвечает, что эти страницы уже были, писарь, раскладывающий по номерам, копию не берёт, а писарь, клеящий в порядке прихода, получает копию в конце, и на кого из них похож настоящий читатель, мы не знаем. последние строки multidisorder: что будет с пакетом ядраlua/zapret-antidpi.lua 625if multidisorder_send(desync, data, seqovl, pos)==VERDICT_PASS then626  return VERDICT_PASS627end628replay_drop_set(desync)629return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP отправка не удалась:PASS, оригинал идёт как есть записка на потом:для большого приветствия вердикт: nodrop решаетвыбросить оригинал или нет без nodrop: штамп DROPпакет ядра уничтожен,оригинал на провод не выходит с nodrop: штамп PASSпакет ядра выпущен,оригинал едет следом за кусками когда что уходит (время слева направо) 1 · внутри вызова: куски по одномучасть 2, потом часть 1 2 · остальные функции профиля 3 · вердикт пакету ядрауничтожить или выпустить вердикты сводятся в один: nfq2/desync.c:1087 поэтому оригинал после кусков, а не между ними выдуманный примерписьмо ушло отрывками, а потом приехало ещё и целиком отрывок 2 отрывок 1 целое письмо кладовщик: «эти страницы у меня уже были» писарь раскладывает по номерамвидит, что страницы уже есть: копия не страшна1…104105…250без копии писарь клеит в порядке приходаполучает копию в конце текста105…2501…104копия игрушка: на какого из двух писарей похож настоящий читатель, мы не знаем

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

Что ответил сервер. На первый кусок: ACK один и блок от ста пятого до двести пятидесятого. На второй: ACK двести пятьдесят один. А на оригинал пришёл снова ACK двести пятьдесят один, и в нём есть блок.

Проверь себя. Оригинал пришёл последним, после обоих кусков. Какой диапазон назовёт этот блок?

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

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

Так выглядит дубликат, а блок в таком ответе называется «дублирующий SACK». По общим сведениям, он описан в стандарте RFC две тысячи восемьсот восемьдесят три и бывает двух видов: блок лежит ниже подтверждения, или блок лежит внутри другого блока того же ответа. И там и там это байты, которые пришли дважды. Здесь первый вид. В выпуске «multisplit: резать на части» мы называли его дублирующим блоком. Цена nodrop: лишний пакет и отчёт о нём.

То же было у multisplit. Здесь отличие одно: куски стоят в обратном порядке, а оригинал по-прежнему приходит последним.

Ещё проверка на бумаге. Пять кусков и nodrop. Сколько пакетов с данными уйдёт с клиента?

Шесть: пять кусков и оригинал. Так следует из кода, опыта с пятью кусками и nodrop в проекте нет.

nodrop: три пакета и дубликат Опыт с nodrop на нашем приветствии в 250 байт: на проводе клиента три пакета, часть 105-250 в 146 байт с IP id 35120, часть 1-104 в 104 байта с IP id 35121 и оригинал 1-250 в 250 байт, у которого IP id такой же, как у первого куска (lua/zapret-lib.lua:1098-1099), сервер отвечает ACK 1 с блоком с 105 по 250, потом ACK 251 и на оригинал снова ACK 251 с блоком с 1 по 250 ниже подтверждения, то есть дублирующим SACK, потому что все 250 байт пришли дважды; по общим сведениям (RFC 2883) такой блок бывает двух видов, ниже подтверждения или внутри другого блока того же ответа, а цена nodrop это лишний пакет и отчёт о нём; порядок отправки у multisplit часть 1, часть 2, оригинал, а у multidisorder часть 2, часть 1, оригинал, и на бумаге для пяти кусков с nodrop выйдет шесть пакетов с данными, опыта с этим нет, а приложение в нашем тесте получило 250 байт один раз. опыт с nodrop: три пакета на проводе и ответ сервера на каждый 1кусок 105…250, 146 байтIP id 35120 · ACK 1 блок [105,251) 2кусок 1…104, 104 байтаIP id 35121 · ACK 251 всё подряд: ACK 251, блока нет 3оригинал 1…250, 250 байтIP id 35120 · ACK 251 проверь себя: какой диапазон в блоке? блок [1,251) ниже подтверждения у оригинала тот же IP id, что у первого куска: первый отправленный пакет берёт идентификатор оригиналаlua/zapret-lib.lua:1098-1099 у сервера на этот момент байты 1…250: уже все получены второй слой: копия, те же 250 байт пришли дважды ответ сервера в 14 байт идёт после; приложение получило 250 байт один раз дублирующий SACK (по общим сведениям, RFC 2883)блок с байтами, которые уже пришли; бывает двух видов:блок ниже подтверждения (здесь) или внутри другого блока того же ответав выпуске про multisplit это «дублирующий блок»; цена nodrop: лишний пакет и отчёт о нём порядок отправки при nodrop multisplit, по порядкучасть 1часть 2оригинал multidisorder, в обратномчасть 2часть 1оригинал оригиналв обоих последний на бумаге: пять кусков и nodrop, сколько пакетов с данными уйдёт с клиента? = 6: пять кусков и оригинал; по коду, опыта нет

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

Часть, закрывающая дыру, потеряна

Первый кусок приходит последним. А что, если пакет, который закрывает дыру, потерялся по дороге?

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

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

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

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

Потерянный пакет в лаборатории Лабораторная потеря пакета: сначала выдуманный пример, где почтальон потерял первую страницу, кладовщик говорит «жду первую» и ничего не отдаёт читателю, потом схема лаборатории с клиентом, роутером и сервером, на входе роутера стоит правило лаборатории выбросить ОДИН пакет длиной 156 байт, оно придумано для опыта, программа о нём не знает и это не свойство техники; кусок 146 байт доходит до сервера, а кусок с началом в 104 байта пропадает на роутере, и 156 это 104 байта данных, 20 байт заголовка IP и 32 байта заголовка TCP (20 и ещё 12 байт опций); следы такие: у входа роутера пакет виден, у выхода роутера и на сервере его нет, счётчик правила показал один пакет в 156 байт, так мы убеждаемся, что пакет не дошёл, а не просто не записался. потеря начала: правило лаборатории на роутере выдуманный примерпочтальон потерял первую страницу страница 1: потеряна страницы 2…: лежат в коробке кладовщик: «жду первую», читателю ничего лаборатория: клиент, роутер, сервер клиент роутер сервер правило: выбросить ОДИН пакет в 156 байт правило стоит на входе роутера; программа о нём не знает; оно придумано для опыта, это не свойство техники 146 байт: 105…250 дошёл до сервера 104 байта: 1…104 пропал на роутере почему 156 байт 104 данных IP 20 TCP 32 = 156 (в TCP: 20 и ещё 12 опций) где лежат следы у входа роутерапакет виден у выхода роутерапакета нет на серверепакета нет счётчик правила1 пакет, 156 байт так мы убеждаемся, что пакет не дошёл, а не просто не записался

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

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

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

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

Что ответил сервер. На вторую пару пришёл ACK один с двумя одинаковыми блоками: от ста пятого до двести пятидесятого, и второй такой же. Это дублирующий SACK второго вида, из прошлой сцены: блок лежит внутри другого блока, здесь они совпали; эти байты пришли дважды. Потом «начало» дошло, и пришёл ACK двести пятьдесят один. Приложение получило свои двести пятьдесят байт одним чтением, но только после повтора.

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

Потерян кусок с началом: повтор Опыт, где потерян кусок с началом: клиент отправляет 146 байт со 105-го, сервер отвечает ACK 1 и блоком с 105 по 250, потом уходит кусок в 104 байта с 1-го и пропадает на роутере, через около 204 миллисекунд в нашем тесте (первая повторная отправка ядра клиента, это не постоянная величина) снова уходят 146 байт, и повтор проходит через стратегию, потому что это новый исходящий пакет с приветствием, поэтому функция сработала дважды и дважды выбросила пакет ядра; на вторую пару сервер отвечает ACK 1 с двумя одинаковыми блоками с 105 по 250, это дублирующий SACK второго вида, потом начало доходит и приходит ACK 251, и приложение получает 250 байт одним чтением только после повтора; не выяснено, почему ядро отправило снова все 250 байт, а не только дыру. начало потеряно: приложение ждёт повторной отправки клиентсервер 0146 байт: 105…250 ACK 1, блок [105,251) 0,2 мспотерян на роутере104 байта: 1…104 ожидание около 204 мс: первая повторная отправка ядра клиентав нашем тесте; не постоянная величина и не свойство стратегии 203,8 мс146 байт: 105…250, снова ACK 1, блок [105,251) и такой же второй: дублирующий SACK 204,0 мс104 байта: 1…104, снова ACK 251: дыра закрыта проверь себя: пройдёт ли повтор через стратегию? да, повтор проходит через стратегию: это новый исходящий пакет с приветствиемв журнале функция сработала дважды, и дважды пакет ядра выброшен: drop, drop приложение: 250 байт одним чтением, но только после повтора не выяснено: почему ядро отправило снова все 250 байт, а не только дыру

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

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

Что мы знаем: ответы получателя одной таблицей

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

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

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

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

С nodrop в конце приходит ещё и дублирующий SACK, а приложение получает запрос один раз. Если потерялось начало, приложение ждёт повторной отправки, и она снова перемешивается.

Проверь себя по таблице. В какой строке приложение получило запрос позже всех? И в какой читало его двумя чтениями?

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

Ответы получателя одной таблицей Карточка стратегии multidisorder из пяти окошек (ворота, позиции, цикл с конца, вердикт, ответ получателя) и таблица ответов получателя в нашем тесте по шести опытам: пакет без изменений даёт ACK 251 и одно чтение в 250 байт; multisplit по порядку даёт ACK 105 без блока и два чтения по 104 и 146 байт; multidisorder на два куска даёт ACK 1 с блоком с 105 и потом ACK 251, одно чтение; пять кусков дают четыре ACK 1 с растущим блоком и потом ACK 251, одно чтение; с nodrop в конце приходит лишний ACK 251 с блоком ниже подтверждения, это дублирующий SACK первой формы, а приложение получает 250 байт один раз; если потеряно начало, приложение получает 250 байт только после повтора, и в ответах есть дублирующий блок; позже всех запрос получила строка с потерянным началом, а двумя чтениями читала строка с multisplit; внизу границы: измерен один получатель, сервер лаборатории на Linux 6.18, а Windows, BSD и другие системы не проверялись, и о настоящем оборудовании мы ничего не знаем. что мы знаем: карточка стратегии и ответы получателя ворота ворота позиции позиции цикл с конца цикл с конца вердикт вердикт ответ получателя ответ получателя опытчто ответил серверприложение пакет без измененийточка отсчётаACK 251250 байтодно чтение multisplit, по порядкукуски 104 и 146ACK 105, блока нетдва чтения:104 и 146 байт multidisorder, два кускакуски 146 и 104ACK 1 и блок [105,251)потом ACK 251250 байтодно чтение пять кусков138, 8, 7, 95 и 2четыре ACK 1, блок растёт влево:113, 105, 98 и 3; потом ACK 251250 байтодно чтение nodropоригинал идёт следомкак в двух кусках, а в конце лишнийACK 251 с блоком [1,251): дублирующий SACK250 байтодин раз потеряно началождём повторную отправкуACK 1 и блок [105,251); потом ACK 1 с двумяодинаковыми блоками (дубль); потом ACK 251250 байт,но после повтора проверь себя: в какой строке запрос получен позже всех, а в какой его читали двумя чтениями? позже всех: потеряно начало (ждали повтор); двумя чтениями: куски шли по порядку, multisplit измерен один получатель: сервер лабораториина Linux, ядро 6.18; Windows, BSD и другиесистемы не проверялись настоящее оборудование не измерялосьписари из игрушки выдуманы; о настоящем ТСПУмы ничего не знаем

Теперь границы. Всё это измерено на одном получателе, сервере нашей лаборатории на Linux, ядро версии шесть точка восемнадцать. Для Windows, BSD и других систем мы этого не знаем. Определения ACK и SACK взяты по общим сведениям. Настоящее оборудование, которое могло бы стоять на пути, мы вообще не измеряли: наши писари из игрушки выдуманы.

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

И слова. ACK: следующий байт, которого ждёт получатель. SACK: блок «у меня лежат такие-то байты». Дублирующий SACK: блок с уже полученными байтами, раньше мы называли его дублирующим блоком. nodrop: не выбрасывать оригинал. Аргумент pos равен нулю: это не разрез.

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

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

Числа, слова и мост к накладке Числа и слова первой трети выпуска и мост к следующей части: приветствие в 250 байт режется по метке midsld на куски 104 и 146 байт, а пять кусков в порядке отправки имеют длины 138, 8, 7, 95 и 2 байта и вместе дают 250; слова: ACK это следующий байт, которого ждёт получатель, SACK это блок с байтами, которые у получателя уже лежат, дублирующий SACK это блок с уже полученными байтами (раньше мы называли его дублирующим блоком), nodrop значит не выбрасывать оригинал, а аргумент pos равный нулю не разрез; к следующей части три вопроса: что будет, если куски перекрываются, что будет с приветствием из нескольких пакетов и чем старая версия функции отличается от новой; дальше в кусок, который начинается с первого разреза, у нас это конец, можно вписать черновик, то есть несколько лишних байт перед настоящими, они накроют хвост начала, их число задаёт метка левее разреза, а в нашем тесте приложение сервера получило настоящие байты. числа и слова этой части и что дальше числа: приветствие в 250 байт, два куска по метке midsld 104146 уходит вторымуходит первым пять кусков, в порядке отправки: 138, 8, 7, 95, 2 95138 278 138 + 8 + 7 + 95 + 2 = 250 слова ACKследующий байт, которого ждётполучатель SACKблок «у меня лежат такие-тобайты» дублирующий SACKблок с уже полученнымибайтами nodropне выбрасывать оригинал pos=0это не разрез раньше «дублирующий блок»= дублирующий SACK три вопроса к следующей части 1 · что будет, если кускиперекрываются 2 · что будет с приветствием изнескольких пакетов 3 · чем старая версия функцииотличается от новой что дальше: накладка (seqovl) начало, 104 байтаконец, 146 байт черновик перед концом, накрывает хвост начала сколько байт, задаёт метка левее разреза; в нашем тесте приложение сервера получило настоящие байты расстановка черновика на ленте условная, это рисунок идеи; подробности в следующей части

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

Повтор и идея: seqovl в multidisorder это метка, а накладки лежат внутри сообщения

Это середина выпуска «Перемешать: disorder», и её можно смотреть отдельно от остальных. Начнём с короткого повтора.

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

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

Сервер нашей лаборатории, Linux, отвечает на первый пакет: первый байт я всё ещё жду, но эти номера уже держу. Это блок выборочного подтверждения, по-английски SACK. Когда приходит начало, сервер подтверждает всё, и приложение получает все двести пятьдесят байт одним чтением.

Вопрос сцены: что делает у функции multidisorder аргумент seqovl? Пойдём медленно.

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

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

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

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

Повтор и идея: seqovl это метка Повтор и идея накладки в multidisorder: настоящий ТСПУ в этом выпуске не обсуждается, только код, лаборатория и игрушечные модели; повтор показывает приветствие в 250 байт, разрезанное на 104 и 146 байт, куски уходят с конца (сначала 146 со смещения 104, потом 104 с начала), оригинал выбрасывается, сервер отвечает ACK 1 с блоком с 105 по 250, потом ACK 251, и приложение читает все 250 байт одним чтением; затем выдуманный пример с письмом из пронумерованных строк (числа выдуманы): страница 1 это строки 1-10, страница 2 уходит раньше страницы 1, но в её начале приписаны четыре черновые строки с номерами 7-10, как у последних строк страницы 1, и на эти четыре номера теперь два варианта, черновой пришёл первым, настоящий придёт позже, и какому из них поверит читатель, зависит от самого читателя; такие лишние байты называются накладкой, число их задаёт seqovl, имя по-видимому от английского sequence overlap, но в коде расшифровки нет, а в multidisorder seqovl это одна метка, не число и не список, она задаёт только длину накладки, а не место, и накладок столько, на сколько байт метка отстоит от начала сообщения. повтор и идея: накладки лежат внутри сообщения настоящий ТСПУ здесь не обсуждается: код, лаборатория и игрушечные модели повтор части уходят с конца1041461-й: 146 байт со смещения 1042-й: 104 байта с началаоригинал: DROP сервер (Linux) отвечаетACK 1, блок [105,251)«первый байт жду, эти держу»приходит начало: ACK 251SACK = выборочный ACK приложение читает250 байтодно чтение вопрос части: что делает у multidisorder аргумент seqovl? выдуманный примерписьмо на пронумерованных страницах, числа выдуманы страница 1: строки 1…10страница 2: строки 11…201234567891011121314151617181920 страницу 2 шлём раньше страницы 1, в её начало приписаны черновые строки 7…10 7891011121314151617181920 пакет: на номерах 7…10теперь два варианта:черновой, пришёл первымнастоящий, придёт позже какому поверит читатель,зависит от самого читателя накладкаseqovlтак называем эти черновые байты; в выпуске про multisplit их звали и подкладкой: это одно и то же в multidisorder seqovl это одна метка: не число и не списокметка задаёт только длину накладки, а не место: накладок столько, на сколько байт метка от начала по-видимому, от английского sequence overlap («наложение порядковых номеров»); в коде расшифровки нет

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

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

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

Третий шаг, счёт. Число накладок равно Lua-значению метки минус один. Lua-значение это смещение плюс один, потому что в Lua позиции считаются с единицы. Метка на смещении ноль даёт ноль накладок, метка на смещении семь даёт семь.

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

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

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

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

Пять шагов накладки в коде Код блока про накладку в функции multidisorder_send (lua/zapret-antidpi.lua:564-581) по пяти шагам: накладку получает только вторая часть, это часть с индексом 1 (564), метка seqovl должна стоять левее первого разреза, иначе накладка отменяется (565), число накладок равно Lua-значению метки минус один (568), узор по умолчанию один нулевой байт (569) и помощник pattern ставит его перед настоящими байтами части (577), а номер начала пакета равен смещению начала части минус число накладок (581); на выдуманных числах с первым разрезом 10 и меткой 4 накладок 4, пакет начинается на смещении 10 минус 4, то есть 6, и серые клетки 6-9 ложатся на хвост первой части, а проверка на бумаге для разреза 12 и метки 5 даёт 5 накладок и начало пакета на смещении 7, это арифметика по формуле, а не запуск. накладка в multidisorder_send: пять шаговlua/zapret-antidpi.lua 564if i==1 and seqovl and seqovl>0 then565  if seqovl>=pos[1] then568    ovl = seqovl - 1569    local pat="\x00"577    part = pattern(pat,1,ovl)..part581if not rawsend_payload_segmented(desync,part,pos_start-1-ovl) then 1 · условиетолько вторая часть, индекс 1 2 · проверкаметка левее разреза, или отмена 3 · счётнакладок = метка (в Lua) − 1 4 · узорпо умолчанию один нулевой байт 4 · склейкаpattern ставит узор перед частью 5 · адресначало части − число накладок Lua считает с единицы: метка на смещении 7 даёт значение 8, и 8 − 1 = 7 накладок выдуманные числапервый разрез 10, метка 4 левая часть: смещения 0…9вторая часть: 10…1701234567891011121314151617 начало пакета: 10 − 4 = 6 67891011121314151617 пакет:накладок 4: серые клетки 6…9 ложатся на хвост первой части проверь себя: разрез на смещении 12, метка на смещении 5: сколько накладок, где начало? 5 накладок; начало на смещении 12 − 5 = 7; это арифметика по формуле, а не запуск формула: накладок S (смещение метки), начало P − S (P — первый разрез); S строго меньше P

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

Три числа на проводе: seqovl равно единице, host и midsld минус один

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

Исходная картина из начала выпуска: приветствие в двести пятьдесят байт, разрез на сто четвёртом смещении. Слово example разорвано: левая часть кончается буквами «www.exa», вторая начинается с «mple.com». Без накладки уходят сто сорок шесть байт со смещения сто четыре, а следом сто четыре байта с начала.

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

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

Договоримся о счёте. Смещение считается с нуля, а номер байта в потоке с единицы, как в выпуске «Что видит наблюдатель». Значит, смещение сто три это номер сто четыре.

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

Второй опыт: seqovl равно host. Метка host стоит на смещении девяносто семь, это начало имени сайта. Значит, накладок девяносто семь, а пакет длиной двести сорок три байта: девяносто семь плюс сто сорок шесть. Начинается он на смещении сто четыре минус девяносто семь, то есть семь. В нашем тесте первые байты пакета нулевые.

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

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

Три накладки на проводе клиента Накладка на проводе клиента при трёх значениях seqovl на нашем приветствии в 250 байт с разрезом на смещении 104, где слово example разорвано на www.exa и mple.com: при seqovl=1 первый пакет начинается на смещении 103 и несёт 147 байт с головой 00 6D 70 6C 65 2E 63 6F, а сервер потом называет дублирующим блоком один номер, 104; при seqovl=host пакет начинается на смещении 7 и несёт 243 байта, блок с 8 по 104 номер; при seqovl=midsld-1, то есть 103, пакет начинается на смещении 1 и несёт 249 байт, блок со 2 по 104 номер; левая часть в 104 байта каждый раз уходит вторым пакетом, а нулевые байты накладки лежат поверх её хвоста. как накладка выглядит на проводе клиента вопрос: с какого байта начинается пакет с накладкой и сколько в нём байт? www.example.comлевая часть: байты до 103вторая часть: с 10497103104111www.example.comсмещения 97…111 без накладки: два пакета вторым: 104@0первым: 146@1040104250 seqovl=1метка на смещении 1: накладок 1len=147 seqovl=1 накладка: 1 нулевой байтпервым: 147@103 вторым: 104@0 ack 1 S[104,251), затем ack 251 S[104,105) dsack 1 + 146 = 147 байтначало 104 − 1 = 103голова: 00 6D 70 6C 65 2E 63 6F смещение с нуля, номер с единицы: смещение 103 = номер 104, под накладкой лежит буква a seqovl=hostметка на смещении 97: накладок 97len=243 seqovl=97 накладка: 97 нулейпервым: 243@7 вторым: 104@0 ack 1 S[8,251), затем ack 251 S[8,105) dsack 97 + 146 = 243 байтаначало 104 − 97 = 7голова: 00 00 00 00 00 00 00 00 под нулями смещения 7…103, в том числе первые семь букв имени: www.exa seqovl=midsld-1метка на смещении 103: накладок 103len=249 seqovl=103 накладка: 103 нуляпервым: 249@1 вторым: 104@0 ack 1 S[2,251), затем ack 251 S[2,105) dsack 103 + 146 = 249 байтначало 104 − 103 = 1голова: 00 00 00 00 00 00 00 00 103 — наибольшая допустимая метка: она должна быть меньше разреза 104

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

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

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

Проверь себя. seqovl равно midsld минус два, то есть метка на смещении сто два. Сколько накладок, какая длина пакета и с какого смещения он начнётся?

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

Таблица накладок и сумма 104 Три опыта с seqovl 1, host и midsld-1 в одной таблице: накладок 1, 97 и 103, длина пакета 147, 243 и 249 как 146 плюс накладки, начало 103, 7 и 1 как 104 минус накладки, поэтому накладок и начала в сумме всегда 104, то есть разрез; сервер называет дублирующим блок из 1, 97 и 103 номеров, хэш всех полученных байт во всех трёх опытах равен хэшу запроса клиента, и приложение читает все 250 байт одним чтением; для проверки метка 102 даёт по формуле 102 накладки, длину 248 и начало 2, такой опыт мы не запускали. три опыта в одной таблице seqovlнакладокдлина пакетаначало пакетанакладок + началодублирующий блок 111 + 146 = 147104 − 1 = 103[104,105)1 номер host (97)9797 + 146 = 243104 − 97 = 7[8,105)97 номеров midsld-1 (103)103103 + 146 = 249104 − 103 = 1[2,105)103 номера 1 + 103 = 104 97 + 7 = 104 103 + 1 = 104 длина = 146 + накладок, начало = 104 − накладок, значит их сумма = 104 (разрез) метка строго левее разреза: начало не ниже 1, пакет не выходит левее первого байта сообщения хэш = короткий отпечаток данных (sha256)приёмник считает его от всех полученных байт;совпал с хэшем запроса клиента: байты сошлисьвсе до одногозапрос клиентапринято серверомхэшхэшравны приложение сервера в каждом из трёх опытов: одно чтение, 250 байт, хэш равендо приложения дошли настоящие байты, а не нули проверь себя: seqovl = midsld − 2, то есть метка на смещении 102сколько накладок, какая длина пакета и с какого смещения он начнётся? 102 накладки · длина 102 + 146 = 248 · начало 104 − 102 = 2по формуле, не запускали

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

Отмена и неразрешённая метка; какая часть получает накладку

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

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

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

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

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

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

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

Отмена накладки: правило и три опыта Когда функция multidisorder отменяет накладку: метка seqovl должна стоять строго левее первого разреза, то есть на смещении от 1 до 103 при разрезе на 104, а метка на самом разрезе или правее него отменяет накладку; в трёх опытах на нашем приветствии seqovl=midsld даёт в журнале строку об отмене с числами 104 и 104, seqovl=999 не разрешается и тоже отменяется, а seqovl=0 даёт ноль накладок без строки об отмене, и во всех трёх случаях на проводе те же два пакета, 146@104 и 104@0, ответы сервера без дублирующего блока (ack 1 с блоком с 105 по 250, затем ack 251), приложение читает 250 байт с равным хэшем. когда накладка отменяется метка левее разреза: 1…103на разрезе или правее: отмена 0103104: первый разрез2500 — ноль накладок: не ошибка, но и накладки нет999: за концом сообщения, не разрешается метка выдуманный пример: писецчерновые строки пишутся только поверх первой страницы;перед ними должна остаться хотя бы одна настоящая строка;иначе писец откладывает перо и отправляет страницу как есть seqovl=midsldметка 104, разрез 104 seqovl cancelled because seqovl 104 is not less than the first split pos 104 146@104104@0ack 1 S[105,251), ack 251 функция не падает: работает как без seqovl как без seqovl seqovl=999метка не разрешается seqovl cancelled because could not resolve marker '999' 146@104104@0ack 1 S[105,251), ack 251 разрез делается как обычно, те же два пакета как без seqovl seqovl=0метка на смещении 0 строки об отмене в журнале нет 146@104104@0ack 1 S[105,251), ack 251 ноль накладок: pattern(pat,1,0) возвращает пустую строку как без seqovl во всех трёх опытах: приложение прочитало 250 байт, хэш равен, дублирующего блока нет

Теперь про разрезы. Возьмём три позиции разреза: host, midsld и endhost, а seqovl пусть будет host минус один, то есть девяносто шесть. Три разреза дают четыре части. Левая, девяносто семь байт с начала. Вторая, семь байт со смещения девяносто семь, это «www.exa».

Третья часть, восемь байт со смещения сто четыре. Последняя, сто тридцать восемь байт со смещения сто двенадцать.

Накладку получает часть, которая начинается с первого разреза, то есть вторая. Девяносто шесть меньше девяносто семи, проверка проходит, накладок девяносто шесть. Длина пакета девяносто шесть плюс семь, сто три, а начало на девяносто седьмом смещении минус девяносто шесть, то есть на смещении один.

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

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

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

Проверь себя. Позиция разреза host, то есть смещение девяносто семь, а seqovl равно host плюс один. Что будет?

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

Какая часть получает накладку Какая часть получает накладку при нескольких разрезах: на опыте pos=host,midsld,endhost с seqovl=host-1, то есть меткой 96, приветствие в 250 байт режется на четыре части, левую в 97 байт, вторую в 7 байт (www.exa), третью в 8 и последнюю в 138, и накладку получает вторая часть, которая начинается с первого разреза, а не какая-то часть с именем: на проводе идут 138@112, 8@104, 103@1 (96 нулей плюс 7 настоящих байт, начало 97 минус 96 равно 1) и 97@0, а сервер отвечает блоками с 113, с 105 и со 2 по 250 номер и в конце ack 251 с дублирующим блоком со 2 по 97 номер, 96 номеров; если первый разрез стоит на смещении 2, то та же метка 96 не меньше разреза, накладка отменяется, и на проводе обычные пять частей 138@112, 8@104, 7@97, 95@2 и 2@0; для проверки pos=host с seqovl=host+1 даёт метку 98 при разрезе 97, то есть отмену со строкой в журнале про 98 и 97, по коду, такой опыт не запускали. какая часть получает накладку pos=host,midsld,endhost и seqovl=host-1 (метка 96): три разреза, четыре части левая: 0…96, 97 байтпоследняя: 112…249, 138 байт вторая: 97…103, 7 байт (www.exa)третья: 104…111, 8 байт в журнале номера частей с единицы: part 2 = вторая часть на провод, с конца 1: 138@112 2: 8@104 накладка: 96 нулей3: 103@1 = 96 + 7 байт, начало 97 − 96 = 1 4: 97@0 ответы сервера по порядку ack 1 S[113,251) ack 1 S[105,251) ack 1 S[2,251) ack 251 S[2,98) dsack блок [2,98): 96 номеров, ровно столько, сколько накладокприложение: 250 байт, хэш равен накладка достаётся не части с именем, а части, которая начинается с первого разреза первый разрез рядом с началом: pos=2,host,midsld,endhost, та же метка 96 5432195@2138@112 провод: 138@112, 8@104, 7@97, 95@2, 2@0ack 251 без дублирующего блока seqovl cancelled because seqovl 96 is not less than the first split pos 2 метка 96 не левее разреза 2: накладка отменена, части идут как обычно проверь себя: pos=host (разрез на смещении 97), seqovl=host+1. Что будет?метка на смещении 98, разрез на 97 98 не меньше 97: накладка отменена, в журнале строка с числами 98 и 97по коду, не запускали

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

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

Вопрос сцены: чем заполнена накладка, и что будет, если узор не найден?

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

По умолчанию узор это один нулевой байт, поэтому накладка состоит из нулей. Это мы видели на проводе: у пакета с seqovl равным host все байты головы нулевые.

Узор можно заменить аргументом seqovl_pattern. Он принимает имя блоба, то есть заранее заданных байтов, или строку в шестнадцатеричной записи с приставкой «ноль икс». В нашем тесте узор это четырнадцать байтов, которые читаются как «www.decoy.test», то есть выдуманный текст-приманка.

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

На проводе пакет начинается словами «www.decoy.testwww.decoy.testwww» и так далее. Это двести сорок три байта со смещения семь. Приложение сервера получило двести пятьдесят байт, хэш равен оригиналу. Приманка до приложения не дошла.

Чем заполнена накладка Чем заполнена накладка: выдуманный штамп из пяти знаков 12345 повторяется и обрезается до восьми клеток, получается 12345123, так же работает помощник pattern из lua/zapret-lib.lua (строки 567-574); в multidisorder_send узор по умолчанию один нулевой байт (строка 569 в lua/zapret-antidpi.lua), при optional и отсутствии блоба берётся нулевой узор, иначе блоб (строка 574), а строка 577 ставит узор перед частью; в опыте с seqovl=host и seqovl_pattern из 14 байт www.decoy.test нужно заполнить 97 байт, 97 равно 6 по 14 плюс 13, голова пакета 77 77 77 2E 64 65 63 6F и дальше, то есть текст www.decoy.testwww., пакет 243@7, сервер подтверждает блоки с 8 по 250 и с 8 по 104, приложение получило 250 байт с равным хэшем, и приманка до приложения не дошла. чем заполнена накладка выдуманный примерштамп из пяти знаковзаполнить 8 клеток1234512345штамп дваждыобрезатьpattern(штамп, 1, 8)= 12345123 lua/zapret-antidpi.lua, multidisorder_send: откуда берутся байты накладки 569local pat="\x00"по умолчанию узор один нулевой байт, накладка из нулей 571if desync.arg.optional and not blob_exist(desync, desync.arg.seqovl_pattern) thenoptional и такого блоба нет: остаются нули, в журнал идёт строка 574pat = blob(desync,desync.arg.seqovl_pattern)иначе узор берётся из блоба: имя или строка 0x… в шестнадцатеричной записи 577part = pattern(pat,1,ovl)..partpattern размножает узор и обрезает до ovl байт; результат ставится перед частью pattern: lua/zapret-lib.lua:567-574, в комментарии кода пример pat="12345" len=10 offset=4 даёт "4512345123" seqovl=host, seqovl_pattern: 14 байт в записи 0x…, читаются как www.decoy.testнужно заполнить 97 байт накладки узором в 14 байт www.decoy.testwww.decoy.testwww.decoy.testwww.decoy.testwww.decoy.testwww.decoy.testwww.decoy.tes 6 × 14 = 84+ 13 97 = 6 × 14 + 13: шесть полных повторов и 13 знаков с начала седьмого на проводе клиента пакет 243@7: 97 байт приманки, затем 146 настоящих77 77 77 2E 64 65 63 6F 79 2E 74 65 73 74 77 77 77 2E …голова пакета читается как www.decoy.testwww. сервер:ack 1 S[8,251)ack 251 S[8,105) dsack приложение: 250 байт, хэш равен оригиналуприманка до приложения не дошла

Теперь отказ. Пусть узор назван именем, которого нет: блоб с выдуманным именем. Первый случай без аргумента optional, то есть «необязательно»: с ним, если блоба нет, берётся нулевой узор. Берём три разреза, как в прошлой сцене, и seqovl равно host минус один.

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

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

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

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

Проверь себя. Один разрез midsld, seqovl равно host, узор seqovl_pattern назван, но блоба с таким именем нет, optional не задан. Успеет ли хоть одна часть уйти на провод до ошибки?

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

Для сравнения: у аргумента blob, который заменяет данные для разреза, отказ похожий. Без optional в нашем тесте ошибка, с optional функция пропускает работу. В обоих случаях оригинал уходит целым, ничего не разрезано.

Узор не найден посреди цикла Что будет, если seqovl_pattern назван именем блоба, которого нет: на разрезах host,midsld,endhost с seqovl=host-1 без optional цикл идёт с конца, часть 4 (138@112) и часть 3 (8@104) уходят, а на второй части, где ищется узор, функция blob вызывает ошибку (строка 554 в lua/zapret-lib.lua), движок пишет, что пропускает пакет без изменений, и на провод идёт оригинал целиком 250@0, сервер получает две части, затем оригинал и называет дублирующим диапазон с 105 по 250 номер, а приложение всё равно читает целое приветствие; с optional берётся нулевой узор и уходят обычные четыре части 138@112, 8@104, 103@1 и 97@0; для проверки при одном разрезе midsld и seqovl=host без optional ошибка случится до первой отправки и уйдёт только оригинал, по коду, такой опыт не запускали; у аргумента blob отказ похожий: без optional ошибка, с optional строка skipped, и в обоих случаях оригинал уходит целым. узор не найден: отказ посреди цикла без optionalpos=host,midsld,endhost seqovl=host-1 seqovl_pattern=nosuchblob цикл идёт с конца; накладка нужна только второй части, и узор ищется именно там часть 4: 138@112ушла на провод часть 3: 8@104ушла на провод часть 2: нужен узорблоб недоступен часть 1: 97@0до неё не дошло LUA ERROR: ...zapret-lib.lua:554: blob 'nosuchblob' unavailabledesync ERROR. passing packet unmodified. движок после ошибки отпускает оригинал: nfq2/desync.c:1143 1: 138@112 2: 8@104 3: оригинал 250@0, целиком ack 1 S[113,251) ack 1 S[105,251) ack 251 S[105,251) dsack уже отправленное не вернуть; в нашем тесте приложение всё равно получило целое приветствие, 250 байт с optionalblob 'nosuchblob' not found. using zero pattern часть 4: 138@112 часть 3: 8@104 часть 2: 103@1 часть 1: 97@0 узор из нулей, накладка 96 байт в третьем пакете; в конце ack 251 S[2,98) dsack проверь себя: один разрез midsld, seqovl=host, узор назван, блоба нет, optional не задануспеет ли хоть одна часть уйти на провод до ошибки? нет: цикл начинается со второй части, узор ищется сразу; уйдёт только оригиналпо коду, не запускали для сравнения: аргумент blob=nosuchblob у самой multidisorderбез optional: ошибка; с optional: строка skipped, функция ничего не делаетв обоих случаях оригинал уходит целым, ничего не разрезано

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

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

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

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

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

Руководство к программе говорит: когда позднее перекрытие приходит, оно записывается поверх того, что уже лежит в буфере; так ведут себя все системы, кроме Windows, где техника не работает. Это слова руководства, а не наш опыт: мы проверили только Linux.

Приёмник и перекрытые байты Что сервер нашей лаборатории на Linux делает с перекрытыми байтами: в пяти опытах итоговое подтверждение несёт дублирующий блок SACK ровно на перекрытом диапазоне номеров, от 104 до 105 не включая при seqovl=1 (1 номер), от 8 до 105 при seqovl=host (97 номеров), от 2 до 105 при seqovl=midsld-1 (103 номера), от 8 до 105 при узоре-приманке (97 номеров) и от 2 до 98 при трёх разрезах (96 номеров), приложение во всех пяти получило настоящие 250 байт с равным хэшем, а какую из двух копий ядро хранит внутри, из захвата не видно; руководство (docs/manual.en.md, строка 4078) говорит, что позднее перекрытие записывается поверх лежащего в буфере и так ведут себя все системы, кроме Windows, но это слова руководства, а мы проверяли только Linux. что делает приёмник с перекрытыми байтами итоговое подтверждение сервера: дублирующий блок на ряду номеров 1…250 seqovl=1блок [104,105), 1 номер← один номер: 104 seqovl=hostблок [8,105), 97 номеров seqovl=midsld-1блок [2,105), 103 номера host + приманкаблок [8,105), 97 номеров три разреза, host-1блок [2,98), 96 номеров 1051250 приложение во всех пяти опытах: настоящие байты, 250 байт, хэш равенсервер называет дублирующим ровно перекрытый диапазон какую из двух копий ядро хранит внутри, из захвата не видно, и мы этого не утверждаемпо общим сведениям, форма дублирующего блока описана в RFC 2883 руководство к программе, docs/manual.en.md:4078«позднее перекрытие записывается поверх того, что уже лежит в буфере;так ведут себя все системы, кроме Windows»это слова руководства, а не наш опыт: проверен только Linux, Windows и другие системы мы не проверяли

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

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

Возьмём опыт с seqovl равным host. Сначала приходит пакет из нулей и настоящих байтов, с седьмого смещения. Нули ложатся на места с седьмого по сто третье, настоящие байты второй части на места со сто четвёртого. Первые семь байт имени, «www.exa», оказались под нулями. Затем приходит левая часть с настоящими байтами с нулевого по сто третий.

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

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

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

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

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

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

Два игрушечных читателя Два выдуманных читателя пересобирают поток по номерам байтов: «первый записавший побеждает» не трогает занятое место, «последний записавший побеждает» кладёт новое поверх старого, и накладка из нулей обманывает только первого; в опыте с seqovl=host сначала приходит пакет 243@7 (нули на местах 7…103, настоящие байты с 104), затем левая часть 104@0: первый читатель оставляет нули, имя www.example.com остаётся закрытым нулями с места 97 по 103 и он слеп, а последний записывает настоящие байты поверх нулей и видит имя целиком; в таблице опытов первый слеп при seqovl=host, midsld-1, с приманкой www.decoy.test и при разборе захвата с seqovl=1, а при обычном беспорядке и при трёх разрезах с накладкой 1…96 видит, последний видит везде; во втором nfqws2 в роутере читатель «первый» пропустил всё и сервер получил 250 байт, читатель «последний» нашёл имя, выбросил левую часть и все следующие данные клиента, и сервер получил 0; для проверки при трёх разрезах и seqovl=host-1 читатель «первый» имя увидит, потому что нули лежат левее имени. два игрушечных читателя: кто верит первой копии, а кто последней выдуманные модели читателей: к настоящему оборудованию отношения не имеют первый кладовщикувидел занятое место — больше не трогает«первый записавший побеждает» второй кладовщиккладёт новую страницу поверх старой«последний записавший побеждает» накладка из черновиков обманывает только первоговидит = поиск находит www.example.com в собранном потоке; слеп = не находит 1. приходит пакет 243@7: нули на местах 7…103, настоящие байты с 104 2. приходит левая часть 104@0: настоящие байты на местах 0…103 первый записавший побеждает нулинастоящие байты второй части имя: 97…111 места 7…103 заняты нулями, настоящие байты он игнорирует: имя закрыто нулями с 97 по 103слеп: имя не собралось последний записавший побеждает настоящие байты второй части настоящие байты имя: 97…111 настоящие байты записаны поверх нулей: имя собрано целикомвидит: имя найдено результаты в наших опытах опытпервыйпоследний обычный беспорядок, без накладкивидитвидит seqovl=hostслепвидит seqovl=midsld-1слепвидит seqovl=host с приманкой www.decoy.testслеп (видит приманку)видит seqovl=1, разбор готового захватаслепвидит три разреза, seqovl=host-1: накладка 1…96, имя с 97видитвидит ещё одна наша игрушка: второй nfqws2 в роутере на пути к серверу клиентроутер: второй nfqws2читатель-игрушкасервер «первый»: сервер получил 250«последний»: сервер получил 0 второй nfqws2 — второй экземпляр той же программы:ничего не отправляет и не меняет, только решает: выбросить пакет или пропуститьчитатель ищет подстроку, без TLS и таймеров;нашёл имя — выбрасывает этот пакет и все следующие данные клиента «последний» нашёл имя на втором пакете и выбросил левую часть 104@0;без начала приложение ничего не получило проверь себя: три разреза, seqovl=host-1 (накладка 1…96, имя с 97)увидит ли имя читатель «первый записавший побеждает»? увидит: нули лежат левее имени, а само имя пришло настоящими байтамипо разбору захвата

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

Проверь себя: одно слово seqovl в трёх стратегиях, три геометрии; что мы знаем о накладке

Вопрос сцены: слово seqovl встречается у нескольких стратегий. Значит ли оно везде одно и то же?

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

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

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

Третья строка: multidisorder. Здесь seqovl метка, а пакет с накладкой начинается правее первого байта, на смещении семь для метки host. Разрез минус метка: сто четыре минус девяносто семь. Накладка лежит внутри сообщения, на хвосте левой части.

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

Проверь себя. Seqovl равно пяти, разрез на сто четвёртом смещении. Где начнётся пакет с накладкой у multisplit и где у multidisorder?

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

Одно слово, три геометрии Одно слово seqovl в трёх стратегиях и три геометрии на нашем приветствии в 250 байт: у multisplit с pos=midsld и seqovl=5 это число, первый пакет 109@-5 начинается на пять байт левее первого байта сообщения и сервер называет дублирующим блок из пяти номеров левее первого; у tcpseg с pos=0,-1 и seqovl=1 (оригинал выбросила отдельная стратегия drop) это тоже число, один пакет 251@-1 начинается на байт левее первого, дублирующий блок из номера 0; у multidisorder с pos=midsld и seqovl=host это метка, пакет 243@7 начинается внутри сообщения, 104 минус 97 равно 7, блок со 8 по 104 номер; для проверки при seqovl=5 и разрезе 104 у multisplit пакет в 109 байт начинается на пять байт левее первого (опыт запускали), а у multidisorder по формуле 5 накладок, длина 151 и начало на смещении 99, такой опыт не запускали. одно слово seqovl: три геометрии выдуманный пример: линейка с нулём у первого байта; левее нуля — до первого байта -8-7-6-5-4-3-2-10123456789101112131415до первого байтавнутри сообщения0 = первый байт сообщения multisplit:pos=midsld:seqovl=5 — seqovl это число накладка 5настоящие байты левой части … (всего 109) 5 + 104 = 109 байт, начало −5ack 105 S[-4,1) dsack tcpseg:pos=0,-1:seqovl=1 — seqovl это число накладка 1 байтнастоящие байты … (всего 251) 1 + 250 = 251 байт, начало −1ack 251 S[0,1) dsack multidisorder:pos=midsld:seqovl=host — seqovl это метка накладка 97 нулей: места 7…103 … 104 − 97 = 7, пакет 243@7 внутри сообщенияack 251 S[8,105) dsack у tcpseg оригинал выбросила отдельная стратегия drop, сама tcpseg его пропускает;разбор tcpseg позже в этом выпуске сводка по трём строкам стратегияseqovl этопакет с накладкойначало multisplitчисло109@-5левее первого байта tcpsegчисло251@-1левее первого байта multidisorderметка243@7внутри сообщения у multisplit и tcpseg число сдвигает пакет влево от начала той части, которую отправляют;у multisplit накладку получает только левая часть, она всегда с первого байта: накладка левее него;у tcpseg это зависит от pos: в наших опытах часть шла с первого байта, и накладка тоже ушла левее него;у multidisorder метка, пакет лежит внутри сообщения и начинается не раньше смещения 1 аргумент называется одинаково, а читать его нужно по функции проверь себя: seqovl=5, разрез на смещении 104где начнётся пакет с накладкой у multisplit и где у multidisorder? multisplit: 109 байт, начало на 5 байт левее первого: этот опыт запускали multidisorder: метка 5 = смещение 5, накладок 5, 5 + 146 = 151 байт, начало 104 − 5 = 99это счёт по формуле, такой опыт не запускали

Что мы знаем об seqovl в multidisorder. Это метка, и число накладок равно её смещению. Накладку получает вторая часть, метка стоит строго левее первого разреза, начало пакета не ниже единицы.

Узор по умолчанию из нулей, а seqovl_pattern заменяет его блобом или шестнадцатеричной строкой; без блоба и без optional это ошибка, и при нескольких разрезах отправленные части остаются на проводе.

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

Что мы знаем о seqovl Что мы знаем об seqovl в multidisorder, пять пунктов: это метка, а не число, и накладок столько, на сколько байт метка отстоит от начала сообщения; накладку получает вторая часть, та, что начинается с первого разреза, метка стоит строго левее разреза, а начало пакета равно разрезу минус метка и не ниже 1; узор по умолчанию из нулей, seqovl_pattern заменяет его блобом или шестнадцатеричной строкой, без блоба и без optional это ошибка, и при нескольких разрезах уже отправленные части остаются на проводе, а за ними идёт оригинал; сервер в нашем тесте на Linux получает настоящие байты и называет перекрытие дублирующим блоком, а про Windows руководство говорит иначе и мы не проверяли; игрушечный читатель «первый записавший побеждает» слеп, если накладка накрывает байты имени, «последний записавший побеждает» видит имя во всех наших опытах, а что из этого похоже на настоящее оборудование, мы не знаем. что мы знаем об seqovl в multidisorder 1это метка, а не числоseqovl=host → 97 накладокчисло накладок равно смещению метки от начала сообщения 2накладку получает вторая частьначало = разрез − метката, что начинается с первого разреза; метка строго левее разреза, начало пакета не ниже 1 3узор: по умолчанию нулиpattern(pat,1,ovl)seqovl_pattern заменяет их блобом или строкой 0x…; без блоба и без optional ошибка,при нескольких разрезах отправленные части остаются на проводе, за ними идёт оригинал 4сервер: наш тест, Linuxdsack [8,105)получает настоящие байты, перекрытие называет дублирующим блоком;про Windows руководство говорит иначе, мы не проверяли 5игрушечные читателитолько лаборатория«первый» слеп, если накладка накрывает байты имени; «последний» видит имя везде в наших опытах;что из этого похоже на настоящее оборудование, мы не знаем всё это про лабораторию и наши игрушки; о настоящем оборудовании ничего не утверждаем

Итак. Одно слово, три геометрии: у multisplit и tcpseg в наших опытах накладка левее первого байта, у multidisorder внутри сообщения. Всё это пока про лабораторию и наши игрушки.

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

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

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

Стратегию вызывают на каждый кусок проигрыша, и на каждом ей доступна вся сборка. Поэтому договор такой: стратегия действует на первом куске и по всей сборке, оставляет пометку в записи об этом соединении (программа хранит такие записи, об этом выпуск «Память о соединении»), а остальные куски выбрасывает по пометке. Это делают помощники replay_first и replay_drop, и multidisorder следует этому договору.

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

Сборка, один разрез, остальное выбросить Повтор про сборку и проигрыш и договор новой функции multidisorder: ядро клиента отправило приветствие в 1484 байта двумя сегментами по 1448 и 36 байт, программа удержала оба и склеила в одну ленту в 1484 байта, потом возвращает их по одному, и на каждом куске стратегии доступна вся лента; на первом куске multidisorder действует (replay_first, строка 610 в zapret-antidpi.lua), режет всю ленту, ставит пометку в записи о соединении (replay_drop_set, 628) и возвращает DROP (629), а на втором куске только пишет в журнал, что не действует на последующие куски (634), и выбрасывает его по пометке (replay_drop, 637-638); в выдуманном примере наборщик склеивает два листа статьи в одну ленту, режет её по метке и подаёт части с конца, забыв прежние границы листов. повтор: сборка и повторный проигрыш 1 · ядро клиента отправило приветствие двумя сегментами сегмент 1448сегмент 36 стенка между 1448-м и 1449-м байтом 2 · программа удерживает оба и склеивает их в одну ленту ящик «удержаны»сборка: одна лента, 1484 байта 3 · потом возвращает их из очереди по одному, и каждому куску стратегия видит всю ленту кусок 1: 1448 байтвся лента 1484 доступна кусок 2: 36 байтвся лента 1484 доступна договор: действовать на первом куске, остальные выбросить по пометке 610if replay_first(desync) then628      replay_drop_set(desync)629      return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP634    DLOG("multidisorder: not acting on further replay pieces")637if replay_drop(desync) then638    return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP lua/zapret-antidpi.lua первый кусок: действуемreplay_first верно режем всю ленту, пометкав записи о соединении оригинал куска 1: DROPесли не задан nodrop второй кусок: не действуемтолько строка в журнале пометка стоит? давыбрасываем кусок 2 оригинал куска 2: DROPоригиналов на проводе нет пометку хранит запись об этом соединении (программа держит такие записи: выпуск «Память о соединении») выдуманный примернаборщик склеивает два листа и режет всю ленту, границы листов забыты лист 1: строки 1…12лист 2: строки 13…201234567891011121314151617181920 одна лента: границы листов забыты, режем перед строкой 15 (число выдумано)1234567891011121314151617181920 1первой уходит часть 15…202второй часть 1…14

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

Всё сообщение это тысяча четыреста восемьдесят четыре байта, левая часть сто четыре. Значит, вторая: минус сто четыре, то есть тысяча триста восемьдесят. Первой уходит длинная, со смещения сто четыре, потом левая, сто четыре байта с начала.

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

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

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

Три части. Минус один это смещение тысяча четыреста восемьдесят три. Средняя часть идёт со ста четвёртого смещения до тысяча четыреста восемьдесят второго: тысяча триста семьдесят девять байт. Вместе со ста четырьмя и одним байтом получается тысяча четыреста восемьдесят четыре.

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

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

Опыт: один разрез и проверь себя с двумя Два опыта с multidisorder на приветствии в 1484 байта: при pos=midsld сначала уходит часть в 1380 байт со смещения 104, потом 104 байта с начала, сервер отвечает ACK 1 с блоком с 105 по 1484 и затем ACK 1485, приложение читает 1484 байта одним чтением, а пакетов в 1448 и 36 байт на проводе нет, потому что оригиналы выброшены; при pos=midsld,-1 (проверь себя) уходят 1 байт со смещения 1483, 1379 байт со смещения 104 и 104 байта с начала (104 + 1379 + 1 = 1484), блок подтверждений растёт влево: сначала блок из одного байта 1484, потом блок с 105 по 1484, затем ACK 1485, и приложение снова читает 1484 байта одним чтением. опыт 1 · pos=midsld · один разрез всей ленты 104часть 1380 байтграница сегментов ядра (1448 и 36) осталась внутри длинной части0104 12 calc: 104 + 1380 = 1484; номер в кружке это порядок отправки: сначала длинная часть, потом левая на проводебайтыответ серверау сервера есть 11380@104байты 105…1484ACK 1, [105,1485) 2104@0байты 1…104ACK 1485 на обоих пакетах только флаг ACK, без PSH: флаги копируются с первого куска, а у него PSH не было приложение: одно чтение, 1484 байта пакетов в 1448 и в 36 байт на проводе нет кусок 2: «multidisorder: not acting on further replay pieces»и «dropping replay packet because reasm was already sent»: оригинал выброшен по пометке проверь себя · pos=midsld,-1 · метка −1 это последний байт, смещение 1483 сколько частей? три; в средней 1484 − 104 − 1 = 1379 байт 104средняя часть 1379 байт1 байт, смещение 1483: он во втором сегменте ядра, функции это всё равно 321 calc: 104 + 1379 + 1 = 1484 на проводеответ серверау сервера есть 11@1483последний байт уходит первымACK 1, [1484,1485) 21379@104байты 105…1483ACK 1, [105,1485) 3104@0байты 1…104ACK 1485 блок слева растёт: сначала один последний номер, потом от 105 до конца, потом всё подтверждено приложение: снова одно чтение, 1484

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

Старая функция: позиции в окне куска и пересчётчик окна

Вопрос сцены: как старая функция, multidisorder_legacy, понимает, какая позиция разреза относится к какому куску проигрыша?

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

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

Отметка на строке пять достаётся первой странице, а на второй её нет. Строка тринадцать это первая строка второй страницы: на первой её нет, на второй она становится строкой номер один. Разрез перед первой строкой страницы разрезом не считается.

Статья на двух страницах: своя нумерация Выдуманный пример про окна кусков: статья в 20 строк напечатана на двух страницах (строки с 1 по 12 и с 13 по 20), каждая типография нумерует строки своей страницы заново с единицы, отметка на строке 15 не находит строки на первой странице и даёт на второй строку 15 − 13 + 1 = 3, отметка на строке 5 попадает на первую страницу и не находит строки на второй, а отметка на строке 13 даёт на второй странице строку 1, и разрез перед первой строкой страницы разрезом не считается. как отметку перевести в номер строки внутри страницы выдуманный примерстатья в 20 строк, две типографии, у каждой своя нумерация с единицы страница 1: строки 1…12страница 2: строки 13…201234567891011121314151617181920 каждая типография нумерует свою страницу заново, с единицы12345678910111212345678 отметка редакторастраница 1страница 2 отметка: строка 15такой строки нет15 − 13 + 1 = 3: строка 3 отметка: строка 5строка 5такой строки нет отметка: строка 13такой строки нетстрока 1 (первая строка страницы) номер на странице = номер в статье − первая строка страницы + 1 разрез перед первой строкой страницы не считается: строка 13 стала строкой 1

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

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

Метка midsld. Это смещение сто четыре, а в коде позиция сто пять. Для первого куска окно начинается с единицы, сто пять входит, результат сто пять минус один плюс один, то есть сто пять. Для второго куска сто пять меньше нижней границы, результат пусто.

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

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

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

Окно куска и пересчётчик Окна кусков проигрыша и пересчётчик окна pos_normalize (zapret-antidpi.lua, строки 542-544): окно куска 1 это позиции от 1 до 1448 (нижняя граница входит, верхняя 1449 нет), окно куска 2 от 1449 до 1484, позиция в коде равна смещению плюс один, и метка midsld (смещение 104, позиция 105) даёт на куске 1 номер 105 и пусто на куске 2, а метка минус один (смещение 1483, позиция 1484) даёт на куске 1 пусто и на куске 2 номер 1484 − 1449 + 1 = 36 (в журнале 35); пересчёт списка pos_array_normalize (545-556) для списка из 105 и 1484 оставляет на куске 1 одно значение 105, а на куске 2 удаляет 105 без сдвига счётчика и оставляет 36; по счёту в коде, а не по запуску, метка на смещении 1448 пропадает на обоих кусках: на куске 1 она равна верхней границе и даёт пусто, на куске 2 она становится позицией 1 и её убирает delete_pos_1. у каждого куска проигрыша своё окно позицийlua/zapret-antidpi.lua зачем пересчёт: функция получает байты только своего куска, а метки посчитаны на всей сборке кусок 1: 1448 байт окно куска 1: [1, 1449) окно куска 2 (36 байт): [1449, 1485) нижняя граница входит, верхняя нет; в коде позиции с единицы, в журнале с нуля: смещение = Lua − 1 542function pos_normalize(pos, low, hi)543    return (pos>=low and pos<hi) and (pos-low+1) or nil544end 1 · pos в окне: low ≤ pos < hi2 · да: pos − low + 1 это номер внутри куска3 · нет: nil, пусто меткасмещение → кодкусок 1: [1, 1449)кусок 2: [1449, 1485) midsld104 → 105105 − 1 + 1 = 105пусто −11483 → 1484пусто1484 − 1449 + 1 = 36, в журнале 35 список позиций: pos_array_normalizeстроки 545-556 548while i<=#pos do549  pos[i] = pos_normalize(pos[i], low, hi)551    i = i + 1553    table.remove(pos, i); идёт по списку и пересчитываетпусто: ячейку удалитьи счётчик i не двигатьсписок правится на месте кусок 1 · окно [1, 1449)список {105, 1484}i=1: 105 остаётсяi=2: 1484 → nil, ячейка удаленаитог: {105} кусок 2 · окно [1449, 1485)список {105, 1484}i=1: 105 → nil, ячейка удалена, i на местеснова i=1: 1484 → 36, потом i=2: конецитог: {36} потом delete_pos_1 (669) убирает позицию 1: разрез перед первым байтом куска не считается проверь себя: метка на смещении 1448, это первый байт куска 2; что с ней сделает пересчётчик? метка на смещении 1448 ▼ в коде позиция 1449. Кусок 1: 1449 < 1449 неверно, результат пустоКусок 2: 1449 − 1449 + 1 = 1, и позицию 1 убирает delete_pos_1метка пропала на обоих кусках: разреза нет нигде (по коду, проверим в опыте)

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

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

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

Старая функция по строкам и её первый прогон на большом приветствии

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

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

Ворота те же: не TCP, направление и тип нагрузки. Отличий три: у старой функции нет аргумента blob, нет аргумента nodrop и нет помощников проигрыша, replay_first и replay_drop.

Поэтому каждый кусок проигрыша она судит сам по себе и выбрасывает или пропускает именно его.

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

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

Если какая-то позиция осталась, функция, когда задан seqovl, так же пересчитывает его метку и отправляет части этого куска тем же общим отправителем, что и новая.

Если не осталось ни одной, она отправляет кусок как есть и возвращает «выбросить».

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

Старая функция по шагам Старая функция multidisorder_legacy (zapret-antidpi.lua, строки 649-696) по шести шагам: ворота как у новой функции (650-659), данные (data это нагрузка именно этого пакета, fulldata это вся сборка, 657-658), окно куска (660-661), позиции разрешаются на всей сборке, пересчитываются в окно куска и теряют позицию 1 (665-669), если позиции остались, режется этот кусок общим отправителем multidisorder_send (670-687), а если не осталось, кусок отправляется как есть через rawsend_payload_segmented и возвращается DROP (691-692); у старой функции нет аргументов blob и nodrop и нет помощников replay_first и replay_drop, а слова как есть значат глубокую копию пакета с применёнными аргументами подмены полей, например ip_ttl. старая функция multidisorder_legacy: шесть шаговlua/zapret-antidpi.lua:649-696 657local data = desync.dis.payload658local fulldata = desync.reasm_data660local range_low = (desync.reasm_offset or 0) + 1661local range_hi = range_low + #data665local pos = resolve_multi_pos(fulldata, desync.l7payload, spos)668pos_array_normalize(pos, range_low, range_hi)669delete_pos_1(pos)670if #pos>0 then687  return multidisorder_send(desync, data, seqovl, pos)691  if rawsend_payload_segmented(desync) then692    return VERDICT_DROP 1воротакак у новой, строки 650-659 2данныеэтот пакет и вся сборка 3окно куска[low, hi) этого куска 4позициина сборке, без позиции 1 5есть позициирежем этот кусок 6позиций неткусок как есть, DROP у старой функции нет blob, нет nodrop, нет replay_first и replay_drop: каждый кусок она судит сама «как есть» это глубокая копия текущего пакета с аргументами подмены полей из строки стратегии(например ip_ttl); оригинал выбрасывается, копия уходит. Не значит «не трогали».

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

Режутся только эти тысяча четыреста сорок восемь байт. Левая часть сто четыре, правая это остаток, тысяча триста сорок четыре. Первой уходит правая, со смещения сто четыре, потом левая.

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

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

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

Первый запуск старой функции Первый запуск старой функции на приветствии в 1484 байта с pos=midsld, куски по одному: на куске 1 (окно от 1 до 1448, в журнале диапазон 0-1447) пересчитанная позиция 104, поэтому режутся только эти 1448 байт на 1344 и 104 (1448 − 104 = 1344), сначала уходит 1344 байта со смещения 104, потом 104 с начала, оригинал выброшен; на куске 2 (в журнале 1448-1483) пересчитанной позиции нет, поэтому уходит копия как есть, 36 байт со смещения 1448 (пакет в 88 байт: 20 + 32 + 36), оригинал выброшен; в итоге на проводе 1344@104, 104@0, 36@1448, сервер отвечает блоком с 105 по 1448 и ACK 1449, приложение делает два чтения, 1448 и 36 байт (число чтений решает момент чтения, а не только функция), тогда как новая функция с теми же аргументами отправляет 1380 и 104 байта и приложение читает 1484 одним чтением; при проверке на приветствии в 250 байт старая функция отправляет то же, что новая: 146 байт со смещения 104, затем 104 с начала. опыт: multidisorder_legacy, pos=midsld, приветствие в 1484 байта кусок 1: 1448 байт окно [1, 1449): в журнале «диапазон 0-1447» кусок 2: окно [1449, 1485) = 1448-1483 кусок 1: режем только его 1448 байтreasm piece range: 0-1447normalized split pos: 104 calc: 1448 − 104 = 1344 11344@1042104@0первой уходит правая часть, потом леваяоригинал 1448 байт: DROP кусок 2: позиций нет, копия как естьreasm piece range: 1448-1483no normalized split pos in this packet 336@1448rawsend_dissect repeats=1 size=88calc: 20 + 32 + 36 = 88IP 20, TCP 32 (с опциями), данные 36оригинал 36 байт: DROP опытна проводеответы сервераприложение старая1344@104, 104@0, 36@1448ACK 1, [105,1449) и ACK 1449два чтения:1448 и 36 байт новая1380@104, 104@0ACK 1, [105,1485) и ACK 1485одно чтение:1484 байта сколько чтений, решает момент чтения, а не только функция проверь себя: multidisorder_legacy, pos=midsld, приветствие в 250 байт, один пакет то же, что у новой: 146@104, затем 104@0; окно одного пакета равно всему сообщениюжурнал: диапазон 0-249 и пересчитанная позиция 104; одно чтение, 250 байт

Проверь себя. Старая функция, позиция midsld, приветствие в двести пятьдесят байт, один пакет. Что она отправит?

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

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

Поздние куски и границы: три опыта, две функции; итог части

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

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

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

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

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

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

Новая и старая: поздние куски Два опыта с большим приветствием в 1484 байта, где новая функция режет всю ленту, а старая каждый кусок проигрыша в его окне: при pos=midsld,-1 новая отправляет 1@1483, 1379@104, 104@0 и приложение читает 1484 байта одним чтением, а старая отправляет 1344@104, 104@0, 1@1483, 35@1448, ни один пакет не пересекает границу клиентских сегментов, и приложение делает два чтения, 1448 и 36 (их число зависит от момента чтения); при pos=-1 новая начинает с последнего байта (1@1483, 1448@0, 35@1448), а старая сначала отправляет копию первого куска целиком (1448@0), потом 1@1483 и 35@1448, причём часть в 1483 байта у новой уходит двумя пакетами, потому что в пакете данных не больше 1448 байт (MSS 1460 минус 12 байт опций с меткой времени), а почему так, объяснит следующая часть выпуска. опыт 1 · позиции midsld и −1 (последний байт, смещение 1483) новая: режет всю ленту, границы клиентских сегментов ей безразличны321на проводе: 1@1483, 1379@104, 104@0ответы: ACK 1 и блок [1484,1485); ACK 1 и блок [105,1485); ACK 1485приложение: одно чтение, 1484 байта старая: режет каждый кусок отдельно, пакеты не пересекают стенку1234на проводе: 1344@104, 104@0, 1@1483, 35@1448ответы: ACK 1 и блок [105,1449); ACK 1449; ACK 1449 и блок [1484,1485); ACK 1485приложение: два чтения, 1448 и 36 (зависит от момента чтения) на куске 2 метка −1 стала 35 (журнал «normalized split pos: 35»): 36 байт режутся на 35 и 1 опыт 2 · только позиция −1 старая: на первом куске разреза нет, копия целиком; потом кусок 2123на проводе: 1448@0, 1@1483, 35@1448ответы: ACK 1449; ACK 1449 и блок [1484,1485); ACK 1485приложение: два чтения, 1448 и 36 (зависит от момента чтения) новая: сначала последний байт, потом всё остальное231на проводе: 1@1483, 1448@0, 35@1448ответы: ACK 1 и блок [1484,1485); ACK 1449 и блок [1484,1485); ACK 1485приложение: два чтения, 1448 и 36 (зависит от момента чтения) часть в 1483 байта не влезла в пакет: данных в нём не больше 1460 − 12 = 1448(MSS из SYN минус 12 байт опций), поэтому она ушла двумя пакетами; почему, в следующей части

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

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

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

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

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

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

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

Граница кусков и итог части Опыт с позицией 1448, ровно на границе двух клиентских сегментов, и сводка двух функций: новая функция отправляет 36@1448 и потом 1448@0, сервер отвечает блоком с 1449 по 1484 и затем ACK 1485, приложение читает 1484 байта одним чтением, а старая два раза пишет в журнал, что в этом пакете нет пересчитанной позиции, потому что метка выпала из обоих окон (на куске 1 она равна верхней границе, на куске 2 становится позицией 1 и её убирают), и отправляет 1448@0 и 36@1448 в обычном порядке, с ответами ACK 1449 и потом ACK 1486 вместе с FIN; таблица четырёх опытов (midsld, midsld и −1, только −1, граница) показывает, что новая функция каждый раз переставляет части всей ленты, а старая оставляет пакеты внутри клиентских сегментов; для проверки старая функция с позицией 1447 по коду режет первый кусок на 1 байт и 1447 байт и отправляет их в обратном порядке, а второй кусок уходит копией (счёт по коду, опыт не запускался); итог части: накладка лежит внутри сообщения, новая функция режет всю ленту, старая каждый кусок в его окне. опыт 3 · pos=1448: метка ровно на границе клиентских сегментов кусок 1: окно [1, 1449) метка на смещении 1448 (позиция 1449): не входит в окно 1, а в окне 2 это позиция 1 ▲ метка «падает в щель» между окнами: это ответ на вопрос про окна кусков новая: режет всю ленту по метке 1448, переставляет сегменты12на проводе: 36@1448, 1448@0ответы: ACK 1 и блок [1449,1485); ACK 1485приложение: одно чтение, 1484 байта старая: в журнале дважды «no normalized split pos in this packet»12на проводе: 1448@0, 36@1448ответы: ACK 1449, потом ACK 1486 вместе с FINприложение: два чтения, 1448 и 36 (зависит от момента чтения) позицииновая: режет всю лентустарая: каждый кусок отдельно midsld1380@104, 104@0пакет в 1380 пересекает границу1344@104, 104@0, 36@1448пакеты внутри клиентских сегментов midsld, −11@1483, 1379@104, 104@0последний байт первым1344@104, 104@0, 1@1483, 35@1448последний байт после первого куска только −11@1483, 1448@0, 35@1448начинает с последнего байта1448@0, 1@1483, 35@1448начинает с копии первого куска граница 144836@1448, 1448@0переставляет сегменты1448@0, 36@1448ничего не переставлено проверь себя: старая функция, pos=1447, байт перед границей; что и в каком порядке? смещение 1447, в коде позиция 1448: в окно куска 1 входит, в окно куска 2 неткусок 1: сначала 1@1447, потом 1447@0 (обратный порядок); кусок 2 уходит копией, 36@1448это счёт по коду, такой опыт мы не запускали итог части seqovl в multidisorderодна метка: длина накладкинакладка лежит внутри сообщенияприёмник получает настоящие байты проигрыш: новая и стараяновая режет всю ленту, куски выбрасываетстарая режет каждый кусок в его окнеметка на границе у старой пропадает дальше: нарезка по MSS, ловушки старой функции, tcpseg и drop, игрушечные читатели →

Повтор и вопрос: кусок длиннее пакета, ломтики

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

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

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

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

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

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

Потолок данных в пакете Вопрос про длинный кусок при мелких пакетах: при обычном размере пакета (максимум данных 1448) длинная часть в 1380 байт уходит одним пакетом, а в опыте с MTU клиента 600 (приём лаборатории, к стратегии не относится) она в пакет не помещается; код берёт меньший из двух MSS из SYN-пакетов (минимум считает программа на C, desync.c 1021-1030, а если известны не оба, берётся запасное значение 1220) и вычитает 12 байт опций (две пустышки NOP и метка времени в 10 байт), в опыте клиент объявил 560, сервер 1460, значит 560 − 12 = 548 байт данных в пакете, и ядро клиента нарезало приветствие на 548, 548 и 388 (548 + 548 = 1096, 1484 − 1096 = 388), а в обычных опытах обе стороны объявляли 1460 и 1460 − 12 = 1448, это потолок из прошлой части выпуска. вопрос части: что с длинным куском, если пакеты мельче? рамка это один пакет; обычный размер: максимум данных 14481380@104: влезает в один пакет размер пакета уменьшен (MTU 600): максимум данных 5481380@104: в пакет не помещается приём лаборатории: пакет на клиенте ограничили 600 байтами; к стратегии это не относится размер сегмента (MSS) объявляют в первых пакетах соединения; код берёт меньший SYN клиентаMSS 560SYN-ACK сервераMSS 1460меньший560минус 12 опций= 548 548 это максимум данных в пакете; 12 байт опций: две пустышки NOP и метка времени в 10 байт SYN клиентаMSS 1460SYN-ACK сервераMSS 1460меньший1460минус 12 опций= 1448 обычные опыты: потолок 1448 из прошлой части (1460 − 12) так ядро клиента нарезало это приветствие при MSS 560 548548388calc: 548 + 548 = 1096; 1484 − 1096 = 388 минимум считает C, Lua его только читаетnfq2/desync.c:1021-1030 1024if (pos && pos->mss && rpos && rpos->mss)1026    lua_pushf_int(params.L, "tcp_mss", rpos->mss > pos->mss ? pos->mss : rpos->mss);1029    lua_pushf_global(params.L, "tcp_mss", "DEFAULT_MSS"); в стратегии: сколько данных помещается в пакетlua/zapret-lib.lua:1233-1279 1240  mss = mss or desync.tcp_mss1241  local extra_len = l3l4_extra_len(discopy)1243  local max_data = mss - extra_len размер сегмента, который сообщила программа байты опций заголовка: у нас 12 максимум данных в пакете: 560 − 12 = 548 оба MSS известны берётся меньший иначе запасное значение 1220

Что делает новая функция на этом приветствии? Короткий кусок, сто четыре байта, помещается в пакет. Длинный не помещается, и его нарезают на ломтики; это слово из выпуска «multisplit: резать на части», повторим его, чтобы не путать ломтики с кусками по метке. Два ломтика по пятьсот сорок восемь байт, вместе тысяча девяносто шесть. Остаток, двести восемьдесят четыре байта, это последний ломтик.

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

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

Код тот же, что у multisplit. Функция нарезки при отправке, rawsend_dissect_segmented, получает от программы размер сегмента, вычитает из него байты опций и получает максимум данных, в коде max_data. Если в куске данных больше, она отрезает ломтик за ломтиком, а последнему отдаёт остаток. По умолчанию каждый ломтик получает свой номер IP id, на единицу больше предыдущего, а порядковый номер сдвигается на длину ломтика, поэтому ломтики идут по возрастанию.

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

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

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

Ломтики, ответы и потолок с накладкой Новая функция при максимуме данных в пакете 548 байт режет ленту в 1484 байта на 104 и 1380 байт и нарезает длинный кусок на ломтики 548, 548 и 284 (1484 − 104 = 1380, 1380 − 1096 = 284): на проводе уходят ломтик со смещения 104 длиной 548, со смещения 652 длиной 548 (104 + 548 = 652), со смещения 1200 длиной 284 (652 + 548 = 1200) и последним короткий кусок со смещения 0 длиной 104, получатель отвечает ACK 1 с блоком, который растёт вправо (с 105 по 652, с 105 по 1200, с 105 по 1484), а потом ACK 1485, тогда как в опыте с пятью кусками блок рос влево; код rawsend_dissect_segmented (zapret-lib.lua) вычитает опции из размера сегмента, режет кусок длиннее максимума на ломтики, даёт каждому свой IP id и сдвигает порядковый номер на длину ломтика (строки 1243-1271); тот же потолок виден с накладкой: seqovl=host добавляет к длинному куску 97 байт (1380 + 97 = 1477), это больше 1448, поэтому уходят ломтики 1448@7 и 29@1455 (1477 − 1448 = 29, 7 + 1448 = 1455) и затем 104@0; для проверки multisplit при тех же условиях отправляет по порядку с начала: 104 байта со смещения 0, потом ломтики 548 со смещения 104, 548 со смещения 652 и 284 со смещения 1200. новая функция при максимуме данных 548 байт: длинный кусок идёт ломтиками метка midsld режет ленту на 104 и 13801041380 длинный кусок режется на ломтики по максимуму данных1045485482841234 calc: 1484 − 104 = 1380; 548 + 548 = 1096; 1380 − 1096 = 284 смещение = номер − 1: смещение 104 это байт номер 105 на проводекак полученоответ сервераблок у сервера 1смещение 104, длина 548ACK 1, [105,653) 2смещение 652, длина 548104 + 548 = 652ACK 1, [105,1201) 3смещение 1200, длина 284652 + 548 = 1200ACK 1, [105,1485) 4смещение 0, длина 104короткий кусокACK 1485 три ответа «жду первый байт»: блок с 105 растёт вправо, в первой части (пять кусков) он рос влево нарезка при отправке: rawsend_dissect_segmentedlua/zapret-lib.lua 1243  local max_data = mss - extra_len1246  if #discopy.payload > max_data then1252      if len > max_data then len = max_data end1264      discopy.payload = string.sub(payload,pos,pos+len-1)1265      apply_ip_id(desync, discopy, options and options.ipid)1270      discopy.tcp.th_seq = u32add(discopy.tcp.th_seq, len) максимум данных в пакетеразмер сегмента минус опции кусок длиннее максимума:режем на ломтики ломтик не длиннее макс.,последнему отдаём остаток берём ломтик кускаи отправляем у каждого свой IP id,по умолчанию на 1 больше номер сдвигаетсяна длину ломтика: вверх тот же потолок с накладкой: seqovl=host добавляет 97 байт 1380 накладка 97 клеток, смещения 7…103 calc: 1380 + 97 = 1477, это больше потолка 1448 ломтик 1448@7: накладка и начало29@1455 потом 104@0 (левая часть) calc: 1477 − 1448 = 29; 7 + 1448 = 1455 проверь себя: multisplit при тех же условиях (максимум 548, метка midsld): порядок? 10, длина 104 2104, длина 548 3652, длина 548 41200, длина 284 по порядку, с начала; внутри длинного куска порядок обычный, между кусками у multidisorder с конца

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

Старая функция: четыре ловушки

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

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

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

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

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

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

Ловушка 1: кусок без разреза Первая ловушка старой функции multidisorder_legacy: сначала выдуманный пример, где копировальная контора режет длинный лист на два, а короткий лист в 36 слов не режет, но переписывает на чистый бланк, и на все три листа на выходе ставит штамп «время жизни три»; потом наш опыт на большом приветствии в 1484 байта с аргументом ip_ttl=3, где на проводе три пакета: 1344 байта со смещения 104, 104 байта с начала и хвост в 36 байт со смещения 1448, и у всех трёх TTL равен 3, то есть хвост мы не резали, а штамп он получил; и наконец код ветки «ничего не режем» (строки 688-694 файла zapret-antidpi.lua): запись в журнал, отправка копии текущего пакета со всеми аргументами подмены полей и вердикт DROP, то есть копия вместо оригинала. ловушка 1: кусок, в котором резать нечего выдуманный примеркопировальная контора: лист без разреза всё равно переписывают на столедлинный листрежем на два короткий: 36 словтрогать не надо контораштамп на каждый лист«время жизни 3» на выходе часть 13 часть 23 короткийновый бланк3 три листа, на всех штамп: не резали, но переписали опыт: большое приветствие 1484 байтаmultidisorder_legacy:pos=midsld:ip_ttl=3 граница ядра: 144836midsld 104сообщение от клиента 1344@104TTL 3 104@0TTL 3 36@1448TTL 3хвост, который не резали по общим сведениям, время жизни (TTL) ограничивает число переходов пакета по сетиу нас от клиента до сервера один переход, трёх хватает; опыта с недостаточным значением нет откуда это в кодеlua/zapret-antidpi.lua 688else689  DLOG("multidisorder_legacy: no normalized split pos in this packet")691  if rawsend_payload_segmented(desync) then692    return VERDICT_DROP журнал: резать нечего копия пакета со всемипараметрами подмены копия вместо оригинала это ветка «ничего не режем»: на ней TTL и получает хвост

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

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

Третья ловушка, мелкая: что куски берут у проигрываемого пакета? Счётчик IP id у куска начинается с номера этого пакета. Ядро нумерует свои два пакета подряд, поэтому в нашем тесте у куска в сто четыре байта и у хвоста в тридцать шесть номер один и тот же. Флаги копируются: у первого пакета ядра был только ACK, у второго ещё и PSH, проталкивание. Смотрит ли на эти поля настоящее оборудование, мы не знаем.

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

Ловушки 2, 3 и 4 старой функции Ловушки 2, 3 и 4 старой функции: она не читает параметры blob и nodrop (с blob=nosuchblob старая режет как обычно, 146@104 и 104@0 на маленьком приветствии, а новая пишет в журнал ошибку, что блоб nosuchblob недоступен, и пакет идёт как есть, 250@0; с nodrop старая всё равно выбрасывает оригинал, и на проводе только куски, а новая на большом приветствии отправляет 1380@104 и 104@0 и следом оригиналы 1448@0 и 36@1448); куски берут IP id и флаги у пакета ядра (ядро пронумеровало два своих пакета 57761 и 57762, поэтому у куска в 104 байта и у хвоста в 36 байт один и тот же id 57762, а флаги ACK у первого пакета ядра и PSH с ACK у второго, функция их не меняет); и строки отправки в журнале подписаны словом multidisorder, даже когда работает старая функция, потому что их пишет общий помощник multidisorder_send из строк 558-586 файла zapret-antidpi.lua. ловушки 2, 3 и 4 старой функции 2параметры, которые старая функция не читает старая функцияновая функция blob=nosuchblobприветствие250 байтбез ошибки, режет как обычно146@104, 104@0 ошибка в журнале: blob'nosuchblob' unavailableпакет идёт как есть: 250@0 nodropстарая: 250 байтновая: 1484 байтаоригинал всё равно выброшен146@104, 104@0на проводе только куски два куска, следом оригиналы1380@104, 104@0, 1448@0, 36@1448ответы с дублирующими блоками 3IP id и флаги: куски берут их у пакета ядра ядро отдало два пакетапакет ядра 1: 1448 байтid 57761, флаг ACK пакет ядра 2: 36 байтid 57762, флаги PSH+ACK на проводе 1344@104id 57761ACK 104@0id 57762ACK 36@1448id 57762PSH+ACK в нашем тесте у двух кусков один и тот же номер:ядро нумерует свои пакеты подряд поля наследуются,функция их не меняет смотрит ли настоящее оборудование на эти поля, мы не знаем 4журнал: строки отправки подписаны чужим именем multidisorder_legacy: normalized split pos: 104 multidisorder: sending part 2 104-1447 len=1344 seqovl=0 multidisorder: sending part 1 0-103 len=104 seqovl=0 multidisorder_sendобщий помощник, строки 558-586строка отправки: 580 остальные строки старой функции подписаны её собственным именем: не удивляйтесь

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

Проверь себя. Старая функция, маленькое приветствие, pos равно нулю, время жизни три. Что окажется на проводе?

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

И ещё проверка на старой функции. Максимум данных пятьсот сорок восемь, большое приветствие, метка midsld. Что на проводе?

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

Накладка и проверки старой функции Накладка в старой функции и две проверки: метка seqovl считается по всему приветствию, но накладка должна попасть в диапазон текущего пакета ядра; с seqovl=host, то есть 97, на большом приветствии на проводе 1441@7 (97 байт накладки и 1344 байта данных), 104@0 и хвост 36@1448, сервер отвечает ACK 1 с блоком с 8 по 1448 и потом ACK 1449 с блоком с 8 по 104 как дубликатом, а с seqovl=-5 метка попадает на смещение 1479, за пределы первого пакета (0-1447), накладки нет и на проводе 1344@104, 104@0, 36@1448; первая проверка: старая функция на маленьком приветствии при pos=0 и TTL 3 отправляет один пакет в 250 байт со смещения 0 с TTL 3, оригинал ядра выброшен; вторая проверка: при максимуме данных 548 и метке midsld на большом приветствии на проводе 444@104, 104@0, 548@548 и 388@1096, а приложение в этом запуске сделало три чтения 548, 548 и 388. старая функция: накладка и две проверки seqovl=hostметку считают по всему приветствию (host = 97), накладка идёт в пакет ядра первый пакет ядраhost 97midsld 1040 … 1447 1441@7накладка 97 байт: другой цвет 104@0 36@1448 97 + 1344 = 1441 хвост без разреза: накладка только в первом пакете ответ сервера: ACK 1 и блок [8,1449), потом ACK 1449 и блок [8,105)второй блок лежит ниже подтверждения: перекрытый диапазон получатель отметил как дубликат seqovl=-5метка указала в хвост приветствия: 1479, за пределы пакета 0 … 1447 накладки нетна проводе: 1344@104, 104@0, 36@1448, как без накладки 1проверь себя: старая, маленькое приветствие, pos=0, TTL 3: что на проводе? 250@0один пакет, TTL 3оригинал ядра выброшен: резать нечего, ушла копия 2и ещё: максимум данных 548, большое приветствие, старая, midsld: что на проводе? 444@104 104@0 548@548 388@1096 первый пакет ядра режется: сначала 444, потом 104; второй и третий идут целыми, по возрастанию чтения приложения в этом запуске: 548, 548, 388; их число зависит от моментов чтения

Итак, у старой функции четыре ловушки. Кусок без разреза она отправляет копией, со всеми аргументами подмены полей. Параметров nodrop и blob не читает. Проигрываемые куски берут IP id и флаги у пакета ядра. Строки отправки в журнале подписаны именем другой функции. А seqovl у неё привязан к текущему пакету ядра.

tcpseg: идея, код и мерка диапазона resolve_range

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

Вопрос. Можно ли отправить серверу только начало запроса, а остальное оставить самому клиенту?

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

Теперь настоящая команда нашего опыта: tcpseg с параметром pos «ноль, midsld» и отдельный вызов drop. Назовём tcpseg «одиночный сегмент»: она вырезает один диапазон байт и отправляет его одним сегментом. Слишком длинный диапазон нарежется на ломтики, как в первой сцене.

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

Теперь код. Начало знакомо: если пакет не TCP, функция уходит, потом отсечка для входящих, то есть просьба к движку больше не звать этот вызов для входящих (выпуск «Lua внутри программы»). Новое одно: параметр pos обязателен, без него функция сразу вызывает ошибку, раньше работы с данными. Дальше берутся данные: указанный блоб, иначе собранное сообщение, иначе сам пакет.

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

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

Остался отправляющий вызов. Функция отправки получает кусок и сдвиг порядкового номера, начало диапазона минус параметр seqovl. Результат отправки никто не проверяет. Потом функция кончается без оператора return, то есть ничего не возвращает, а для движка «нет значения» означает вердикт «пропустить».

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

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

tcpseg: диапазон и код Идея tcpseg и её код: выдуманная телеграмма в 250 слов, из которой помощник отправляет первые 105 и рвёт остальное, потом настоящая команда tcpseg с pos=0,midsld и отдельный вызов drop, диапазон от смещения 0 до смещения 104 включительно, то есть 104 - 0 + 1 = 105 байт, тогда как первый кусок multisplit по той же метке равен 104 байтам; строки кода из файла zapret-antidpi.lua (1050-1089): pos обязателен и без него сразу ошибка, данные берутся из блоба, собранного сообщения или пакета, проверка проигрыша, resolve_range даёт два номера, срез string.sub включает обе границы, отправка идёт со сдвигом начало диапазона минус seqovl, а функция кончается без return, то есть вердикт «пропустить»; и проверка: при pos=0,1 уходит 2 байта, в нашем опыте 2@0. tcpseg: один диапазон, оба конца включены выдуманный примерпомощник и телеграмма в 250 слов телеграмма105145 отправлено адресату порвана адресат«получил 105» что будет дальше,покажет настоящий опыт настоящая команда опыта--lua-desync=tcpseg:pos=0,midsld --lua-desync=drop метка 0midsld = 104tcpseg: от 0 до 104 включительно, 105 байт multisplit по той же метке: первый кусок 0…103, это 104 байта 104 − 0 + 1 = 105: в журнале «sending 0-104 len=105» кодlua/zapret-antidpi.lua 1050if not desync.arg.pos then1051  error("tcpseg: no pos specified")1057local data = blob_or_def(desync, desync.arg.blob)  or desync.reasm_data or desync.dis.payload1059if replay_first(desync) then1062    local pos = resolve_range(data, desync.l7payload, desync.arg.pos)1066    local part = string.sub(data,pos[1],pos[2])1081    rawsend_payload_segmented(desync,part,pos[1]-1-seqovl)1089end pos обязателен: без негосразу ошибка, до данных данные: блоб, иначесообщение, иначе пакет проверка проигрыша два номера из двух меток срез: обе границы входят сдвиг: начало − seqovl return нет: «пропустить» проверь себя: pos=0,1: сколько байт уйдёт одним сегментом? 2 байта: смещения 0 и 1, оба конца входят; в опыте 2@0

Как список из двух меток становится диапазоном? Этим занимается «мерка диапазона», в коде resolve_range. Она написана не на Lua, а на языке Си.

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

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

Потом каждая метка вычисляется тем же переводчиком меток, что в выпуске «Где резать: метки». Метка может не найтись, например имя сайта не обнаружено. Дальше правила как у линейки. Обе не найдены, ответ пустой. Не найдена первая, она становится началом данных. Не найдена вторая, она становится последним байтом. Первая позже второй, ответ пустой.

Иначе функция возвращает два числа с прибавленной единицей, как на мосту в Lua из выпуска «Где резать: метки»; журнал переводит их обратно на счёт с нуля.

Сначала обычный случай. Pos равно «host, midsld», обе метки найдены, смещения девяносто семь и сто четыре. Оба конца входят, значит, восемь байт: «www.exam». Это чтение кода, не опыт.

Теперь примеры в нашем тесте. Pos равно «host, девятьсот девяносто девять»: вторая метка за пределами данных и становится последним байтом. Диапазон со смещения девяносто семь по смещение двести сорок девять, сто пятьдесят три байта, затем идёт оригинал.

Pos равно «девятьсот девяносто девять, midsld». Теперь не найдена первая. Диапазон со смещения ноль по смещение сто четыре, сто пять байт, и оригинал следом.

Pos равно «midsld, host». Первая метка позже второй. В журнале написано «диапазон не удалось определить», и на проводе только оригинал.

И полезный случай. Pos равно «host, endhost минус один». Метка endhost на смещении сто двенадцать, это первый байт после имени. Поэтому диапазон идёт со смещения девяносто семь по сто одиннадцать включительно, ровно пятнадцать байт имени www.example.com. Минус один нужен из-за включённого конца: без него диапазон дошёл бы до смещения сто двенадцать включительно, шестнадцать байт, имя и ещё один. Это чтение кода, а не опыт.

resolve_range: мерка диапазона Мерка диапазона resolve_range из файла nfq2/lua.c (строки 3351-3398): выдуманная линейка с двумя флажками и четырьмя картинками (оба стоят, потерян первый, потерян второй, потеряны оба или флажки в обратном порядке), код (меток ровно две, каждая метка вычисляется как в выпуске «Где резать: метки», потерянный первый конец становится началом данных, потерянный второй последним байтом, первая позже второй даёт пустой ответ, возвращаются два числа с прибавленной единицей) и примеры на приветствии в 250 байт: host,midsld по коду даёт 8 байт www.exam, в опыте host,999 даёт 153@97 и следом оригинал, 999,midsld даёт 105@0 и оригинал, midsld,host даёт «диапазон не удалось определить» и только оригинал 250@0, а host,endhost-1 по коду даёт 15 байт имени, потому что без минус единицы включённый конец дал бы 16 байт. resolve_range: два флажка и четыре случая выдуманный примерлинейка и два флажка: «начало» и «конец» оба стоятучасток между ниминачкон первый потерянот края линейкикон второй потеряндо другого краянач оба потеряны илив обратном порядке: нетначкон код: resolve_range на языке Сиnfq2/lua.c 3374if (!posmarker_list_parse(smarkers,markers,&ctm))3375  luaL_error(L, "bad marker list");3376if (ctm!=2)3377  luaL_error(L, "resolve_range require 2 markers");3378pos[0] = ResolvePos(data, len, l7payload, markers);3380if (pos[0]==POS_NOT_FOUND && pos[1]==POS_NOT_FOUND || …)3385if (pos[0]==POS_NOT_FOUND) pos[0] = 0;3386if (pos[1]==POS_NOT_FOUND) pos[1] = len-1;3387if (pos[0]>pos[1])3394lua_pushi_int(L,1,pos[0]+!bZeroBased); меток ровно две:иначе одна из двух ошибок каждая метка: «Где резать: метки» обе не найдены: ответ пустой первая не найдена: начало данных вторая не найдена: последний байт первая позже второй: пусто Lua считает с единицы: +1 примеры на приветствии в 250 байтпунктир: по коду, без запуска host,midsld8 байт: www.examпо коду, запуска нет host,999153@97, затем 250@0опыт: 249 − 97 + 1 = 153 999,midsld105@0, затем 250@0опыт: 104 − 0 + 1 = 105 midsld,hostзаливки неттолько 250@0опыт: «range cannot be resolved» host,endhost-1первый байт после имени15 байт: www.example.comпо коду; endhost = 112 минус один нужен из-за включённого конца: host,endhost без него дал бы112 − 97 + 1 = 16 байт, имя и ещё один; арифметика по коду, запуска нет

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

tcpseg без drop: лишний сегмент

Что будет, если вызвать только tcpseg, без drop?

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

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

Договоримся о словах. Когда сервер получил сто пять байт, он ждёт байт номер сто шесть, и в ответе стоит число сто шесть. Мы будем говорить: «подтверждено сто пять, жду сто шестой».

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

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

Почему оригинал вообще идёт дальше? Потому что функция не возвращает вердикта, она кончается без return, и движок читает это как «пропустить». Параметра nodrop у неё нет: tcpseg ничего не выбрасывает, поэтому отменять нечего. В нашем тесте с этим параметром, вместе с drop, на проводе то же самое, что без него: параметр просто не читается.

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

tcpseg без drop: лишний сегмент Опыт tcpseg без drop: выдуманная телеграмма, где курьер отправил первые 105 слов и на столе осталась вся телеграмма, которую тоже отправляют, поэтому адресат получает первые слова дважды; в нашем тесте на проводе два пакета, 105@0 и затем весь оригинал 250@0 с тем же IP id 19329, потому что первый отправленный функцией пакет берёт номер оригинала (строки 1098-1099 файла zapret-lib.lua); сервер отвечает ACK 106, то есть подтверждено 105 байт и жду 106-й, а после оригинала ACK 251 и блоком с 1 по 105, который лежит ниже подтверждения и значит это дубликат; приложение получило 250 байт один раз, в этом запуске чтениями 105 и 145; функция кончается без return, и движок читает это как «пропустить», параметр nodrop у неё не читается, поэтому без drop идея «остаток пришлёт ядро» не работает, остаток и так приходит в оригинале. tcpseg без drop: добавочный сегмент и оригинал выдуманный примеркурьер и телеграмма на столе курьер: первые 105 слов вся телеграмма: 250 слов адресат первые слова пришли дваждыотбросить повтор должен получатель опыт: что на проводеtcpseg:pos=0,midsld 1105@0ip id 19329добавочный сегмент 2250@0ip id 19329оригинал один IP id: первый пакет функции берёт номер оригиналаlua/zapret-lib.lua:1098-1099 слова: в ответе число 106, значит «подтверждено 105 байт, жду 106-й» ответ 1: ACK 106, подтверждено 105, жду 106-й ответ 2: ACK 251, подтверждено 250, жду 251-й, и блок [1,106) ниже подтверждения: дубликат приложение получило 250 байт один раз: чтения 105 и 145 (чтения, не пакеты) почему оригинал идёт дальшеlua/zapret-antidpi.lua 1081        rawsend_payload_segmented(desync,part,pos[1]-1-seqovl)1089end return нет: «пропустить» у tcpseg нет параметра nodrop: она ничего не выбрасывает, отменять нечего в опыте с nodrop вместе с drop провод тот же, что без nodrop: параметр не читается без drop идея «остаток пришлёт ядро» не работает: остаток и так в оригинале

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

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

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

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

tcpseg без drop: два байта и TTL 1 Два опыта с tcpseg без drop: при pos=0,1 на проводе 2@0 и затем оригинал 250@0, сервер отвечает ACK 3 и потом ACK 251 с блоком с 1 по 2, приложение сделало два чтения, 2 и 248 байт; с аргументом ip_ttl=1 добавочный сегмент 105@0 исчезает на роутере, захват на входе роутера его видит, а захваты на выходе роутера и на сервере нет, оригинал 250@0 доходит и соединение обычное, что делает с таким сегментом настоящее оборудование, мы не знаем; проверка: первые 105 байт приходят на сервер дважды, в добавочном сегменте и в оригинале, а приложение получает их один раз, потому что повтор получатель отбрасывает и сообщает о нём дублирующим блоком. тот же приём: диапазон из двух байт и время жизни 1 Аpos=0,1: два байта, потом оригиналtcpseg:pos=0,1 2@0добавочный сегмент, 2 байта 250@0оригинал ACK 3: подтверждено 2, жду 3-йACK 251: подтверждено 250, жду 251-й, и блок [1,3) приложение: два чтения, 2 и 248 байт Бчтобы добавочный сегмент не дошёл: ip_ttl=1tcpseg:pos=0,midsld:ip_ttl=1 клиентроутерсервервходвыход оригинал 250@0, TTL 64 добавочный 105@0, TTL 1 исчез захват: вход роутера105@0 с TTL 1 виден захват: выход роутерадобавочного нет захват: сервердобавочного нет оригинал доходит, соединение обычное что с таким сегментом делает настоящее оборудование, мы не знаем проверь себя: опыт без drop, время жизни обычное:сколько раз первые 105 байт дошли до сервера и сколько до приложения? до сервера дважды: в добавочном сегменте и в оригинале до приложения один раз: повтор получатель отбросил и сообщил о нём дублирующим блоком

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

tcpseg и drop: первый запуск

Как заменить оригинал, а не добавлять к нему? Добавить вызов drop. Начнём с него.

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

А если вызовы идут друг за другом, кто главнее? Когда вызовы идут друг за другом, движок сливает их ответы: «выбросить» сильнее «изменить», «изменить» сильнее «пропустить». Это разобрано в выпуске «Lua внутри программы». Движок не останавливается после «выбросить», и tcpseg выполняется всё равно. С обратным порядком вызовов провод в нашем тесте тот же.

Теперь первый запуск. tcpseg с pos «ноль, midsld» и drop. На проводе в нашем тесте сначала сто пять байт с самого начала. Оригинал, двести пятьдесят байт, на провод не выходит. Потом приходит ещё один пакет: сто сорок пять байт, со смещения сто пять. Эту часть отправила не наша функция: в журнале она проходит как обычный пакет, пропущенный без изменений.

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

drop и первый запуск tcpseg Функция drop и первый запуск tcpseg с drop: код drop из файла zapret-antidpi.lua (строки 79-85) проверяет направление и тип данных, умолчания у неё другие, чем у стратегий (направление любое и данные любые), и возвращает вердикт «выбросить»; когда вызовы идут подряд, движок сливает их ответы и «выбросить» сильнее «пропустить», поэтому у tcpseg с drop и при обратном порядке вызовов итог один; на проводе в нашем тесте сначала 105@0, оригинал 250@0 на провод не выходит, потом приходит ещё 145@105, которую отправила не наша функция, а ядро клиента, это обычная повторная отправка TCP, и в журнале этот пакет проходит как пропущенный без изменений. drop: заменить оригинал, а не добавлять к нему функция drop, «выбросить»lua/zapret-antidpi.lua 79function drop(ctx, desync)80  direction_cutoff_opposite(ctx, desync, "any")81  if direction_check(desync, "any") and payload_check(desync,"all") then83    return VERDICT_DROP проверяет направлениеи тип данных умолчания другие вердикт «выбросить» у стратегий направление только исходящее и данные только известные; у drop любые но опция payload перед ней в командной строке всё равно ограничивает, что до неё дойдёт вызовы идут друг за другом: движок сливает ответы tcpseg_1_2: пропуститьdrop_1_3: выбросить слияние выбросить DROP сильнееостальных ответов «выбросить» сильнее «изменить», «изменить» сильнее «пропустить»движок не останавливается после «выбросить»: tcpseg выполняется всё равнопри обратном порядке вызовов провод в нашем тесте тот же первый запуск: что на проводеtcpseg:pos=0,midsld + drop 1105@0отправила tcpseg 2оригиналвыброшен: на проводе нет 3145@105не наша функция откуда третий: ядро клиента отправило его само, это повторная отправка TCPв журнале он проходит как чужой: packet: id=10 pass unmodified по общим сведениям, ядро считает неподтверждённое потерянным;что запустило отправку именно сейчас, мы не выясняли

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

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

Но почему остаток не выбросили второй раз, ведь в командной строке есть drop? Он начался с середины приветствия. Проверка типа данных не узнаёт в нём приветствие и пишет «тип неизвестен». В нашем опыте перед обеими функциями стоит фильтр «только приветствие», поэтому обе не запускаются, и пакет идёт без изменений.

А без этого фильтра? Умолчание у drop «любые данные», так что по коду она выбросила бы и остаток. Опыта с таким запуском у нас нет. Руководство само советует ограничивать drop только известными данными.

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

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

Ответы получателя и фильтр Что ответил получатель и почему остаток не выбросили: сервер отвечает ACK 106, потом ACK 106 с блоком с 251 по 251, потому что клиент лаборатории закрыл соединение сигналом FIN с номером 251, а сервер держит пока только 105 байт, и этот сигнал приходит для него из будущего, потом после остатка ACK 252, приложение сделало два чтения, 105 и 145 байт, и запрос дошёл целиком; остаток начался с середины приветствия, проверка типа данных не узнаёт в нём приветствие и пишет «тип неизвестен», а в нашем опыте перед обеими функциями стоит фильтр tls_client_hello, поэтому ни tcpseg, ни drop не запускаются и пакет идёт без изменений, тогда как без фильтра drop по коду выбросила бы и остаток, опыта с таким запуском нет; проверка: сервер подтвердил 105 байт из 250, значит ядро клиента считает неподтверждёнными 250 - 105 = 145 байт с номера 106. ответы получателя: подтверждено 105, блок с FIN, подтверждено всё 1 · ACK 106: подтверждено 105, жду 106-йдальше пока пусто 2 · ACK 106 и блок [251,252): FIN клиента пришёл раньше временидыра: байты 106…250 ещё не пришлиFIN, номер 251 3 · ACK 252: после остатка подтверждено всёостаток 145@105 закрыл дыру закрытие тоже из будущего, поэтому оформлено блоком, а не принято: сервер держит пока только 105 байт клиент лаборатории закрывает соединение сразу после запроса; номер сигнала 251 приложение: два чтения, 105 и 145 байт; запрос дошёл целиком почему остаток не выбросили второй разв командной строке перед обеими функциями: --payload=tls_client_hello dpi desync ... payload_type=unknown * lua 'tcpseg_1_2' : payload_type 'unknown' does not satisfy filter * lua 'drop_1_3' : payload_type 'unknown' does not satisfy filter packet: id=10 pass unmodified остаток с середины:начала нет, тип не узнан,обе функции пропустили без этого фильтра drop по коду выбросила бы и остаток: опыта с таким запуском нет руководство само советует ограничивать drop только известными данными проверь себя: сервер подтвердил 105 из 250 байт;сколько байт ядро клиента считает неподтверждёнными и с какого номера? клиент отправил 250 подтверждено 105неподтверждено 145, с номера 106 250 − 105 = 145: именно их ядро и отправило; для ядра это обычная потеря, а не стратегия

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

Чёрная дыра, seqovl числом и большое приветствие с tcpseg

Что будет, если диапазон начинается не с нуля, а с имени сайта?

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

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

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

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

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

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

Сама функция работает правильно: вырезает то, что просили. Беда в сочетании диапазона посреди приветствия и повторной отправки, которая каждый раз приносит приветствие целиком. По коду то же будет с pos «host, девятьсот девяносто девять» и drop, но опыта с такой парой у нас нет.

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

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

Чёрная дыра: шесть отправок Чёрная дыра в нашем опыте: стратегия tcpseg с диапазоном pos=host,endhost-1 (15 байт имени, смещения 97…111) и drop отправила сегмент 15@97 шесть раз, на пять отправок сервер ответил ACK 1 с блоком с 98 по 112 (со второго ответа с пометкой дубликат), потом сервер лаборатории сам закрыл соединение по своему таймауту в 2 секунды, на шестую отправку пришёл RST, и приложение получило 0 байт, потому что каждая повторная отправка клиента снова приносит приветствие целиком в 250 байт, и стратегия режет его заново; в выдуманном примере секретарь каждый раз сдаёт то же письмо с начала, а помощник вырезает середину и выбрасывает остальное; для сравнения диапазон от нуля pos=0,midsld дал 105@0 и остаток 145@105 и 250 байт у приложения; проверка: при блоке 98…112 и ответе жду 1 не хватает 97 байт, номеров с 1 по 97, и досылать ядро будет с номера 1. чёрная дыра: диапазон начинается с имени, а не с нуля выдуманный примерсекретарь не получил ответа и каждый раз сдаёт то же письмо с начала письмо 1остальное выброшеноначалосерединаконец письмо 2остальное выброшеноначалосерединаконец письмо 3остальное выброшеноначалосерединаконец каждое новое письмо снова обрезается, начало не уходит никогда наш опыт: pos=host,endhost-1 и drop, приветствие 250 байт host,endhost-1смещение 0смещение 24997…111 это 15 байт имени www.example.com; смещение 97 это номер 98: сервер считает номера с первого байта отправкаответ серверау сервера: номера 1…250 115@97ACK 1, блок 98…112 215@97ACK 1, блок 98…112, дубликат 315@97ACK 1, блок 98…112, дубликат 415@97ACK 1, блок 98…112, дубликат 515@97ACK 1, блок 98…112, дубликат сервер лаборатории закрыл соединение сам: его таймаут 2 с (FIN) 615@97RSTсоединение сброшено красным: чего нет у сервера, номера 1…97зелёным: блок 98…112, 15 байт имени tcpseg отработала6 раз приложение на сервере получило0 байт сравнение: тот же приём, но диапазон от нуля диапазон от нуля: pos=0,midsldна проводе 105@0, потом остаток 145@105ACK 106: сервер подтвердил 105 байтостаток начинается с середины: как приветствиене узнаётся, стратегия на него не действует диапазон от имени: pos=host,endhost-1на проводе 15@97 шесть разACK остался 1: до первого байта не дошлоядро каждый раз повторяет приветствиецеликом, стратегия узнаёт его и режет снова в итоге приложение: 250 байтв итоге приложение: 0 байт проверь себясервер держит блок 98…112, ответ «жду 1»: сколько байт начала ему не хватаети с какого номера ядро будет их досылать? дыра: 97 байтблок 98…112номер 197номер 250 ядро досылает с номера 1: подтверждение дальше первого не ушло calc: 97 − 1 + 1 = 97 по коду то же будет с pos=host,999 и drop; опыта с такой парой нет дыра 1…97залито: смещения 97…249рассуждение по коду, опыта с drop нет

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

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

Параметр seqovl у tcpseg, в отличие от multidisorder, это число, а не метка. В начало сегмента вписывается столько байт накладки, и порядковый номер сдвигается влево на то же число. В нашем тесте pos был «ноль, минус один»: весь запрос, двести пятьдесят байт. Плюс один байт накладки, и вместе с drop ушёл один пакет в двести пятьдесят один байт, начинающийся на байт раньше первого.

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

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

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

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

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

Проверь себя. Большое приветствие, tcpseg с drop, pos «ноль, midsld». Сколько байт будет в повторной отправке?

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

seqovl числом, большое приветствие Два дополнения к tcpseg: seqovl у него это число слева от начала диапазона, а не метка внутри окна: в нашем опыте pos=0,-1:seqovl=1 с drop ушёл один пакет 251@-1 (250 байт запроса плюс 1 байт накладки, начало на байт левее первого), сервер ответил ACK 251 и блоком из одного байта с номером 0 как дубликатом, приложение получило ровно 250 байт, а у multisplit с seqovl=5 пакет был 109@-5 и у multidisorder накладка метка внутри окна; большое приветствие в 1484 байта приходит в проигрыше двумя пакетами по 1448 и 36 байт, и tcpseg действует только на первом из них, но на всём собранном сообщении, а на втором пишет в журнал, что не действует; без drop на проводе 105@0, затем оба оригинала 1448@0 и 36@1448 и приложение читает 105, 1343 и 36 байт (1448 минус 105 равно 1343), с drop оба оригинала выброшены и на проводе 105@0 и из повторной отправки клиента 1379@105, приложение читает 105 и 1379 байт (1484 минус 105 равно 1379); в выдуманном примере лишнюю клетку слева получатель считает повтором старого и выбрасывает. seqovl у tcpseg: число слева от начала диапазона выдуманный примерлента клеток, слева прикладывают лишнюю клетку лишняя 12345678 первая настоящая клетка: 1 получатель уже расписался за всё до первойнастоящей клетки, поэтому лишнюю считаетповтором старого и выбрасывает наш опыт: pos=0,-1:seqovl=1 и drop, приветствие 250 байт диапазон: смещения 0…249, 250 байтнакладка: 1 байт, смещение −1не в масштабе: накладка нарисована шире calc: смещения 0…249 это 250 байт; 250 + 1 = 251pos «0,-1» значит от первого до последнего байта на проводеответ сервера 1251@-1ACK 251, блок 0…0, дубликатблок в номере 0: на единицу левее первого байта приложение получило ровно 250 байт геометрия «слева от начала диапазона»;в нашем опыте диапазон начинался с нуля одно имя seqovl, три геометрии multisplitчисло, влево+диапазонпример: seqovl=5, пакет 109@-5 tcpsegчисло, влево+диапазонпример: seqovl=1, пакет 251@-1 multidisorderметка в окне+окноразобрано раньше: метка в окне большое приветствие: 1484 байта, tcpseg с pos=0,midsld 105пакет ядра: 1448 байтпакет 36смещения 0…1483 программа удержала оба пакета ядра, собрала одно сообщение в 1484 байтаи возвращает пакеты по одному: это и есть проигрыш первый проигрываемый пакетtcpseg действует, но на всём собранномсообщении: диапазон 0…104, 105 байт второй проигрываемый пакетв журнале «not acting on furtherreplay pieces», больше ничего без drop: на проводе и что читает приложение 1105@0 21448@0оригинал, как был 336@1448оригинал, как был ответы: ACK 106, потом ACK 1449 и блок 1…105 как дубликат приложение 105134336 calc: 1448 − 105 = 1343; 105 + 1343 + 36 = 1484 с drop: на проводе и что читает приложение 1105@0 21379@105из повторной отправки клиента в журнале: DROPPING delayed packet #1 и #2: оба проигрываемых оригинала выброшены приложение 1051379 calc: 1484 − 105 = 1379 проверь себябольшое приветствие, tcpseg с drop, pos=0,midsld: сколько байт будет в повторной отправке? 1484 − 105 = 1379 байт: ровно такой пакет мы увидели на проводе (1379@105)

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

Ошибки и угловые случаи: проверь себя таблицей

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

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

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

Ошибка: две строки журнала Что происходит, когда tcpseg вызван без параметра pos: функция падает на строке 1051 файла zapret-antidpi.lua с текстом tcpseg: no pos specified, вызов Lua возвращает ошибку (desync.c, строка 1079), программа пишет в журнал две строки (сначала текст ошибки с файлом и номером строки, потом строка движка с пометкой ERROR о том, что пакет пропущен без изменений), результат всей цепочки заменяется на PASS (desync.c, строка 1145), поэтому оригинал в 250 байт уходит как есть, соединение остаётся обычным и приложение получает все 250 байт; в выдуманном примере приёмщик посылок с карточкой вопросов ставит на неправильно заполненную посылку штамп не принято, и посылка уходит дальше как была. ошибка в Lua-функции: пакет идёт как есть журнал при ошибке: tcpseg вызван без posLUA ERROR: ...lua/zapret-antidpi.lua:1051: tcpseg: no pos specifieddesync ERROR. passing packet unmodified. строка 1: файл, строка кода, текст строка 2: решение движка откуда берутся две строки 1функция падает: error("tcpseg: no pos specified")zapret-antidpi.lua:1051 2вызов функции из программы вернул ошибкуdesync.c:1079 3первая строка журнала: текст ошибки, файл, строка кодаdesync.c:1081 4переход на метку err и вторая строка журналаdesync.c:1082, 1142-1143 5итог всей цепочки заменён на PASS: оригинал уходит как естьdesync.c:1145 выдуманный примерприёмщик посылок с карточкой вопросов посылка приёмщик с карточкой вопросовзаполнено неправильно не принято уходит как была ничего не ломается, но и наш приём не сработал что видно на проводе и у приложения 250@0оригинал, без изменений: ни одной лишней отправки приложение получило все 250 байт соединение осталось обычным:ошибка отключает стратегию, а не рвёт связь

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

Первый. Вызов tcpseg без параметра pos. Что в журнале?

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

Второй. Pos равно «host, midsld, endhost», три метки.

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

Третий. Pos равно «host», одна метка.

Ошибка «нужны две метки»: одна метка диапазона не задаёт. Оригинал идёт как есть.

Четвёртый. Pos обычный, а seqovl задан буквами, а не числом.

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

Пятый. Pos равно «midsld, host». Метки в обратном порядке.

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

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

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

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

Шестой. Pos равно «host, девятьсот девяносто девять». Это ошибка?

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

Шесть случаев tcpseg Шесть случаев tcpseg по одному вопросу: без pos функция падает с текстом tcpseg: no pos specified (zapret-antidpi.lua, строка 1051), три метки host,midsld,endhost дают bad marker list (строка 1062), одна метка host даёт resolve_range require 2 markers (строка 1062), seqovl=abc даёт attempt to compare number with nil (строка 1068), и в этих четырёх случаях следом идёт строка движка с пометкой ERROR о том, что пакет пропущен без изменений, и на проводе остаётся оригинал 250@0; метки в обратном порядке midsld,host не ошибка, а сообщение range cannot be resolved без слова ERROR и только оригинал на проводе, а host,999 тоже не ошибка, потому что вторая метка чинится до последнего байта и уходят диапазон 97…249 из 153 байт и оригинал; в выдуманном примере приёмщик посылок ставит штамп не принято на неправильно заполненные карточки, а комментарий в коде «only 2 first pos used» неверен, потому что три метки это ошибка. шесть случаев: что в журнале? выдуманный примерприёмщик ставит штамп «не принято» на неправильно заполненные условиезапись в журналеитог 1tcpsegбез параметра pos что в журнале? tcpseg: no pos specifiedzapret-antidpi.lua:1051 desync ERROR.passing packet unmodified.оригинал 250@0 на проводе 2pos=host,midsld,endhostтри метки что в журнале? bad marker listzapret-antidpi.lua:1062 desync ERROR.passing packet unmodified.оригинал 250@0 на проводе 3pos=hostодна метка что в журнале? resolve_range require 2 markerszapret-antidpi.lua:1062 desync ERROR.passing packet unmodified.оригинал 250@0 на проводе 4pos=0,midsld:seqovl=abcseqovl буквами что в журнале? attempt to compare number with nilzapret-antidpi.lua:1068 desync ERROR.passing packet unmodified.оригинал 250@0 на проводе 5pos=midsld,hostметки в обратном порядке что в журнале? tcpseg: range cannot be resolvedzapret-antidpi.lua:1083 не ошибка, слова ERROR нетдиапазона нет, ничего не шлётна проводе только оригинал 250@0 6pos=host,999вторая метка не найдена ошибка? tcpseg: resolved range: 97 249tcpseg: sending 97-249 len=153 не ошибка, слова ERROR нетдиапазон 97…249, 153 байтана проводе 153@97, потом 250@0 во всех шести опытах оригинал дошёл, приложение получило 250 байт:ошибка отключает стратегию, а не рвёт связь ошибку от тихого случая отличает слово ERROR в журнале; все опыты шли без drop, с drop такого опыта нет комментарий в коде и что на самом деле -- arg : pos=<postmarker list> … 2 pos required, only 2 first pos used.три метки это ошибка, а не «берём первые две»zapret-antidpi.lua:1038

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

Игрушечные инспекторы: три вида читателей и одна таблица

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

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

С multidisorder, с seqovl, привязанным к host, и с tcpseg и drop счётчик равен нулю, а сервер получил весь запрос.

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

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

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

«Пакетный» смотрит каждый пакет отдельно.

«Склейщик» склеивает данные в порядке прихода.

«Первый записал» собирает по порядковым номерам, и если байты перекрываются, остаётся записанное первым.

«Последний записал» тоже собирает по номерам, но остаётся записанное последним.

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

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

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

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

И tcpseg с drop: слеп только «пакетный», остальные видят, потому что повторная отправка клиента приносит остаток имени.

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

Без стратегии сервер получил ноль. Multisplit против «первый записал» и «строго по порядку»: сто четыре, выброшен кусок, который достраивает имя. Multidisorder против «первый записал» и «последний записал»: ноль, выброшен короткий кусок. Против «строго по порядку» и «склейщика»: двести пятьдесят, имя не собралось.

С seqovl против «первый записал»: двести пятьдесят, он видел накладку вместо имени. Против «последний записал»: ноль. С tcpseg и drop против «первый записал»: сто пять байт и обрыв, потому что повторная отправка достраивает имя, и читатель выбрасывает её и всё, что после.

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

Два читателя в роутере Два наших игрушечных читателя в роутере на пути к серверу (настоящий ТСПУ мы не видели): правило nft смотрит каждый пакет по отдельности и ищет 15 байт www.example.com на смещении 97, без стратегии его счётчик равен 6 (оригинал и пять повторных отправок), а сервер получил 0, с multidisorder, с seqovl=host и с tcpseg и drop счётчик равен 0, а сервер получил весь запрос (у tcpseg два чтения, 105 и 145 байт); второй nfqws2 в роутере, наша программа на Lua, собирает данные клиента по одному из четырёх правил (first первый записал, last последний записал, inorder строго по порядку, arrival склейщик), а найдя имя, выбрасывает этот и все следующие пакеты клиента с данными, и сервер получает без стратегии 0, при multisplit 104, при multidisorder 0 против first и last и 250 против inorder и arrival, при seqovl=host 250 против first и 0 против last, при tcpseg с drop против first 105 байт; у игрушки нет времени, таймаутов, учёта дыр, проверки TTL и контрольной суммы и разбора TLS, она обслуживает одно соединение. в роутере: правило и второй nfqws2 (наши игрушки) настоящий ТСПУ мы не видели; всё ниже это наши игрушки вид 1: правило смотрит каждый пакет по отдельности клиентроутер: правило nftищет 15 байт на смещении 97 в каждом пакетесервер правило ищет 15 байт на фиксированном месте; в нашей модели оно обходитсятеми нарезками, которые мы пробовали: порядок отправки ему безразличен счётчик правиласервер получил без стратегии60 байт multidisorder: pos=midsld0250 байт multidisorder: pos=midsld, seqovl=host0250 байт tcpseg + drop: pos=0,midsld0105 + 145 байт шесть пакетов в первой строке: оригинал и пять повторных отправок клиента вид 3: второй nfqws2 в роутере клиентроутер: второй nfqws2наша программа на Lua, режим выбираетсясервер first«первый записал» last«последний записал» inorder«строго по порядку» arrival«склейщик» для каждого соединения собирает данные клиента по одному из четырёх правил и ищет имя;нашла: выбрасывает этот пакет и все следующие пакеты клиента с данными режимстратегиясервер получил first«первый записал»без стратегии0 first«первый записал»multisplit: pos=midsld104 inorder«строго по порядку»multisplit: pos=midsld104 first«первый записал»multidisorder: pos=midsld0 last«последний записал»multidisorder: pos=midsld0 inorder«строго по порядку»multidisorder: pos=midsld250 arrival«склейщик»multidisorder: pos=midsld250 first«первый записал»multidisorder, seqovl=host250 last«последний записал»multidisorder, seqovl=host0 first«первый записал»tcpseg + drop: pos=0,midsld105 полная полоса: все 250 байт запроса ограничения игрушки · без времени · без таймаутов · без учёта дыр · без проверки TTL и контрольной суммы · поиск подстроки, без разбора TLS · одно соединение

Проверь себя. Стратегия multidisorder с pos по умолчанию, то есть два: куски в двести сорок восемь байт и два. Кто из пяти читателей видит имя?

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

Пять читателей игрушки Пять выдуманных читателей разбирают готовый захват наших опытов (настоящий ТСПУ мы не видели): пакетный смотрит каждый пакет отдельно, склейщик склеивает данные в порядке прихода, первый записал и последний записал собирают по номерам байтов и при перекрытии оставляют записанное первым или последним, а строго по порядку принимает только сегмент, начинающийся не позже ожидаемого байта; слово видит значит, что имя появилось в собранных данных и приём не сработал, а слеп значит, что читателя приём обошёл; у multisplit слеп только пакетный, у multidisorder слепы пакетный, склейщик и строго по порядку, с seqovl=host имя видит только последний записал (на начале имени лежат нули накладки), у tcpseg с drop слеп только пакетный, потому что повторная отправка клиента приносит остаток имени; для проверки при multidisorder с pos по умолчанию 2 (куски 248@2 и 2@0) имя видят четыре из пяти читателей, слеп только строго по порядку. пять читателей над готовым захватом (наши игрушки) настоящий ТСПУ мы не видели; всё ниже это наши игрушки, названия придуманы нами видитимя появилось в собранных данных: приём не сработал слепчитателя приём обошёл PK«пакетный»каждый пакет отдельно AR«склейщик»склеивает данные в порядке прихода RF«первый записал»по номерам байтов: при перекрытии остаётся записанное первым RL«последний записал»по номерам байтов: остаётся записанное последним IO«строго по порядку»берёт сегмент не позже ожидаемого байта, из будущего игнорирует PKпакетныйARсклейщикRFпервыйRLпоследнийIOпо порядку multisplit: pos=midsldкуски 104@0 и 146@104 слепвидитвидитвидитвидит multidisorder: pos=midsldкуски 146@104 и 104@0 слепслепвидитвидитслеп multidisorder, seqovl=hostкуски 243@7 и 104@0, нули накладки слепслепслепвидитслеп tcpseg + drop: pos=0,midsldкуски 105@0 и остаток 145@105 слепвидитвидитвидитвидит multisplit: слеп только «пакетный», имя лежит в двух пакетах; куски идут по возрастанию multidisorder: слепы «пакетный», «склейщик», «строго по порядку»: они читают порядок прихода seqovl=host: на начале имени лежат нули, видит только «последний записал» tcpseg + drop: слеп только «пакетный», повторная отправка клиента приносит остаток имени проверь себяmultidisorder с pos по умолчанию 2, куски 248@2 и 2@0: кто из пяти видит имя? multidisorder: pos по умолчаниюкуски 248@2 и 2@0 ????? видитвидитвидитвидитслеп четыре из пяти: имя целиком лежит в длинном куске, который идёт первым;слеп только «строго по порядку»: он игнорирует кусок из будущего

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

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

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

Multisplit, которому посвящён выпуск «multisplit: резать на части», режет сообщение по меткам и отправляет куски по порядку. Multidisorder режет всё сообщение один раз и отправляет с конца, а внутри длинного куска, если он не влезает в пакет, ломтики идут вверх. Параметр seqovl в нём это метка внутри самого сообщения, а позиция «ноль» не режет вообще.

Старая функция, multidisorder_legacy, режет каждый проигрываемый пакет отдельно и сохраняет разбивку клиента. Метка seqovl у неё считается внутри текущего пакета ядра. Кусок без разреза она отправляет копией со всеми аргументами подмены полей, параметров nodrop и blob не читает, метка на границе кусков у неё ничего не режет.

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

Четыре стратегии рядом Четыре стратегии выпуска рядом: multisplit режет сообщение по меткам и отправляет куски по порядку, seqovl у него число слева; multidisorder режет всё сообщение один раз и отправляет с конца, внутри длинного куска ломтики идут вверх, nodrop оставляет оригинал, seqovl у него метка внутри сообщения, позиция 0 не режет, а по умолчанию 2; multidisorder_legacy режет каждый проигрываемый кусок отдельно с конца и сохраняет разбивку клиента, nodrop не читает, seqovl у неё метка внутри текущего пакета ядра, кусок без разреза идёт копией через все параметры подделки, а метка на границе кусков ничего не режет; tcpseg с drop отправляет один диапазон с включённым концом одним сегментом, если он помещается в пакет, вердикта не даёт, оригинал выбрасывает drop, остаток в нашем тесте присылает ядро клиента, seqovl у него число слева, а диапазон от имени в нашем опыте дал чёрную дыру. четыре стратегии выпуска рядом multisplitzapret-antidpi.lua:483-539отправляеткуски по меткампорядокпо возрастанию, без перестановкиоригиналвыбрасывается; разобрано раньшеseqovlчислом, слева от первого байталовушкав отличие от multidisorder, без перестановки multidisorderzapret-antidpi.lua:597-641помощник 558-586отправляетвсё сообщение, режет один разпорядокс конца; ломтики вверх, если кусок не влезает в пакеторигиналвыбрасывается; nodrop оставляет оригиналseqovlметкой, внутри сообщенияловушкапозиция 0 не режет, по умолчанию 2;ломтики вверх, если кусок не влезает в пакет multidisorder_legacyzapret-antidpi.lua:649-696отправляеткаждый проигрываемый кусок отдельнопорядокс конца внутри куска; разбивка клиента сохраненаоригиналвыбрасывается; nodrop не читаетсяseqovlметкой, нормализованной на текущий пакет ядраловушкакусок без разреза идёт копией через все параметрыподделки; метка на границе кусков не режет tcpseg + droptcpseg: 1043-1089drop: 79-85отправляетодин диапазон, оба конца включеныпорядокодним сегментом, если помещается в пакет (иначе ломтики)оригиналtcpseg вердикта не даёт, выбрасывает drop;остаток в нашем тесте присылает ядро клиентаseqovlчислом, слева от начала диапазоналовушкадиапазон от имени в нашем опыте дал чёрную дыру

Четыре различия, которые легче всего перепутать. Первое: в списке разрезов позиция «ноль» не режет, а «один» режет, зато у tcpseg ноль законное начало диапазона. Второе: seqovl у multidisorder это метка внутри сообщения, а у multisplit и tcpseg число слева от начала диапазона. Третье: у tcpseg конец диапазона входит в диапазон. Четвёртое: «одним сегментом» верно, пока диапазон помещается в пакет, длинный нарежется на ломтики.

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

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

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

Различия, граница, что дальше Четыре различия, которые легче всего перепутать (в списке разрезов ноль не режет, а один режет, но у tcpseg ноль это начало диапазона; seqovl у multidisorder метка внутри сообщения, а у multisplit и tcpseg число слева от начала диапазона; конец диапазона tcpseg входит в диапазон; «одним сегментом» верно, пока диапазон помещается в пакет), граница лаборатории (код и провод, один получатель Linux с ядром 6.18, в наших опытах приложение ни разу не получило неверный байт; не проверены настоящий инспектор, другие получатели, в том числе утверждение руководства про Windows в строке docs/manual.en.md:4078, а также IPv6, UDP, repeats, ipfrag и badsum) и анонс следующего выпуска «Фейк и его условия»: добавочный сегмент со временем жизни 1 исчезает на роутере, а оригинал доходит до сервера, как в нашем опыте с tcpseg и ip_ttl=1. четыре различия, граница лаборатории и что дальше легко перепутать 1. ноль и один в списке разрезовpos=0pos=1не режетрежету tcpseg ноль законное начало диапазона 2. seqovl: метка или числоmultidisorder+метка внутриmultisplit, tcpseg+число слеваодно имя, две геометрии 3. конец диапазона tcpseg входитmultisplittcpsegpos=midsld: 104 байтаpos=0,midsld: 105 байтв диапазоне и первый, и последний байт 4. «одним сегментом»: если помещаетсякороткийодин сегментдлинныйломтикидлинный диапазон нарежется на ломтики честная граница что показала лабораториякод, прочитанный строка за строкой, и провод;один получатель: Linux, ядро 6.18;в наших опытах приложение ни разу не получилоневерный байт; перестановка, перекрытие идубликат вызывали лишь ответы с блокамии подтверждения чего она не показаланастоящий инспектор: о нём мы ничего не знаем;другие получатели: руководство пишет проWindows (docs/manual.en.md:4078), проверен Linux;IPv6, UDP, repeats, ipfrag и badsum: не проверяли;всё про «срабатывает» относится толькок нашим игрушечным читателям что дальше: «Фейк и его условия» подделки: программа отправляет рядом лишний пакет, fake, с условием, чтобы он дошёл не до всех клиентроутервремя жизни 1здесь кончаетсясервер оригинал 250@0добавочный: время жизни 1 250 105 250 105 250 1051 сегмент исчез на роутере намёк мы уже видели в этом выпуске: сегмент со временем жизни 1 доходил до роутера, но не до сервера,а оригинал доходил; в следующем разберём такие условия по одному итог выпуска стратегии отправляют куски не по порядку и с перекрытием; когда запрос доходил, приложение получало его целиком, а получатель в нашем тесте отвечал блоками и подтверждениями; старая функция режет по пакетам и хранит ловушки, tcpseg с drop оставляет остаток ядру клиента; игрушечные читатели ведут себя по-разному, а настоящий нам неизвестен

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

См. также


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

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