Some checks failed
Published content check / validate (push) Failing after 7s
Полный текст выпусков с анимированными SVG-схемами: «Письмо в сортировочном центре», «Что видит наблюдатель», «Читаем командную строку zapret2», «Где резать: метки», «multisplit: резать на части», «Перемешать: disorder», «Внутри программы». Список видео в Zapret2/Zapret2.md обновлён. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
950 lines
165 KiB
Markdown
Executable file
950 lines
165 KiB
Markdown
Executable file
---
|
||
title: "Видео 5. multisplit: резать на части"
|
||
date: 2026-10-25
|
||
tags:
|
||
- zapret2
|
||
- видео
|
||
- сценарий
|
||
- multisplit
|
||
- tcp
|
||
- seqovl
|
||
aliases:
|
||
- "Видео 5"
|
||
description: "Видео 5 серии о Zapret 2: как multisplit режет пакет на части и отправляет их по порядку, с анимированными схемами."
|
||
---
|
||
|
||
> [!info] О чём заметка
|
||
> Это полный текст видео номер 5 из серии о том, как работает [[Zapret2/Zapret2|Zapret 2]], разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.
|
||
|
||
> [!tip] Как читать пометки
|
||
> **«В нашем тесте»** значит настоящий запуск в лаборатории проекта: клиент, роутер, сервер. **«По коду»** значит, что мы прочитали исходники, но не запускали. **«По общим сведениям»** значит, что это вне нашего кода и лаборатории. О настоящем оборудовании ТСПУ мы ничего не утверждаем: наш «инспектор» это модель.
|
||
|
||
|
||
## Повтор и затравка: разрезать слово посередине
|
||
|
||
Добро пожаловать в выпуск «multisplit: резать на части». Он о стратегии multisplit: так называется приём, который режет один пакет с данными на несколько. Сначала короткий повтор, чтобы эту часть можно было смотреть отдельно от остальных.
|
||
|
||
О настоящем оборудовании на пути пакетов в этом выпуске нет ни слова: мы не знаем, как оно устроено. Мы читаем код zapret2 и смотрим на опыты в нашей лаборатории: клиент, роутер, сервер, а проверяющие в роутере это игрушки, которые мы написали сами.
|
||
|
||
Договоримся о нескольких словах, которые будут встречаться весь выпуск. Программа это nfqws2, основная программа zapret2. Её часть на языке Си, которая разбирает пакеты и зовёт функции на Lua, мы будем называть движком. В выпуске «Письмо в сортировочном центре» эту часть назвали ядром на Си; чтобы не путать её с ядром операционной системы, дальше «ядро» всегда значит ядро Linux. Стратегией мы зовём приём, например multisplit, и в то же время функцию на Lua, которая этот приём выполняет; в командной строке такую функцию вызывает опция lua-desync.
|
||
|
||
И несколько слов про лабораторию. «На проводе» значит: в записи, снятой на сетевой карте клиента, на первом отрезке пути; такая запись называется захватом. «Проверяющий» это то, что в выпуске «Письмо в сортировочном центре» называлось игрушечным инспектором: игрушка, которую мы сами собрали и которая ищет в пакетах имя сайта. Таких проверяющих будет два вида: правило в роутере и модели, которые читают записанные пакеты. Наконец, «прогон функций», из выпуска «Где резать: метки»: мы берём настоящую функцию из программы, подаём ей готовые данные и смотрим ответ, а в сеть при этом ничего не уходит.
|
||
|
||
Когда мы скажем, что приём «сработал», это значит одно: он обошёл нашу игрушку.
|
||
|
||
Из выпуска «Что видит наблюдатель» нам нужно одно: приветствие ClientHello, первое сообщение защищённого соединения. В нашей лаборатории оно занимает двести пятьдесят байт, и имя «www точка example точка com» лежит в нём с девяносто седьмого байта, считая с нуля.
|
||
|
||
Из выпуска «Читаем командную строку zapret2»: стратегию включает опция lua-desync. До первого двоеточия стоит имя функции, дальше аргументы вида «название равно значение», и значение приходит в Lua обычным текстом. У нас это multisplit, двоеточие, pos равно midsld. Без pos функция возьмёт «два», а если pos написан дважды, побеждает записанный первым. Условие на вид данных, payload равно tls_client_hello, стоит перед вызовом: нам нужно только приветствие.
|
||
|
||
Из выпуска «Где резать: метки»: позиция это номер байта, перед которым делают разрез, счёт с нуля. Вместо числа можно написать метку, слово, которое программа сама превращает в позицию. Host это начало имени, девяносто седьмой байт, а midsld середина слова example: сто один плюс семь, поделённое нацело на два, то есть три; получается сто четыре.
|
||
|
||
Разрез по midsld даёт два пакета. В первом сто четыре байта, он кончается на «www точка exa». Во втором сто сорок шесть байт, он начинается с «mple точка com». Исходный пакет программа выбрасывает. Заголовки у частей скопированы с оригинала, различаются данные и два счётчика, номер последовательности и идентификатор IP; как это делается, разберём в следующей части. Сервер складывает части и читает те же двести пятьдесят байт.
|
||
|
||
![[wiki-anim-video05-s01-cut-recap.svg|Повтор разреза по midsld]]
|
||
|
||
Такой разрез мы уже видели в начале серии, но как чёрный ящик. Сегодня откроем ящик. Вопрос выпуска: как одно приветствие превращается в несколько пакетов и что в каждом из них лежит?
|
||
|
||
Сначала игрушка, пример выдуманный. Вахтёр читает пропуск через картонку с окошком, всегда на одном месте листа, где стоит фамилия. Каждый лист он читает отдельно и ничего не склеивает. Фамилия Иванов целиком в окошке: шлагбаум закрыт. Но если фамилию разрезать между двумя листами, в окошках окажутся «Ива» и «нов», и вахтёр ничего не заметит. А получатель читает листы подряд, и для него фамилия цела.
|
||
|
||
В нашей лаборатории есть такая игрушка: правило в роутере, которое ищет имя «www точка example точка com» в одном определённом месте каждого пакета. У правила есть счётчик: он считает пакеты, которые правило поймало. Сначала прогон без программы и без разреза. Клиент не получал ответа и повторял пакет, поэтому счётчик правила показал шесть пакетов, а сервер не получил ни одного байта. Теперь прогон с разрезом посередине имени: счётчик остался нулевым, и сервер получил все двести пятьдесят байт.
|
||
|
||
![[wiki-anim-video05-s01-toy-guard-lab.svg|Вахтёр и правило-игрушка]]
|
||
|
||
План части: как стратегия вызывается и когда она ничего не делает; четыре главных условия мы назовём воротами. А потом посмотрим, как мост, то есть функция на Си, которую зовёт Lua, превращает список меток в список чисел.
|
||
|
||
Итак. Строка multisplit с pos равно midsld режет приветствие в двести пятьдесят байт на сто четыре и сто сорок шесть. В нашей лаборатории такой разрез обходит игрушечного вахтёра, который смотрит в одно место каждого пакета. О настоящем оборудовании мы ничего не знаем. Начнём с самой функции.
|
||
|
||
|
||
## Карточка функции multisplit: где живёт, что получает, что отдаёт
|
||
|
||
Вопрос сцены: что за функция multisplit, где она живёт, что ей дают и что от неё получают? Составим карточку функции.
|
||
|
||
Игрушка, пример выдуманный. Мастерская. Мастер получает заготовку и наряд с пожеланиями. Он работает по порядку: проверяет заготовку, режет, кладёт детали на конвейер и пишет кладовщику записку: старую списать или оставить.
|
||
|
||
Где живёт. Это функция multisplit в файле zapret-antidpi точка lua, глобальная. Перед ней комментарий с описанием аргументов.
|
||
|
||
Кто её зовёт. Движок, то есть часть программы на Си, о которой мы договорились в начале выпуска: он находит глобальную функцию по имени и вызывает с двумя входами под защитой, чтобы ошибка в Lua не обрушила программу. Как это устроено, разбирает выпуск «Lua внутри программы».
|
||
|
||
Первый вход, ключ ctx. Сама функция его не читает, а только передаёт помощникам отсечки. Отсечка это просьба к движку больше не звать этот вызов для такого-то направления; как движок её исполняет, тоже разбирает выпуск «Lua внутри программы». След просьбы в журнале: строка «instance cutoff».
|
||
|
||
Второй вход, таблица desync. Из неё функция берёт разобранный на поля пакет, данные пакета, собранное из кусков приветствие, тип нагрузки, направление, поля проигрыша и таблицу аргументов. Откуда они берутся, разбирают выпуски «От байтов к таблице», «Тип нагрузки и имя сайта» и «Сборка и повторный проигрыш».
|
||
|
||
Теперь аргументы. Первая группа отвечает на вопрос «что резать». Это pos, список меток, по умолчанию строка «два». И ещё имя готовых данных, которые нужно резать вместо пакета; в коде оно называется blob, по умолчанию его нет.
|
||
|
||
Есть ещё аргументы про накладку на первую часть, про необязательность блоба, про ответ движку и про подмену полей. Все по умолчанию выключены, каждый разберём, когда дойдём до него. Подмена полей это аргументы, которыми можно изменить заголовок каждого отправляемого пакета, например время жизни, TTL, из выпуска «Что видит наблюдатель». Для multisplit это опасно, и ловушку мы покажем в последней части.
|
||
|
||
Два стандартных аргумента пригодятся уже в следующей сцене. Направление, в коде dir: in значит входящие, out исходящие, any любые; по умолчанию out, то есть наружу.
|
||
|
||
И фильтр типов нагрузки, в коде payload: по умолчанию известные типы, то есть любые, кроме неизвестного и пустого.
|
||
|
||
Что функция отдаёт. Первое, пакеты: каждая часть уходит в сеть сама, а не ответом ядру. Для этого программа использует сырой сокет; по общим сведениям, так называют способ отправить в сеть пакет, который программа собрала сама, целиком, вместе с заголовками. Части не попадают в очередь, через которую ядро отдаёт пакеты программе: помните, в журнале нашего опыта было одиннадцать пакетов, и частей среди них не было. Почему так, разбирает выпуск «Внутри программы».
|
||
|
||
Второе, ответ движку: после всех частей «выбросить», а если задан аргумент «не выбрасывать», в коде nodrop, то «пропустить». Если функция ничего не сделала, она молчит, а молчание движок читает как «пропустить». Такой ответ называется вердиктом; как движок складывает вердикты нескольких вызовов, разбирает выпуск «Lua внутри программы».
|
||
|
||
![[wiki-anim-video05-s02-function-card.svg|Карточка функции multisplit]]
|
||
|
||
Третье, строки журнала. Все три роли видны в журнале нашего теста, на пакете номер четыре: это приветствие, перед ним три пакета рукопожатия. Движок пишет, что зовёт multisplit: это вход. Следом строка про отключение экземпляра. Потом собственные строки функции: список меток, найденная позиция, две строки об отправке частей. И последняя строка движка: пакет четыре, «выбросить». Это ответ.
|
||
|
||
![[wiki-anim-video05-s02-log-roles.svg|Три роли в журнале]]
|
||
|
||
Проверь себя. Мы запустили multisplit без единого аргумента на нашем приветствии. Что она сделает?
|
||
|
||
Позиция по умолчанию два, оригинал выбрасывается, и функция работает только на исходящих пакетах известного типа. На нашем приветствии выйдут две части, два байта и двести сорок восемь, как в нашем тесте.
|
||
|
||
Итак, multisplit это глобальная функция Lua, которую зовёт движок с ключом ctx и таблицей desync. Она читает аргументы с умолчаниями: позиция два, направление наружу, известные типы. Части отправляет сама, а движку отвечает «выбросить» или, с nodrop, «пропустить».
|
||
|
||
|
||
## Ворота функции: четыре вопроса и что бывает за закрытыми
|
||
|
||
Вопрос сцены: когда функция ничего не делает и что тогда происходит с пакетом?
|
||
|
||
Игрушка, пример выдуманный. Мастер перед резкой читает чек-лист. Болт, а не гайка? Есть что резать? Деталь идёт из цеха наружу? Маркировка знакомая? На любое «нет» тревоги нет: мастер не режет, деталь катится дальше.
|
||
|
||
Первый вопрос: есть ли у пакета заголовок TCP? Если нет, функция выходит сразу, а если это ещё и не ICMP (так по общим сведениям называют служебные сообщения сети, например ping), то по коду просит движок отключить стратегию для соединения; на проводе не проверяли.
|
||
|
||
Ещё до остальных вопросов функция отключает себя для встречного направления, то есть при направлении наружу для входящих пакетов. В журнале нашего теста это строка «instance cutoff», in равно один, out равно ноль.
|
||
|
||
Второй вопрос: что резать, и есть ли оно. Данные выбираются по порядку: блоб из аргумента blob, если он задан; потом собранное приветствие, то есть склеенное из нескольких пакетов (выпуск «Сборка и повторный проигрыш»); потом данные пакета. Режется первое найденное. По коду у обычного пакета сборка уже равна данным пакета, так что третий вариант запасной.
|
||
|
||
Пустой блоб считается найденным, ведь пустая строка в Lua это значение: до сборки дело не доходит, а резать нечего.
|
||
|
||
Прогон функций: берём настоящую функцию, подаём готовые данные и смотрим ответ; в сеть ничего не уходит. Данные заменены блобом из восьми байт, позиции два и четыре, части в два, два и четыре байта. А блоб без единого байта: функция ничего не отправляет.
|
||
|
||
Третий вопрос: направление. По умолчанию функция работает только с пакетами от клиента наружу. Прогон функций: dir равно in на исходящем пакете, функция ничего не вернула и ничего не отправила.
|
||
|
||
Четвёртый: тип нагрузки. Подходят известные типы. Прогон функций с типом unknown: функция молчит, журнал пишет, что unknown не проходит фильтр known.
|
||
|
||
![[wiki-anim-video05-s03-four-gates.svg|Четыре ворота функции]]
|
||
|
||
Фильтров типа два, и их легко спутать. Снаружи опция payload у вызова: по ней движок решает, звать ли функцию вообще, по умолчанию пропуская всё. Внутри аргумент payload: по нему решает сама функция, по умолчанию пропуская только известные типы. В нашем тесте снаружи стоит tls_client_hello: у запроса соединения и подтверждения данных нет, тип «empty», и движок функцию не зовёт. Внутренний фильтр мы смотрели только в прогоне функций.
|
||
|
||
Аргумент payload равно all пускает и тип unknown, но midsld на неизвестных данных не находится, только простые числа. Прогон функций со списком «два, midsld»: части в два и двести сорок восемь байт.
|
||
|
||
![[wiki-anim-video05-s03-gates-runs.svg|Прогон функций через ворота]]
|
||
|
||
Что бывает за закрытыми воротами. Функция ничего не отправляет и не возвращает, молчание движок читает как «пропустить», и оригинал уходит как есть.
|
||
|
||
Есть ещё такой выход. Задан флаг «необязательно», в коде optional, и блоб, которого нет: в прогоне функций журнал пишет «блоб не найден, пропущено», и функция опять ничего не отправляет и не возвращает.
|
||
|
||
Проверь себя. Приветствие уходит от клиента, а в аргументах написано dir равно any. Сколько пакетов отправит функция?
|
||
|
||
Столько же, сколько без этого аргумента: any значит любое направление, ворота открыты, приветствие будет разрезано. Это счёт по коду, именно такой аргумент мы не запускали.
|
||
|
||
Итак, четыре вопроса: TCP ли, какие данные и есть ли они, то ли направление, известный ли тип. Если ворота закрыты, функция ничего не отправляет и ничего не возвращает, оригинал уходит как есть.
|
||
|
||
|
||
## Мост из Lua в Си: что измеряется, что может сломаться, предел меток
|
||
|
||
Вопрос сцены: как слова из pos становятся числами в Lua, что идёт на вход и выход этого моста и что может пойти не так? Сам разбор меток вы видели в выпуске «Где резать: метки», поэтому сначала короткий повтор.
|
||
|
||
На нашем приветствии, считая с нуля: extlen восемьдесят шесть, sniext девяносто два, host девяносто семь, sld сто один, midsld сто четыре, endsld сто восемь, endhost сто двенадцать. Метка method для запроса HTTP на приветствии не находится. К метке можно прибавить поправку: host плюс три даёт сто, endhost минус два сто десять, и в нашем тесте именно эти числа стояли в журнале.
|
||
|
||
Список меток переводится метка за меткой. Не нашедшиеся выпадают без ошибки, остальные сортируются, повторы склеиваются, поэтому порядок меток внутри одного pos не важен. Список «midsld, два, midsld, host, два» дал в нашем тесте два, девяносто семь и сто четыре. «Method, midsld» дал одно число, сто четыре, а один method дал пустой список: «нет допустимых позиций», оригинал уходит целым.
|
||
|
||
Теперь то, чего там не было: сам мост. Игрушка, пример выдуманный. Портной получает ленту ткани и записку: «режь перед воротником, перед карманом». Меряет ту ленту, что дали. Незнакомое слово: не режет, ставит штамп «ошибка». Понятное слово без места на этой ткани: молча вычёркивает.
|
||
|
||
Метки переводит функция, которую назовём мостом: в коде она resolve_multi_pos. Это функция на Си, подключённая к Lua, и Lua зовёт её, как обычную. Почему так, в коде не объяснено; известно лишь, что разбор меток и поиск имени сайта живут на Си.
|
||
|
||
Сигнатура из комментария в коде: данные, тип нагрузки, строка меток и необязательный четвёртый аргумент; всего три или четыре, иначе ошибка. multisplit передаёт три. Возвращает мост таблицу чисел, по числу на каждую найденную метку; данные он только читает, а пакет и таблицу desync не меняет.
|
||
|
||
![[wiki-anim-video05-s04-bridge-code.svg|Мост из Lua в Си]]
|
||
|
||
Первый аргумент, данные: байты, которые собираются резать, а не обязательно пакет. Длинное приветствие программа склеивает из нескольких пакетов и проигрывает заново, об этом выпуск «Сборка и повторный проигрыш»; позиции тогда считаются по всему склеенному приветствию. В нашем тесте оно занимает тысячу четыреста восемьдесят четыре байта.
|
||
|
||
В другом нашем тесте блобом, то есть именованным набором готовых байтов, было встроенное поддельное приветствие в шестьсот восемьдесят байт, и midsld дала сто тридцать три, а не сто четыре: позиция считалась по блобу, части вышли в сто тридцать три и пятьсот сорок семь байт.
|
||
|
||
Настоящее приветствие в двести пятьдесят байт функция выбросила, а резала блоб, поэтому в сеть ушли только куски блоба. Судя по захвату, клиентское ядро об этой подмене не знает: для него подтверждены только первые сто тридцать три байта настоящего приветствия, и позже оно трижды отправило остальные сто семнадцать байт как повтор. Сервер эти повторы в поток не принял: номера этих байтов он уже считал полученными.
|
||
|
||
![[wiki-anim-video05-s04-blob-measures.svg|Измеряется то, что режется]]
|
||
|
||
Второй аргумент, тип нагрузки, идёт по имени. Для tls_client_hello работает разбор приветствия, для http_req разбор запроса HTTP, для остальных типов только простые числа. Прогон функций с типом unknown и списком «два, midsld, host» дал одно число, три: слова выпали. Почему три, а не два, объясним в следующей сцене.
|
||
|
||
Третий аргумент, строка меток, режется по запятым. Пустая строка это ноль меток, не ошибка. Заметьте разницу с умолчанием: слово or в Lua берёт правую часть, двойку, только когда слева «ничего», то есть pos не написан вовсе. Пустая строка в Lua это значение. Поэтому в нашем тесте pos без значения дал не двойку, а пустой список, «нет допустимых позиций», и оригинал ушёл целым.
|
||
|
||
Меток не больше ста двадцати восьми: в прогоне функций сто двадцать восемь прошли, сто двадцать девять дали «плохой список меток».
|
||
|
||
Теперь ошибки. В мосту предусмотрены три собственные: неверное число аргументов, неверное имя типа, неверный список меток. Есть и четвёртый источник: аргументы обязаны быть строками, иначе будет ошибка типа.
|
||
|
||
Но из multisplit достижима только ошибка списка меток, это вывод из кода: аргументов у неё всегда три, имя типа берётся у самого пакета, а аргументы это строки. Хватит одной неизвестной метки, и отказывает весь список: движок ловит ошибку, пишет в журнал и отпускает пакет.
|
||
|
||
В нашем тесте с pos равно foo журнал пишет «плохой список меток», потом «пропускаем пакет без изменений», и на проводе один пакет в двести пятьдесят байт.
|
||
|
||
Проверь себя. В списке написано «midsld, foo». Что получится?
|
||
|
||
Ошибка «плохой список меток»: отказывает весь список, и midsld тоже не используется. Оригинал уходит целым. Это вывод по коду: такой список мы не запускали, а один foo дал ровно это.
|
||
|
||
Итак. Мост принимает данные, имя типа и строку меток и возвращает таблицу чисел. Измеряется то, что режется. Плохой список меток останавливает функцию и отпускает пакет, а метка без места молча выпадает.
|
||
|
||
|
||
## Плюс один, журнал минус один и вычёркивание единицы
|
||
|
||
Вопрос сцены: почему в журнале стоит сто четыре, а цикл дальше работает со сто пятью, и что стратегия делает с единицей? Сдвиг на единицу вы видели в выпуске «Где резать: метки», здесь только строки кода и то, чего там не было.
|
||
|
||
Напомним суть. Си считает байты с нуля, а Lua, по общим сведениям, с единицы. Поэтому мост кладёт в таблицу каждое число плюс один: midsld становится сто пять, а список «два, host, midsld, endhost» становится «три, девяносто восемь, сто пять, сто тринадцать». Смещение с нуля это число байт слева от разреза, а позиция для Lua это номер первого байта новой части.
|
||
|
||
Теперь код моста, две строки. Первая создаёт пустую таблицу. Вторая это цикл по найденным числам: для каждого в таблицу кладётся число плюс один, в ячейку, номер которой на единицу больше номера числа в массиве Си.
|
||
|
||
Плюсов единицы здесь два, и их легко спутать. Первый выбирает номер ячейки: по общим сведениям, первая ячейка таблицы в Lua имеет номер один. Второй сдвигает само число, и его отключает четвёртый аргумент моста. multisplit его не передаёт, поэтому для неё сдвиг числа включён.
|
||
|
||
Зачем нужен сдвиг числа, в коде не сказано. Судя по коду, ради string.sub: функция, которая вырезает кусок строки, берёт номера с единицы, и именно ей позже отдадут эти числа.
|
||
|
||
![[wiki-anim-video05-s05-two-rulers.svg|Два счёта байтов]]
|
||
|
||
Дальше две маленькие функции. Игрушка, пример выдуманный. Закладки в книге. Закладка перед самой первой страницей ничего не делит, и редактор её вынимает. А в журнал архивариус пишет номера страниц по-архивному, с нуля. Книга одна, счёта два.
|
||
|
||
В коде так же. Стратегия печатает список в журнал, вычитая единицу обратно, поэтому в журнале числа Си. Потом вычёркивает из списка единицу: Lua-позиция один это разрез перед нулевым байтом, первая часть вышла бы пустой. Журнал печатается до вычёркивания, поэтому pos равно ноль оставляет в журнале ноль, а список после вычёркивания пуст: «нет допустимых позиций», и оригинал уходит целым.
|
||
|
||
Первая функция, вычитатель единицы, в коде zero_based_pos. Она получает массив, создаёт новый, и в новый кладёт каждое число минус один; всё, что не число, копирует как есть. Исходный массив она не трогает, поэтому цикл дальше работает с прежним, со сто пятью. Прогон функций: из «сто пять, два» получилось «сто четыре, один».
|
||
|
||
Вторая, вычёркиватель единицы, в коде delete, подчёркивание, pos, подчёркивание, один. Курсор идёт по массиву. Если число равно единице, вызывается стандартная команда Lua «убрать элемент», в коде table.remove: остальные сдвигаются влево, а курсор остаётся на месте, чтобы не пропустить того, кто встал на освободившееся место. Если не единица, курсор шагает вперёд. Прогон функций: из «один, пять, один, девять» вышло «пять, девять».
|
||
|
||
![[wiki-anim-video05-s05-two-helpers.svg|Минус один и вычёркивание]]
|
||
|
||
Проверь себя. Что напишет журнал и что уйдёт в сеть при pos равно «ноль, ноль»?
|
||
|
||
Переводчик склеит два нуля в один, поэтому журнал напишет один ноль. Для Lua это единица, её вычёркивают, массив пуст. Функция напишет «нет допустимых позиций», частей не будет, оригинал уйдёт целым. Это вывод по коду: именно два нуля мы не запускали, а один ноль в нашем тесте дал ровно это.
|
||
|
||
Итак. Мост кладёт в таблицу каждое число плюс один, потому что Lua считает с единицы; четвёртый аргумент моста этот сдвиг отключает, но multisplit им не пользуется. Вычитатель единицы работает только на журнал, вычёркиватель убирает разрез перед первым байтом, и делает это после печати журнала. Если массив опустел, это не ошибка, а тихий выход: оригинал уходит как есть.
|
||
|
||
|
||
## Что мы знаем к этому месту: от строки до готового списка позиций
|
||
|
||
Соберём всё, что выяснили в этой части. Одна строка пройдёт всю цепочку.
|
||
|
||
Первое, строка стратегии. Имя multisplit, аргумент pos со списком меток, значение приходит в Lua текстом. Без pos берётся «два», а если pos записан дважды, побеждает первый.
|
||
|
||
Второе, ворота. Это TCP? Какие данные: блоб, сборка или пакет, и не пусты ли они? Подходит ли направление, по умолчанию наружу? Известен ли тип? На любое «нет» функция ничего не отправляет, и оригинал уходит как есть.
|
||
|
||
Третье, мост. Данные, имя типа и строка меток уходят в Си. Там метки разбираются, переводятся в смещения с нуля, ненайденные выбрасываются, остальные сортируются, повторы склеиваются. Плохой список меток это ошибка, и оригинал тоже уходит целым.
|
||
|
||
Четвёртое, сдвиг. Для Lua каждое число получает плюс один, а в журнал оно идёт с минус один, поэтому журнал говорит так же, как Си.
|
||
|
||
Пятое, вычёркивание единицы. Разрез перед первым байтом убирается, а если список опустел, функция пишет «нет допустимых позиций», не отправляет ни одного пакета и ничего не меняет.
|
||
|
||
До отправки частей у списка меток четыре основных исхода. Ворота закрыты: ничего не отправлено, оригинал уходит. Плохой список меток: ошибка в журнале, оригинал уходит. Метки не нашлись: пустой список, «нет допустимых позиций», оригинал уходит. Всё нашлось: готовый список, а оригинал будет выброшен после отправки частей. Ошибку может дать и другой аргумент, например неверное значение накладки, а ещё сама отправка: это разберём в следующих частях.
|
||
|
||
![[wiki-anim-video05-s06-chain-blocks.svg|Цепочка от строки до списка позиций]]
|
||
|
||
Пройдём это на одном примере. Список «host плюс три, endhost минус два». Ворота пройдены, данные это пакет в двести пятьдесят байт. Си возвращает сто и сто десять. Lua получает сто один и сто одиннадцать. Журнал показывает сто и сто десять. Вычёркивать нечего. Список готов. В нашем тесте с таким списком уходят три части.
|
||
|
||
Второй пример, из нашего опыта: midsld, два, midsld, host, два. Си возвращает два, девяносто семь и сто четыре, повторы склеены. Lua получает три, девяносто восемь и сто пять. Журнал показывает два, девяносто семь, сто четыре. Единицы нет. Список готов: три разреза, а значит четыре части.
|
||
|
||
Проверь себя на всей цепочке. Список «host минус сто». Какой из этих четырёх исходов получится?
|
||
|
||
Host даёт девяносто семь, минус сто даёт минус три. Смещение меньше нуля, метка не нашлась и выпадает. Список пуст: «нет допустимых позиций», оригинал уходит. Это третий исход. Счёт по коду, именно такую метку мы не запускали.
|
||
|
||
![[wiki-anim-video05-s06-two-examples.svg|Два примера и проверка на бумаге]]
|
||
|
||
Какие у частей будут размеры, мы разберём в следующей части: там цикл, который режет данные по этому списку. Пока у нас только числа: ни один новый пакет ещё не отправлен. Цикл начнёт с позиций, которые мы только что получили.
|
||
|
||
Итак. Строка превращается в список позиций за пять шагов: аргумент, ворота, мост, сдвиг на единицу, вычёркивание. Если ворота закрыты, список плох или пуст, оригинал уходит целым. Если всё хорошо, у нас на руках готовый список позиций, по которому будут резать.
|
||
|
||
|
||
## Повтор и вопрос: от одного числа к пакетам на проводе
|
||
|
||
Это вторая часть выпуска «multisplit: резать на части». Её можно смотреть отдельно от остальных, поэтому начнём с короткого повтора.
|
||
|
||
Сразу договоримся об одном. О настоящем оборудовании на пути пакетов в этом выпуске нет ни слова: мы не знаем, как оно устроено. Мы читаем код программы zapret2 и смотрим на опыты в нашей лаборатории. В лаборатории есть клиент, роутер и сервер, а настоящего ТСПУ там нет.
|
||
|
||
Напомним и обстановку. Ядро отдаёт программе копию пакета через очередь, а оригинал держит у себя и ждёт вердикта, то есть ответа: пропустить, изменить или выбросить. Пакеты, которые программа создаёт сама, она отправляет в сеть через сырой сокет, и в очередь они не попадают. Как это устроено, показывает выпуск «Внутри программы».
|
||
|
||
Первое. Стратегия multisplit это функция на Lua. Программа вызывает её для пакета, и функция может не пропустить пакет целиком, а разрезать данные на части и отправить части по очереди. В нашем опыте режется приветствие ClientHello на двести пятьдесят байт, третий пакет клиента.
|
||
|
||
Второе, из выпуска «Где резать: метки». Где резать, говорит список меток, через запятую. Метка это разрез перед определённым байтом. Одни метки просто числа, другие имена. Имя host это начало имени сайта, midsld это середина слова example. На нашем приветствии, считая с нуля, host это девяносто седьмой байт, а midsld сто четвёртый. Ещё два имени: sld, начало слова example, сто первый байт, и endhost, место сразу за именем сайта, сто двенадцатый.
|
||
|
||
Третье, из первой части. Метки в числа превращает функция на языке Си. Что не нашлось, она тихо выбрасывает, остальное сортирует и убирает повторы.
|
||
|
||
В Lua каждое число приходит с прибавленной единицей, потому что Lua считает с единицы. Потом стратегия выбрасывает позицию один: разрез перед самым первым байтом не нужен. Для midsld остаётся одно число, сто пять.
|
||
|
||
И результат, который мы уже видели. Приветствие уходит двумя пакетами, в одном сто четыре байта, в другом сто сорок шесть, а исходный пакет программа выбрасывает. Числа этого опыта нам пригодятся дальше.
|
||
|
||
![[wiki-anim-video05-s07-recap-marks-result.svg|Повтор: от метки до двух пакетов]]
|
||
|
||
Но между числом «сто пять» и двумя пакетами на проводе пока чёрный ящик. Эта часть его открывает, в четыре шага. Шаг первый: как цикл превращает список позиций в части. Шаг второй: как каждая часть становится настоящим пакетом с правильным порядковым номером; на деле эти два шага идут в одном цикле. Шаг третий: что будет, если часть не влезает в один пакет. Шаг четвёртый: в каком порядке и с какой паузой части уходят. Каждый шаг мы проверим по числам нашего теста.
|
||
|
||
![[wiki-anim-video05-s07-black-box.svg|Чёрный ящик и четыре шага]]
|
||
|
||
Проверь себя по повтору. В строке написано просто endhost. Какое число придёт в Lua, сколько будет частей и какого размера?
|
||
|
||
Endhost это сто двенадцатый байт, для Lua сто тринадцать. Один разрез, две части: сто двенадцать байт и двести пятьдесят минус сто двенадцать, то есть сто тридцать восемь. Это расчёт. Этот список в лаборатории шёл только с игрушечным правилом на роутере, о нём в последней части; чистым опытом мы его не запускали.
|
||
|
||
Итак, мы знаем, как строка с метками становится списком позиций. Теперь выясним, как список становится частями, а части пакетами.
|
||
|
||
|
||
## Цикл по частям: четыре строки, которые режут данные
|
||
|
||
Вопрос этой сцены: у нас на руках список чисел для Lua, например одно число, сто пять. Как из него получаются части данных?
|
||
|
||
Игрушка, выдуманная. Бумажная лента из десяти клеток, пронумерованных с единицы. Список разрезов: четыре и восемь. Разрез четыре значит: здесь начинается новая часть. Тогда первая часть это клетки с первой по третью, вторая с четвёртой по седьмую, третья с восьмой по десятую.
|
||
|
||
Правило простое. Часть начинается там, где стоит её разрез, и кончается перед следующим разрезом. У первой части своего разреза нет, она начинается с начала ленты. У последней следующего разреза нет, она идёт до конца ленты. Два разреза, три части.
|
||
|
||
![[wiki-anim-video05-s08-toy-ten-cells.svg|Игрушка: лента из десяти клеток]]
|
||
|
||
Теперь настоящий код. Это первые четыре строки цикла внутри multisplit, они режут данные; журнал и отправку разберём дальше. Перед циклом у нас две вещи: данные, наши двести пятьдесят байт, и список позиций. Для midsld в нём одно число, сто пять.
|
||
|
||
Первая строка. Цикл «для счётчика от нуля до числа разрезов» (в коде счётчик это буква i). Число разрезов это длина списка, у нас один. Значит, счётчик примет значения ноль и один, цикл сработает два раза, по разу на каждую часть. Почему счёт идёт с нуля, видно из второй строки.
|
||
|
||
Вторая строка: начало части. Берётся элемент списка с номером счётчика, а если его нет, то единица. Нулевой проход это часть до первого разреза: элемента с номером ноль в списке нет, и начало равно единице, самому первому байту. Для второго прохода берём первый элемент, сто пять.
|
||
|
||
Третья строка: конец части. Если эта часть не последняя, конец равен следующему разрезу минус один. Иначе конец равен длине данных. На первом проходе следующий разрез сто пять, минус один, получается сто четыре. На втором проходе часть последняя, и конец это двести пятьдесят.
|
||
|
||
Четвёртая строка: вырезать. Назовём «вырезалкой» функцию string.sub: она берёт строку и два номера, начало и конец, и возвращает кусок, включая оба края. Для первой части от первого до сто четвёртого: сто четыре байта. Для второй от сто пятого до двести пятидесятого: двести пятьдесят минус сто пять плюс один, сто сорок шесть. Сто четыре и сто сорок шесть дают ровно двести пятьдесят.
|
||
|
||
![[wiki-anim-video05-s08-loop-four-lines.svg|Цикл по частям: четыре строки]]
|
||
|
||
Журнал в нашем тесте пишет те же части, но по-своему: нумерует байты с нуля и показывает оба края. Первая часть с нулевого по сто третий, длина сто четыре; вторая со сто четвёртого по двести сорок девятый, длина сто сорок шесть. Эта строка журнала стоит в том же цикле, чуть ниже вырезалки, а за ней идёт отправка.
|
||
|
||
Почему пустых частей нет? Позиции строго возрастают, не меньше двух и не больше длины данных, значит, у каждой части хотя бы один байт. Это вывод из кода; опыты на краях будут через две сцены.
|
||
|
||
Проверь себя. В списке, который пришёл в Lua, одно число, двести пятьдесят, данные те же, двести пятьдесят байт. Какие получатся части?
|
||
|
||
На первом проходе начало единица, конец двести пятьдесят минус один, двести сорок девять: часть в двести сорок девять байт. На втором начало двести пятьдесят и конец двести пятьдесят: один байт. В нашем тесте с меткой «двести сорок девять» сервер получил именно двести сорок девять байт и потом один.
|
||
|
||
Итак, цикл идёт от нуля до числа разрезов. Начало части это её разрез, а у первой единица. Конец это следующий разрез минус один, а у последней длина данных. Разрезов на один меньше, чем частей, и части в сумме дают исходные данные.
|
||
|
||
|
||
## Пять проходов цикла: части, номера и проверка на бумаге
|
||
|
||
Вопрос этой сцены: как цикл ведёт себя на длинном списке? Возьмём тест с пятью частями. Метки в нём такие: два, host, midsld, endhost.
|
||
|
||
Журнал печатает позиции так, как их считает Си, с нуля: два, девяносто семь, сто четыре и сто двенадцать.
|
||
|
||
В Lua каждое число на единицу больше: три, девяносто восемь, сто пять и сто тринадцать. Четыре разреза, пять частей, цикл сработает пять раз, счётчик от нуля до четырёх. Пройдём проходы по одному.
|
||
|
||
Первый проход, счётчик равен нулю. Элемента с номером ноль нет, начало единица. Следующий разрез три, конец два. Вырезаем с первого по второй байт: два байта.
|
||
|
||
Второй проход, счётчик равен одному. Начало это первый элемент списка, три. Конец это второй элемент минус один: девяносто восемь минус один, девяносто семь. Девяносто семь минус три плюс один, девяносто пять байт.
|
||
|
||
Третий проход, счётчик равен двум. Начало девяносто восемь. Конец сто пять минус один, сто четыре. Семь байт. Это «www точка exa», начало имени сайта.
|
||
|
||
Четвёртый проход. Начало сто пять, конец сто тринадцать минус один, сто двенадцать. Восемь байт, «mple точка com».
|
||
|
||
Пятый проход, счётчик равен четырём. Это последняя часть: следующего разреза нет, и конец равен длине данных, двести пятьдесят. Начало сто тринадцать. Двести пятьдесят минус сто тринадцать плюс один, сто тридцать восемь байт.
|
||
|
||
Складываем: два, девяносто пять, семь, восемь и сто тридцать восемь, получается двести пятьдесят. Ни один байт не потерян и не взят дважды.
|
||
|
||
![[wiki-anim-video05-s09-five-passes.svg|Пять проходов цикла по частям]]
|
||
|
||
Теперь что было на проводе. В нашем тесте от клиента ушло пять пакетов с данными, длиной два, девяносто пять, семь, восемь и сто тридцать восемь, как в расчёте. Сервер собрал из них исходные двести пятьдесят байт.
|
||
|
||
Помним договорённость из выпуска «Что видит наблюдатель»: номера считаем от начального номера, SYN получает номер ноль, а первый байт данных номер один.
|
||
|
||
Теперь порядковые номера, относительные: первый байт данных это номер один. Первый пакет: номер один, длина два, и следующий номер три.
|
||
|
||
Второй: номер три, длина девяносто пять, дальше девяносто восемь.
|
||
|
||
Третий: номер девяносто восемь, длина семь, дальше сто пять.
|
||
|
||
Четвёртый: номер сто пять, длина восемь, дальше сто тринадцать.
|
||
|
||
Пятый: номер сто тринадцать, длина сто тридцать восемь. Сумма двести пятьдесят один: следующий байт после всех данных, и подтверждение с этим номером в нашем тесте пришло.
|
||
|
||
Заметьте: номер каждого пакета совпал с позицией Lua, с которой начинается его часть. Так вышло потому, что приветствие это первые данные соединения, и его первый байт имеет номер один. В общем случае номер равен номеру исходного пакета плюс позиция минус один.
|
||
|
||
![[wiki-anim-video05-s09-wire-numbers.svg|Номера пакетов на проводе]]
|
||
|
||
Проверь себя на бумаге. Метки «sld, endsld». Мы знаем, что sld это сто первый байт, а endsld сто восьмой, считая с нуля. Какие получатся части и какие номера пакетов?
|
||
|
||
Для Lua позиции сто два и сто девять. Две позиции, три части. Первая: сто один байт. Вторая: со сто второго по сто восьмой, семь байт, и это ровно слово «example». Третья: со сто девятого до конца, двести пятьдесят минус сто девять плюс один, сто сорок два. Номера пакетов: один, сто два и сто девять. Это расчёт: именно такой список в лаборатории не запускали.
|
||
|
||
Итак, на каждом проходе цикл берёт начало из списка, конец вычисляет по следующему разрезу и вырезает часть. Номер пакета равен позиции, с которой часть начинается.
|
||
|
||
|
||
## Края, много разрезов и запрос HTTP: от одного байта до двадцати пяти частей
|
||
|
||
Вопрос этой сцены: что происходит у самого края данных, сколько частей можно получить и как это выглядит на другом протоколе?
|
||
|
||
Края данных вы видели в выпуске «Где резать: метки», вспомним их вместе с проводом. Метка «один» даёт части в один байт и в двести сорок девять, номера на проводе один и два. Метка «двести сорок девять» наоборот: двести сорок девять байт и один, номера один и двести пятьдесят. Минус десять считается от конца: двести сорок байт и десять, номера один и двести сорок один. Для цикла края ничего не значат: пустых частей не бывает, а коротких бывает сколько угодно.
|
||
|
||
Игрушка, выдуманная. Батон режут на куски: двадцать четыре надреза дают двадцать пять кусков. Всегда на один больше.
|
||
|
||
Семь имён подряд, host, sld, midsld, endsld, endhost, sniext и extlen, мы тоже видели в том выпуске: журнал показывает семь чисел по возрастанию, на проводе восемь пакетов, самый маленький в три байта, и сервер нашей лаборатории принял их все. Новое здесь только то, что цикл не замечает, мало разрезов или много.
|
||
|
||
![[wiki-anim-video05-s10-edges-names.svg|Края данных и семь имён подряд]]
|
||
|
||
Теперь опыт «много частей». Метки: десять, двадцать, и так далее до двухсот сорока, всего двадцать четыре числа. Двадцать четыре разреза дают двадцать пять частей, и все по десять байт, потому что двадцать пять раз по десять это двести пятьдесят. На проводе двадцать пять пакетов, и сервер прочитал двадцать пять раз по десять байт. Это поведение одного сервера, Linux; как поведёт себя чужая сторона, мы не проверяли.
|
||
|
||
Есть ли предел? В списке может быть не больше ста двадцати восьми меток: в прогоне функций сто двадцать восемь работают, а сто двадцать девятая даёт ошибку «плохой список меток». Значит, частей не больше ста двадцати девяти; это расчёт, в сети мы столько не пробовали.
|
||
|
||
![[wiki-anim-video05-s10-many-parts-limit.svg|Много частей и предел меток]]
|
||
|
||
Те же метки на другом протоколе. Запрос HTTP на восемьдесят один байт: первая строка читается так: «гет, слэш, индекс точка эйч-ти-эм-эль, эйч-ти-ти-пи один точка один». Дальше строка Host с именем «www точка example точка com». Метки «host, midsld». Считая с нуля, host это тридцать второй байт, midsld тридцать девятый. Три части: тридцать два байта, семь и сорок два.
|
||
|
||
Куда упали разрезы. Первый сразу после слова «Host», двоеточия и пробела, перед именем. Второй внутри слова example, как и в приветствии: вторая часть «www точка exa», третья начинается с «mple точка com». На проводе и у сервера в нашем тесте именно тридцать два, семь и сорок два.
|
||
|
||
![[wiki-anim-video05-s10-http-three-parts.svg|Те же метки на запросе HTTP]]
|
||
|
||
Проверь себя. Метки: десять, двадцать, и так далее до ста, десять меток. Сколько частей и какая последняя?
|
||
|
||
Десять разрезов, одиннадцать частей. Первые десять по десять байт, а последняя двести пятьдесят минус сто, сто пятьдесят байт.
|
||
|
||
Итак, цикл не знает, много разрезов или мало, и края для него ничего не значат: он просто идёт по списку. Разрезов на один меньше, чем частей, пустых частей нет, в списке не больше ста двадцати восьми меток, а те же метки работают и на запросе HTTP.
|
||
|
||
|
||
## Что мы знаем: от строки pos до списка частей
|
||
|
||
Соберём то, что знаем к этой минуте, и пройдём цепочку целиком.
|
||
|
||
Вот строка с метками из командной строки. Если позиции не заданы, программа берёт «два». Эта строка идёт в функцию на Си. Та переводит каждую метку в число, считая с нуля, выбрасывает не нашедшиеся и те, что выходят за данные, сортирует и убирает повторы.
|
||
|
||
Дальше каждое число получает плюс единицу: так положено для Lua. Позицию один стратегия выбрасывает. И цикл из прошлых сцен превращает оставшиеся позиции в части: на одну больше, чем разрезов.
|
||
|
||
![[wiki-anim-video05-s11-chain-links.svg|Цепочка до частей]]
|
||
|
||
Три примера подряд. Строка «midsld»: Си даёт сто четыре, Lua сто пять, части сто четыре и сто сорок шесть. Строка «два, host, midsld, endhost»: Си даёт два, девяносто семь, сто четыре, сто двенадцать, Lua три, девяносто восемь, сто пять, сто тринадцать, части два, девяносто пять, семь, восемь и сто тридцать восемь. Номера на проводе в нашем тесте: у первой строки один и сто пять, у второй один, три, девяносто восемь, сто пять и сто тринадцать.
|
||
|
||
Проверь себя на целой цепочке. Строка «host, host, один, midsld». Что напечатает журнал, что придёт в Lua и какие части уйдут в сеть?
|
||
|
||
Повтор пропадает, метки сортируются. Журнал: один, девяносто семь, сто четыре. В Lua два, девяносто восемь, сто пять. Метку «один» не выбросили: это Lua-позиция два, а выбрасывается только позиция один. Три разреза, четыре части: один байт, девяносто шесть, семь и сто сорок шесть. Номера на проводе: один, два, девяносто восемь и сто пять. Это расчёт: такой список в лаборатории не запускали.
|
||
|
||
Тот же путь на запросе HTTP из восьмидесяти одного байта. Метки «host, midsld». Си даёт тридцать два и тридцать девять, Lua по правилу «плюс один» тридцать три и сорок, части тридцать два, семь и сорок два. Путь тот же, меняются только числа: какие правила применять к меткам, выбирает тип нагрузки. На проводе в нашем тесте номера один, тридцать три и сорок.
|
||
|
||
![[wiki-anim-video05-s11-chain-examples.svg|Четыре списка в цепочке]]
|
||
|
||
Что цепочка гарантирует к этому месту. Позиции идут по возрастанию, без повторов, каждая внутри данных и ни одна не стоит перед первым байтом. Поэтому в каждой части хотя бы один байт, а все части вместе равны исходным данным. Это вывод из кода, и наши опыты его подтвердили.
|
||
|
||
Есть и ветка, где разрезов не осталось. Тогда стратегия ничего не меняет, и пакет идёт как был. Её мы разберём в последней части.
|
||
|
||
Где мы сейчас. В памяти программы лежат две вещи: исходные данные и список позиций. Готовых частей пока нет: цикл вырезает их по одной и каждую сразу отправляет, а исходный пакет ждёт вердикта. Остаётся главное: как часть превращается в пакет, который наш сервер примет за часть того же соединения.
|
||
|
||
Итак, цепочка такая: строка, числа от Си, позиции Lua, части. На нашей растущей схеме горят звенья от строки до цикла. Каждое мы видели в работе, и каждое согласуется с проводом. Остаются звенья отправки.
|
||
|
||
|
||
## Отправщик части: копия пакета, новые данные, сдвиг номера
|
||
|
||
Вопрос этой сцены: у нас есть часть данных, например первые сто четыре байта. Как сделать из неё пакет, который наш сервер примет за часть того же соединения?
|
||
|
||
Игрушка, выдуманная. Конверт-шаблон. На конверте адрес отправителя и получателя, штампы и надпись «письмо внутри начинается со страницы такой-то». Чтобы отправить часть письма, берут копию конверта, кладут в неё эту часть и исправляют одно: с какой страницы письмо начинается. Адреса и штампы остаются прежними.
|
||
|
||
![[wiki-anim-video05-s12-envelope-fields.svg|Конверт и поля пакета]]
|
||
|
||
Эту работу делает короткая функция, назовём её «отправщик части». В коде она называется rawsend_payload_segmented. Название длинное, а работа маленькая: функция занимает девять строк. Зовёт её не только multisplit: в библиотеке стратегий таких вызовов двадцать пять.
|
||
|
||
Ей передают четыре вещи. Таблицу desync, в ней лежит текущий пакет, разобранный на поля; такие таблицы строит выпуск «От байтов к таблице». Саму часть данных. Сдвиг: число, которое прибавят к порядковому номеру. И необязательные настройки.
|
||
|
||
Первая строка решает, делать ли копию. Копия нужна, если дали новую часть данных или ненулевой сдвиг: поля будут меняться, а исходную таблицу портить нельзя. Если не дали ни того, ни другого, функция работает с самим пакетом и практически ничего в нём не меняет. Комментарий в коде объясняет цель: сэкономить процессор и память.
|
||
|
||
Вторая строка: если часть передана, она ставится в копию на место прежних данных. Всё остальное в копии прежнее: адреса, порты, флаги, окно, номер подтверждения, параметры TCP.
|
||
|
||
Следующие три строки работают только для TCP. Если сдвиг задан, порядковый номер копии увеличивается на сдвиг. Это простое сложение, без каких-либо хитростей. Для нашей второй части: сто сорок шесть байт данных, сдвиг сто четыре, номер исходного пакета плюс сто четыре.
|
||
|
||
Последняя строка передаёт копию дальше, функции с нарезкой. Размер порции ей не называют, она возьмёт его сама. Об этом через две сцены.
|
||
|
||
Итак, меняются данные и порядковый номер, чуть позже добавится идентификатор IP. Адреса, порты, флаги и опции берутся от обрабатываемого пакета.
|
||
|
||
![[wiki-anim-video05-s12-sender-walk.svg|Отправщик части: по строкам]]
|
||
|
||
Прогон функций: настоящая библиотека, а отправка заменена записью. Отправим три байта, буквы «эй, би, си», со сдвигом семь. Уходит один пакет, три байта, порядковый номер на семь больше, чем у исходного. Позовём без данных и без сдвига: уходит повтор исходного пакета, все двести пятьдесят байт. И после вызовов исходная таблица цела: двести пятьдесят байт и тот же номер.
|
||
|
||
Функция возвращает «получилось» или «не получилось». Если отправка не удалась, multisplit тут же возвращает вердикт «пропустить»: исходный пакет пойдёт дальше целым, а уже отправленные части останутся на проводе. Это мы видели только в прогоне функций; настоящий отказ сокета в сети не воспроизводили.
|
||
|
||
Проверь себя. Для второй части, от сто пятого байта, какой сдвиг получит отправщик?
|
||
|
||
Сдвиг равен числу байт, которые уже ушли: сто пять минус один, сто четыре. У первой части сдвиг ноль. При вызове из сдвига вычитается ещё накладка seqovl; пока она ноль, о ней в следующей части.
|
||
|
||
Итак, отправщик части берёт копию пакета, кладёт в неё часть и сдвигает порядковый номер. Всё остальное остаётся от исходного пакета, а сам исходный пакет не меняется.
|
||
|
||
|
||
## Номера на проводе: от начального номера до подтверждения сервера
|
||
|
||
Вопрос этой сцены: какие номера стоят в пакетах частей, и как на них отвечает сервер? Разберём запись одного прогона с одним разрезом, на метке midsld.
|
||
|
||
Игрушка, выдуманная. Письмо на двести пятьдесят страниц. Первая пачка листов: страницы с первой по сто четвёртую. Следующая пачка начинается со страницы сто пять, то есть с «один плюс сто четыре». Получатель отвечает: «жду страницу сто пять».
|
||
|
||
Теперь то же самое с настоящими пакетами. Относительные номера: номер начала соединения клиента принят за ноль, первый байт данных это номер один. Первая часть: номер один, длина сто четыре. В записи сервер ответил подтверждением «жду со сто пятого».
|
||
|
||
Вторая часть: номер сто пять, длина сто сорок шесть. Сто пять это один плюс сто четыре. А после неё в записи сервер подтвердил двести пятьдесят один, потому что один плюс двести пятьдесят. Приветствие получено целиком.
|
||
|
||
Теперь настоящие номера из того же теста. Они длинные, и поэтому договоримся так. У всех трёх чисел, которые нам нужны, общее начало: один миллиард сто девяносто шесть миллионов шестьсот сорок восемь тысяч. Дальше меняются только последние три цифры.
|
||
|
||
У исходного пакета они семьсот семь. Первая часть ушла с тем же номером. Вторая часть: семьсот семь плюс сто четыре, восемьсот одиннадцать. Подтверждение сервера: восемьсот одиннадцать плюс сто сорок шесть, девятьсот пятьдесят семь.
|
||
|
||
Проверим другим путём. Семьсот семь плюс все двести пятьдесят байт тоже дают девятьсот пятьдесят семь.
|
||
|
||
![[wiki-anim-video05-s13-numbers-wire.svg|Номера частей на проводе]]
|
||
|
||
Теперь размер пакета: двадцать байт заголовок IP, заголовок TCP и данные. Заголовок TCP в нашем тесте тридцать два байта; из чего они, увидим в сцене про ворота размера. Двадцать плюс тридцать два плюс сто четыре, сто пятьдесят шесть. Двадцать плюс тридцать два плюс сто сорок шесть, сто девяносто восемь. Именно эти два размера стоят в журнале нашего теста.
|
||
|
||
По общим сведениям, порядковый номер TCP занимает тридцать два бита и устроен как минутная стрелка: после самого большого значения счёт идёт с нуля. В Lua сумма может выйти за край шкалы; лишнее отбрасывает код на Си, когда собирает пакет.
|
||
|
||
Прогон функций, номер исходного пакета задан вручную: на шестнадцать меньше края шкалы. Прибавляем сто четыре: шестнадцать шагов до нуля и ещё восемьдесят восемь после него. В собранном пакете стоит число восемьдесят восемь. Это проверено только в прогоне функций, в живом опыте такого не было.
|
||
|
||
![[wiki-anim-video05-s13-size-wrap.svg|Размер и край шкалы]]
|
||
|
||
Проверь себя на бумаге. Метки «host» и «endhost»: части в девяносто семь, пятнадцать и сто тридцать восемь байт. У первого пакета хвост номера семьсот семь. Какой хвост у второго и третьего и какое подтверждение придёт в конце?
|
||
|
||
Семьсот семь плюс девяносто семь, восемьсот четыре. Плюс пятнадцать, восемьсот девятнадцать. И плюс сто тридцать восемь, девятьсот пятьдесят семь: то же, что семьсот семь плюс двести пятьдесят. Это расчёт: такой список в лаборатории не запускали.
|
||
|
||
Итак, номер каждой части равен номеру предыдущей части плюс длина предыдущей части, а по общим сведениям подтверждение сервера называет номер следующего ожидаемого байта; в нашем тесте так и было.
|
||
|
||
|
||
## Ворота размера: порция, опции и максимум данных в пакете
|
||
|
||
Вопрос этой сцены: что делает функция с нарезкой, прежде чем отправить часть, и когда она решает, что часть надо резать ещё раз?
|
||
|
||
Договоримся о слове «порция»: так мы называем наибольшее число байт данных в одном пакете. По общим сведениям, каждый конец объявляет в SYN своё число, оно называется MSS, а отправитель берёт меньшее. Для нас это порция: тысяча четыреста шестьдесят плюс сорок байт заголовков IP и TCP дают привычные тысячу пятьсот.
|
||
|
||
В нашей лаборатории большое приветствие шло двумя пакетами, тысяча четыреста сорок восемь байт и тридцать шесть.
|
||
|
||
Игрушка, выдуманная. Коробка на десять мест. Две из них занимает упаковка, под товар остаётся восемь. Товара двадцать штук: значит, три коробки, восемь, восемь и четыре.
|
||
|
||
Теперь вторая функция, назовём её «отправщик с нарезкой»; в коде она называется rawsend_dissect_segmented. Сначала она делает ещё одну, рабочую копию пакета. Если настройки не переданы, берёт их из аргументов строки стратегии: всё, что вы написали после двоеточия и относится к пакетам, действует на каждый пакет, который эта функция отправляет.
|
||
|
||
Потом один раз на каждую часть, ещё до проверки размера, применяются подделки. Подделки это та самая подмена полей из карточки функции в начале выпуска. Что это, расскажет выпуск «Фейк и его условия», а ловушку с ними покажем в последней части; пока запомним порядок: сначала подделки, потом размер.
|
||
|
||
Дальше, для TCP, берётся рекомендуемый размер порции, поле tcp_mss. Его для каждого пакета считает программа на Си: если известны числа обоих концов из SYN (программа их запоминает; как, разбирает выпуск «Память о соединении»), берётся меньшее, иначе запасное, тысяча двести двадцать. В нашей лаборатории оба конца назвали тысячу четыреста шестьдесят.
|
||
|
||
Дальше считается, сколько места уже занято опциями: опции TCP, округлённые до кратного четырём, и опции IP, если они есть. В нашем тесте две пустышки по байту и метка времени в десять байт, итого двенадцать; с двадцатью обязательными это те тридцать два байта заголовка TCP.
|
||
|
||
Теперь максимум данных: размер порции минус эти лишние байты. Тысяча четыреста шестьдесят минус двенадцать, тысяча четыреста сорок восемь. Это ровно тот размер, который был у полных пакетов в нашей лаборатории.
|
||
|
||
![[wiki-anim-video05-s14-box-mss-options.svg|Порция и максимум данных]]
|
||
|
||
Оговорка в коде: если опции занимают столько же места, сколько вся порция, или больше, функция возвращает «не получилось» и ничего не отправляет. Прогон функций: порция двенадцать, опций тоже двенадцать. Ни одного пакета, и multisplit возвращает «пропустить»: исходный пакет идёт целым.
|
||
|
||
И обратная крайность. Порция тринадцать: максимум данных один байт. Те же две части уходят двумястами пятьюдесятью пакетами по одному байту. Порция сто даёт максимум восемьдесят восемь. По общим сведениям, такие маленькие порции в настоящей сети маловероятны; это проверка кода.
|
||
|
||
![[wiki-anim-video05-s14-gate-fork.svg|Ворота размера по строкам]]
|
||
|
||
А если часть не длиннее максимума? Тогда резать нечего. Ставится идентификатор IP, и часть уходит одним пакетом. В нашем тесте так уходят обе части.
|
||
|
||
Проверь себя. Прогон функций с порцией шестьдесят, опций двенадцать. Максимум данных? И сколько пакетов получится из частей в сто четыре и в сто сорок шесть байт?
|
||
|
||
Шестьдесят минус двенадцать, сорок восемь. Часть в сто четыре байта: сорок восемь, сорок восемь и восемь, три пакета. Часть в сто сорок шесть: три раза по сорок восемь и два байта, четыре пакета. Всего семь, и в прогоне функций их было ровно семь.
|
||
|
||
Итак, прежде чем отправить, функция узнаёт размер порции, вычитает опции и получает максимум данных. Если опции съели всё место, не отправляется ничего. Часть не длиннее максимума уходит одним пакетом, длиннее режется, а как, в следующей сцене.
|
||
|
||
|
||
## Часть длиннее предела: нарезка, номера и идентификаторы IP
|
||
|
||
Вопрос этой сцены: что происходит, если часть длиннее максимума данных?
|
||
|
||
Игрушка, выдуманная. В чемодане почти полторы тысячи нумерованных листов, а сумка вмещает тысячу четыреста сорок восемь. Следующая сумка начинается с того листа, на котором кончилась прошлая.
|
||
|
||
Теперь код, цикл «пока». Указатель ставится на первый байт части. Пока он не вышел за конец, цикл берёт столько байт, сколько осталось, но не больше максимума. Одиннадцать строк про срочный флаг в середине пропускаем: они работают только для пакетов со срочными данными. Взятый кусок вырезается так же, как multisplit вырезал части; назовём его ломтиком.
|
||
|
||
Дальше два действия: ломтику назначается идентификатор IP, об этом через минуту, и ломтик отправляется. После этого порядковый номер и указатель сдвигаются на длину отправленного ломтика, и круг повторяется. Последний ломтик это остаток.
|
||
|
||
![[wiki-anim-video05-s15-slice-loop.svg|Цикл нарезки на ломтики]]
|
||
|
||
Теперь реальный опыт. Большое приветствие на тысячу четыреста восемьдесят четыре байта, склеенное программой из нескольких пакетов; как она склеивает и проигрывает такие приветствия, разбирает выпуск «Сборка и повторный проигрыш». Стратегия: multisplit с позицией «два». Части: два байта и тысяча четыреста восемьдесят два.
|
||
|
||
Первая часть влезает, в журнале один пакет. Вторая больше максимума, и для неё в журнале две строки отправки: размер тысяча пятьсот и размер восемьдесят шесть. Тысяча пятьсот это двадцать плюс тридцать два плюс тысяча четыреста сорок восемь; восемьдесят шесть так же, с тридцатью четырьмя.
|
||
|
||
На проводе три пакета. Первый: номер один, длина два.
|
||
|
||
Второй: номер три, длина тысяча четыреста сорок восемь; три это один плюс два.
|
||
|
||
Третий: номер тысяча четыреста пятьдесят один, это три плюс тысяча четыреста сорок восемь. Длина тридцать четыре.
|
||
|
||
Тысяча четыреста пятьдесят один плюс тридцать четыре, тысяча четыреста восемьдесят пять: следующий байт после всех данных, и сервер подтвердил именно его.
|
||
|
||
Заметьте: приложение сервера прочитало данные тремя чтениями: два байта, тысяча четыреста сорок восемь и тридцать четыре. Границы чтений совпали с границами пакетов, а не частей по метке. Это поведение нашего сервера; по общим сведениям, границы чтения определяет стек получателя.
|
||
|
||
Теперь идентификаторы IP. В заголовке «ай-пи-ви-четыре» есть двухбайтовое поле «идентификатор»; по общим сведениям, оно нужно, чтобы различать пакеты и собирать фрагменты. По умолчанию правило такое: первый вышедший пакет берёт идентификатор исходного, если до этого в этом вызове ничего не отправляли, а каждый следующий на единицу больше. В нашем опыте с одним разрезом это тридцать семь тысяч четыреста семьдесят два и тридцать семь тысяч четыреста семьдесят три.
|
||
|
||
![[wiki-anim-video05-s15-lab-pieces.svg|Опыт и ломтики по максимуму]]
|
||
|
||
Прогон функций с порцией сто. Максимум восемьдесят восемь. Часть в сто четыре байта: восемьдесят восемь и шестнадцать. Часть в сто сорок шесть: восемьдесят восемь и пятьдесят восемь. Четыре пакета, сдвиги ноль, восемьдесят восемь, сто четыре и сто девяносто два, идентификаторы подряд, флаги те же.
|
||
|
||
Если подделка добавляет в заголовок свою опцию, лишних байт больше, максимум данных меньше, и ломтики короче. Прогон функций с подделкой, добавляющей опцию подписи: опции тридцать байт, с выравниванием тридцать два, ломтики по шестьдесят восемь вместо восьмидесяти восьми.
|
||
|
||
Проверь себя на бумаге. Порция сто пятьдесят, опций двенадцать, части в сто четыре и сто сорок шесть байт. Максимум данных, число пакетов и сдвиги?
|
||
|
||
Сто пятьдесят минус двенадцать, сто тридцать восемь. Часть в сто четыре байта влезает: один пакет, сдвиг ноль. Часть в сто сорок шесть режется на сто тридцать восемь и восемь, сдвиги сто четыре и двести сорок два. Всего три пакета, идентификаторы подряд. Это расчёт: так не запускали.
|
||
|
||
Итак, часть длиннее максимума функция режет на ломтики по максимуму, остаток уходит последним. Номера идут подряд, идентификаторы IP тоже. А границы частей по меткам и границы пакетов могут разойтись.
|
||
|
||
|
||
## Порядок и пауза между частями; итог части
|
||
|
||
Вопрос этой сцены: в каком порядке части уходят в сеть и сколько времени проходит между ними?
|
||
|
||
Игрушка, выдуманная. Почтальон кладёт в ящик два конверта, один за другим. Если между ними он пишет в толстый журнал на неудобном столе, пауза заметна. Если в маленький блокнот, почти пропадает. Пауза зависит от занятости почтальона, а не от дороги.
|
||
|
||
В коде порядок задаёт цикл: от первой части к последней, подряд. Каждая часть отдаётся функциям отправки и уходит, и только потом цикл берёт следующую. Отправка синхронная: сырой сокет возвращает управление, когда ядро приняло пакет, об этом выпуск «Внутри программы». Ни ожидания, ни сна на этом пути в коде нет.
|
||
|
||
В опытах этой части, без накладки и повторов, пакеты и на проводе клиента шли в порядке отправки, номера по возрастанию. Обратный порядок это задача другой стратегии, о ней следующий выпуск «Перемешать: disorder».
|
||
|
||
Но между частями цикл успевает записать в файл журнала строку «отправляю часть такую-то», когда включена отладка.
|
||
|
||
![[wiki-anim-video05-s16-order-loop-sync.svg|Порядок частей и синхронная отправка]]
|
||
|
||
Поясним, о каком диске речь: лаборатория работает под Linux, а наши файлы опытов лежат на диске Windows, подключённом к этому Linux; запись туда медленнее, чем в оперативную память.
|
||
|
||
Измерения нашей лаборатории. Опыт с одним разрезом, журнал на диске, подключённом из Виндоус: пауза между двумя частями почти четыре миллисекунды. Опыт с пятью частями: четыре паузы, от трёх целых восьмидесяти двух сотых до четырёх целых тридцати пяти сотых. Опыт с накладкой, о ней в следующей части: чуть меньше пяти миллисекунд.
|
||
|
||
Те же аргументы, тот же код, а журнал в оперативной памяти. Опыт с одним разрезом: около трёх десятых миллисекунды. С пятью частями: от девяти сотых до четырёх десятых. С накладкой: около двух десятых.
|
||
|
||
Вывод. Мы поменяли место, где лежат журнал и файлы опыта, и пауза упала с четырёх миллисекунд до долей. В самом пути отправки ожидания нет, поэтому, скорее всего, четыре миллисекунды ранних опытов это запись отладочного журнала на медленный диск. Оговорка: это вывод из сравнения двух наборов прогонов, а не отдельное измерение; отладка была включена в обоих.
|
||
|
||
Проверь себя на бумаге. Пять частей, четыре паузы. Сколько времени пройдёт от первого до последнего пакета с журналом на медленном диске и сколько с журналом в памяти?
|
||
|
||
На медленном диске четыре паузы примерно по четыре миллисекунды: около шестнадцати. С журналом в памяти каждая из четырёх пауз меньше четырёх десятых, значит, сумма меньше одной целой шести десятых; по цифрам на экране чуть меньше миллисекунды. Это сумма уже измеренных пауз, нового опыта не было.
|
||
|
||
![[wiki-anim-video05-s16-gaps-slow-fast.svg|Паузы между частями: диск и память]]
|
||
|
||
Итак, что мы сделали. Первые четыре строки цикла разрезали данные на части. Отправщик части взял копию пакета, положил в неё часть и сдвинул порядковый номер на число уже ушедших байт, поэтому без накладки номера на проводе идут подряд. Отправщик с нарезкой вычел опции из порции и получил максимум данных, тысяча четыреста сорок восемь в нашей лаборатории; часть длиннее максимума режется на ломтики. Части уходят по возрастанию, а в нашем тесте с журналом в памяти пауза была долями миллисекунды. Вердикт для исходного пакета, накладка seqovl и ловушки с подделками в следующей части.
|
||
|
||
Итак, строка стала числами, числа частями, части пакетами. В нашей лаборатории данных в пакете не больше тысячи четырёхсот сорока восьми байт. Пакеты ушли по порядку.
|
||
|
||
|
||
## Накладка: лишние байты перед первой частью и как их строит код
|
||
|
||
Это последняя часть выпуска. В ней мы доведём multisplit до конца: что к частям можно добавить, что функция отвечает движку и что с ней может пойти не так. Сначала короткий повтор.
|
||
|
||
Клиент отправил приветствие в двести пятьдесят байт. Стратегия multisplit превратила метку midsld в позицию разреза, после ста четырёх байт, и цикл разрезал данные на части: в нашем тесте сто четыре байта и сто сорок шесть. Каждая часть ушла в сеть отдельным пакетом. Заголовки у частей, как у оригинала, кроме порядкового номера и идентификатора IP: у второй части номер на сто четыре больше. Оригинал выброшен.
|
||
|
||
Сервер получил два пакета и прочитал те же двести пятьдесят байт.
|
||
|
||
Отсюда вопрос части. Можно ли добавить в первый пакет что-то лишнее так, чтобы сервер этого не заметил? Он читает письмо по номерам, значит, у лишнего должны быть номера, которые ему не нужны.
|
||
|
||
Игрушка, пример выдуманный. Письмо из пронумерованных страниц, одна страница это один байт. Секретарь получателя складывает страницы по номерам и начинает с номера один. Страницы с меньшими номерами лежат до начала письма, такие листы он выбрасывает.
|
||
|
||
Отправитель подкладывает перед первой страницей три листа с любыми словами, под номерами минус три, минус два и минус один. Секретарь выбросит их и соберёт то же письмо. А помощник, который номера не смотрит и читает всё в порядке прихода, начнёт чтение с этих трёх листов. Запомним главное: подкладка занимает номера слева, а настоящие страницы остаются на своих местах, первая под номером один.
|
||
|
||
Живёт это внутри цикла по частям и касается только первой части. Приём называется накладкой, а сами подложенные байты подкладкой.
|
||
|
||
Задаёт приём аргумент «накладка», в записи seqovl. В нём число: сколько байт подложить перед первой частью. В коде имя нигде не расшифровано; по-видимому, оно от английского «сиквенс оверлэп», «наложение порядковых номеров».
|
||
|
||
План части. Сначала накладка: код, числа на проводе, ответ сервера, верные и неверные значения. Потом ответ движку и случаи, когда резать нечего. Затем два игрушечных проверяющих, повторы клиента с ловушкой подмены полей и итог выпуска.
|
||
|
||
![[wiki-anim-video05-s17-overlap-secretary.svg|Накладка: подкладка перед первой частью]]
|
||
|
||
Теперь код. Сначала мелочь, помощница pattern. Игрушка, пример выдуманный: рулон обоев с узором. Продавец отрезает ровно столько, сколько попросили; если узор короче, он повторяется, а последний повтор обрезается.
|
||
|
||
Функция pattern делает то же с байтами. В прогоне функций: узор из двух байт, а-бэ и цэ-дэ, длина пять. Получается а-бэ, цэ-дэ, а-бэ, цэ-дэ, а-бэ. Длина всегда та, что просили, а пустой узор это ошибка.
|
||
|
||
Сама накладка стоит внутри цикла по частям и сначала проверяет три вещи. Это первая часть? Задан ли аргумент seqovl? И больше ли нуля его число? Если хотя бы на один из трёх вопросов ответ «нет», накладки нет, и часть уходит как в обычном разрезе.
|
||
|
||
Значение аргумента приходит текстом. Функция «текст в число», в записи tonumber, делает из него число, например пять. Если текст не число, получится пустое значение, а сравнить его с нулём нельзя: это ошибка, о ней в одной из следующих сцен.
|
||
|
||
Дальше узор. Он берётся из аргумента «узор накладки», в записи seqovl_pattern, а без него это один нулевой байт. Узор может быть шестнадцатеричной записью или именем готового блоба.
|
||
|
||
Склейка: узор растягивается функцией pattern до пяти байт и приклеивается перед частью. Было сто четыре байта, стало сто девять.
|
||
|
||
И номер. В функцию отправки передаётся смещение: на сколько сдвинуть порядковый номер относительно оригинала. Без накладки для первой части это ноль, с накладкой минус пять, то есть минус seqovl. Остальные части получают то же смещение, что и в обычном разрезе.
|
||
|
||
С отладочным журналом функция пишет строку про каждую часть. В нашем тесте для первой: байты от нуля до ста трёх, длина сто девять, накладка пять. Диапазон это настоящие байты, а длина уже вместе с накладкой. В журнале байты считаются с нуля, дальше мы нумеруем с единицы.
|
||
|
||
Проверь себя. А что с накладкой у второй части?
|
||
|
||
Её нет. Условие требует первую часть, поэтому у второй накладка равна нулю, а смещение такое же, как в обычном разрезе.
|
||
|
||
![[wiki-anim-video05-s17-pattern-and-seqovl-code.svg|Как код строит накладку]]
|
||
|
||
Итак. Накладка это лишние байты перед первой частью, с номерами левее начала письма. В коде пять шагов: условие, число, узор, склейка и сдвиг номера на минус seqovl, и только для первой части. А что сделает с подкладкой получатель, зависит от получателя, и в нашем тесте мы это проверим.
|
||
|
||
|
||
## Накладка на проводе: номера от минус четырёх до ста пяти и ответ сервера
|
||
|
||
Вопрос сцены: какие номера получат пакеты, если подложить пять байт, и что сделает с ними наш сервер? Счёт ведём от начала клиента: у самого начала ноль, а первый байт данных имеет номер один. Настоящие номера в пакетах гораздо больше, но разности между ними те же.
|
||
|
||
Сначала без накладки. Первый пакет несёт сто четыре байта, это номера с первого по сто четвёртый. Второй пакет начинается со ста пятого.
|
||
|
||
Теперь накладка в пять байт. Левый край первого пакета уезжает влево: один минус пять, минус четыре. Подкладка занимает номера от минус четырёх до нуля, а настоящие байты остаются на номерах с первого по сто четвёртый.
|
||
|
||
Длина первого пакета теперь пять плюс сто четыре, сто девять. Правым краем назовём номер сразу за последним байтом, то есть тот, с которого начнётся следующий пакет. Минус четыре плюс сто девять, это сто пять: последний настоящий байт сто четвёртый, а сразу за ним сто пятый. Левый край сдвинулся, правый остался на месте, второй пакет не двигается вообще.
|
||
|
||
В нашем тесте вышло именно так. Первый пакет начинается с минус четырёх, длина сто девять. Второй начинается со ста пятого, длина сто сорок шесть. В журнале размер первого пакета сто шестьдесят один: пятьдесят два байта заголовков и сто девять байт данных.
|
||
|
||
Второй пример, тоже из нашего теста: разрез после двух байт и накладка в сто. Один минус сто, минус девяносто девять. Длина сто два, а конец, минус девяносто девять плюс сто два, это три. Второй пакет начинается с номера три.
|
||
|
||
Напомним про круг номеров. В прогоне функций задан номер оригинала три, накладка пять. Сумма в Lua минус два, но в пакет число попадает приведённым к тридцати двум битам. Номера ходят по кругу, как циферблат, и минус два это два шага назад от нуля: предпоследнее число шкалы. Такой номер на проводе мы не запускали.
|
||
|
||
Проверь себя. Накладка семь байт, разрез midsld. Где начнётся и где кончится первый пакет?
|
||
|
||
Начало: один минус семь, минус шесть. Длина: семь плюс сто четыре, сто одиннадцать. Правый край: минус шесть плюс сто одиннадцать, это сто пять. Второй пакет всё так же начинается со ста пятого. Это счёт, такой опыт мы не запускали.
|
||
|
||
![[wiki-anim-video05-s18-seq-edges.svg|Номера пакетов с накладкой]]
|
||
|
||
Теперь ответ сервера. Игрушка, пример выдуманный. Секретарь получил листы от минус четырёх до ста четвёртого и отвечает запиской из двух строк. Первая: принято до номера сто четыре включительно, жду сто пятый. Вторая: листы от минус четырёх до нуля пришли лишними, они мне не нужны.
|
||
|
||
Настоящий ответ. Во-первых, номер, который сервер ждёт следующим: в нашем тесте после первого пакета это сто пять, то же число, что и без накладки. Подкладка в счёт не вошла.
|
||
|
||
Во-вторых, блок выборочного подтверждения, по-английски SACK. По общим сведениям, им обычно сообщают о куске правее ожидаемого номера: листы со сто двадцатого по сто тридцатый есть, а сто пятого по сто девятнадцатый нет. Запись в скобках читаем так: левая граница входит, правая нет.
|
||
|
||
Наш блок лежит левее: от минус четырёх до единицы не включая, то есть номера от минус четырёх до нуля. Ровно подкладка, пять номеров из пяти.
|
||
|
||
По общим сведениям, такая форма называется дублирующим блоком: сервер сообщает о лишней копии. Она описана в стандарте, а в нашем репозитории про неё ничего нет.
|
||
|
||
В-третьих, что прочитало приложение на сервере. В нашем тесте два чтения: сто четыре байта и сто сорок шесть, всего двести пятьдесят, и содержимое совпало с оригиналом. Подкладки в данных нет. По общим сведениям, протокол границ пакетов не обещает, и границы чтений зависят от того, как быстро приложение просыпается; в нашем тесте так вышло.
|
||
|
||
Что отсюда следует. Наш сервер подкладку не посчитал и приложению не передал. Как именно ядро это делает, мы не смотрели; по общим сведениям, оно отбрасывает всё левее ожидаемого номера. Мы видим только следствия: подтверждение, блок и данные.
|
||
|
||
Заметим и то, чего не было: клиент ничего не отправлял повторно, соединение не сбрасывалось. И граница: всё это мы видели на одном приёмнике, Linux. Windows, BSD и встроенные системы мы не проверяли и ничего про них не утверждаем.
|
||
|
||
![[wiki-anim-video05-s18-server-answer.svg|Ответ сервера на накладку]]
|
||
|
||
Итак. Левый край первого пакета уходит влево на seqovl, длина растёт на столько же, а правый край и второй пакет остаются на месте. Наш сервер подкладку не считает и приложению не отдаёт, называет её только в блоке выборочного подтверждения.
|
||
|
||
|
||
## Что подкладывать и сколько: узоры, размеры и нарезка по размеру пакета
|
||
|
||
Вопрос сцены: что можно подложить и сколько? Пройдём варианты нашего теста по одному, везде разрез midsld, первая часть сто четыре байта, кроме последнего варианта.
|
||
|
||
Первый вариант: только число, накладка пять. Подкладка это пять нулевых байт. Первый пакет начинается с минус четырёх, длина сто девять.
|
||
|
||
Второй вариант: узор накладки, в записи seqovl_pattern, задан шестнадцатеричной записью из пяти байт: один-шесть, ноль-три, ноль-три, ноль-ноль, ноль-ноль. Числа те же, меняется только содержимое подкладки. По виду это начало заголовка записи TLS: тип, версия и длина ноль. Эту строку мы взяли из примера в документации zapret2. Почему в примере выбран именно такой узор, там не объяснено, и гадать мы не будем.
|
||
|
||
Третий: узор короче накладки. Узор из двух байт, а-бэ и цэ-дэ, накладка десять байт: функция повторяет узор пять раз. Первый пакет начинается с минус девяти, длина сто четырнадцать.
|
||
|
||
Четвёртый: узор это имя готового блоба, готового поддельного приветствия в шестьсот восемьдесят байт; его имя fake_default_tls. Накладка в шестьдесят четыре байта берёт из него первые шестьдесят четыре. Первый пакет начинается с минус шестидесяти трёх, длина сто шестьдесят восемь.
|
||
|
||
Пятый: самая короткая накладка, один байт. Первый пакет начинается с нуля, длина сто пять.
|
||
|
||
Шестой: накладка больше первой части. Разрез после двух байт и накладка в сто: пакет из ста байт подкладки и двух настоящих. Функция не проверяет, что накладка меньше первой части.
|
||
|
||
Длина узора не важна: короткий повторяется, длинный обрезается.
|
||
|
||
Во всех шести опытах сервер прочитал оригинал без изменений.
|
||
|
||
Ещё один опыт: в списке pos метки два, host и midsld дают четыре части, накладка пять. Накладка есть только у первого пакета, у остальных трёх её нет.
|
||
|
||
![[wiki-anim-video05-s19-pad-variants.svg|Что можно подложить]]
|
||
|
||
Теперь размер. Возьмём накладку в тысячу четыреста байт. Первая часть вместе с подкладкой выходит тысяча пятьсот четыре байта, а в пакет за раз помещается меньше. В рукопожатии оба конца объявили размер сегмента тысяча четыреста шестьдесят; код берёт меньший из двух. Опции съедают двенадцать байт, остаётся тысяча четыреста сорок восемь.
|
||
|
||
Отправка режет такую часть на куски. В нашем тесте первый кусок полный: тысяча четыреста байт подкладки и сорок восемь настоящих. Он начинается с минус тысячи трёхсот девяноста девяти и доходит до сорок восьмого номера. Второй кусок: пятьдесят шесть настоящих байт, он начинается с номера сорок девять. Вторая часть, как раньше, со ста пятого.
|
||
|
||
Сервер ждёт сорок девятый, потом сто пятый. Приложение читает сорок восемь, пятьдесят шесть и сто сорок шесть: тот же оригинал. Куски у приложения повторяют нарезку, а не части. По общим сведениям, границы чтений зависят от скорости приложения; в нашем тесте вышло так.
|
||
|
||
Проверь себя. Узор из трёх байт: один, два, три. Накладка семь. Какие семь байт будут подложены?
|
||
|
||
Один, два, три, один, два, три, один. Узор повторяется и обрезается ровно по длине накладки, как рулон обоев. Это счёт по коду и по прогону функции pattern, с таким узором мы опыт не запускали.
|
||
|
||
![[wiki-anim-video05-s19-mss-slicing.svg|Нарезка части по размеру сегмента]]
|
||
|
||
Итак. Подложить можно нули, свой узор или начало готового блоба; длина целая и положительная, мы пробовали от одного байта до тысячи четырёхсот, и только перед первой частью. Слишком длинную часть отправка режет по размеру сегмента. Нашему серверу узор безразличен, а игрушечному проверяющему нет, и дальше мы это увидим.
|
||
|
||
|
||
## Неверные значения: ошибки, ноль, минус и ловушка дробной накладки
|
||
|
||
Вопрос сцены: что будет, если аргумент накладки неверный? Сначала ошибки, потом странное значение, которое ошибки не даёт.
|
||
|
||
Игрушка, пример выдуманный. Бригадир получил наряд, где вместо числа слово «много». Считать нечем, работа не начинается, заготовка уходит заказчику целой.
|
||
|
||
Первый случай: вместо числа текст, seqovl равно abc. Функция «текст в число», в записи tonumber, возвращает пустое значение, а сравнить пустое значение с нулём нельзя. Это ошибка Lua: «попытка сравнить число с пустым значением». Движок пишет в журнал, что пропускает пакет без изменений, и оригинал уходит целым. В нашем тесте на проводе один пакет в двести пятьдесят байт. То же самое, если у seqovl пустое значение.
|
||
|
||
Второй случай: ноль или отрицательное число. Условие «больше нуля» не выполнено, накладки нет, и получается обычный разрез: сто четыре байта и сто сорок шесть. Здесь нет ошибки, функция просто молча не делает накладку. В нашем тесте это проверено с нулём.
|
||
|
||
Третий: число в шестнадцатеричной записи, с приставкой «ноль-икс» и цифрами один и ноль. Lua читает его как шестнадцать. В нашем тесте подложено шестнадцать нулевых байт, первый пакет начинается с минус пятнадцати.
|
||
|
||
Четвёртый: узор называет блоб, которого нет. Функция blob говорит, что такой блоб недоступен: это ошибка, и оригинал уходит целым, один пакет. А с флагом «необязательно», в записи optional, функция пишет в журнал, что блоб не найден, берёт нулевой узор, и накладка работает как обычно. Этот флаг превращает ошибку в запасной вариант.
|
||
|
||
В прогоне функций ошибку дают ещё шестнадцатеричная запись с нечётным числом цифр и пустое имя узора.
|
||
|
||
Общий вывод. Неверный аргумент не рвёт соединение, но результат разный: ноль и минус молча дают обычный разрез, а текст и пропавший узор срывают всю стратегию, оригинал уходит целым. Заметно это только в журнале и по числу пакетов.
|
||
|
||
![[wiki-anim-video05-s20-bad-values-table.svg|Неверные значения накладки]]
|
||
|
||
А теперь значение, которое ошибки не даёт: дробное. Игрушка, пример выдуманный. Бригадир получил наряд: подложить два с половиной листа. Листов он положит два, целых. А новый номер первой страницы посчитает так: прежний минус два с половиной, и результат округлит вниз. Выходит минус три. Листов два, сдвиг три, и вся стопка съехала на один номер.
|
||
|
||
В коде так же. Число два с половиной используется дважды. Функция pattern берёт из него целую часть и делает два байта подкладки, а в сдвиг номера оно входит как есть. Прогон функций показывает: сумма получается с дробью, а при сборке пакета дробь отбрасывается вниз, и номер выходит на три меньше номера оригинала. По общим сведениям, Lua бывает в двух видах: обычный интерпретатор и луаджит, его ускоренный вариант. В нашей сборке программы стоит луаджит. Так ведёт себя наша сборка с луаджитом.
|
||
|
||
По общим сведениям, в обычном Lua начиная с версии пять-три дробная длина вызвала бы ошибку уже при подсчёте повторов узора. Такую сборку мы не запускали.
|
||
|
||
Опыт: накладка два с половиной, разрез midsld. Первый пакет: два байта подкладки и сто четыре настоящих, длина сто шесть, начинается он с минус двух. Это на три левее первого байта, а не на два, и первый настоящий байт съехал на ноль. Сервер отбрасывает всё левее единицы: два байта подкладки и первый настоящий. Он читает сто три байта и ждёт сто четвёртый.
|
||
|
||
Второй пакет начинается со ста пятого, и между ними дыра в один байт. Сервер отвечает: жду сто четвёртый, а блок выборочного подтверждения показывает всё правее. Тогда отправляет заново сам клиент, судя по журналу, его ядро, а не nfqws2: кусок с номера сто четыре, сто сорок семь байт.
|
||
|
||
Приложение прочитало сто три байта и сто сорок семь. Всего двести пятьдесят, но не те: первого байта нет, а байт на границе повторён. Имя сайта прочесть нельзя, содержимое не совпало с оригиналом. Ошибки нет, соединение не рвётся, а поток испорчен. Накладка должна быть целым положительным числом.
|
||
|
||
Проверь себя. Накладка три с половиной. Сколько байт подкладки и на сколько сдвинется номер?
|
||
|
||
Подкладки три байта, а номер сдвинется на четыре: оригинал минус три с половиной, округлённое вниз, это минус четыре. Первый настоящий байт опять съедет на один левее единицы. Это счёт по коду, такой опыт мы не запускали.
|
||
|
||
![[wiki-anim-video05-s20-fraction-trap.svg|Дробная накладка портит поток]]
|
||
|
||
Итак. Ноль и минус дают обычный разрез без накладки. Текст, пустое значение и пропавший узор дают ошибку Lua, и оригинал уходит целым, а флаг optional спасает пропавший узор. Дробь ошибки не даёт, но портит поток. Всё это видно только в журнале и по пакетам на проводе.
|
||
|
||
|
||
## Ответ движку: выбросить или пропустить, повторы частей и три пути к целому оригиналу
|
||
|
||
Вопрос сцены: что функция отвечает движку, когда части отправлены, и что будет с оригиналом? Путей несколько.
|
||
|
||
Игрушка, пример выдуманный. Курьер разослал заказ по частям, а оригинал остался на столе: выбросить или отправить следом?
|
||
|
||
Это решают две строки после цикла. Сначала служебная отметка для повторного проигрыша: так программа заново пускает собранное приветствие (выпуск «Сборка и повторный проигрыш»); вне проигрыша она по коду ничего не значит. Потом ответ: если задан аргумент «не выбрасывать», в записи nodrop, то «пропустить», иначе «выбросить». По умолчанию оригинал выбрасывается.
|
||
|
||
Тут две хитрости языка. Первая: аргумент приходит строкой, а в Lua любая строка истинна, даже пустая. Поэтому любая запись nodrop его включает.
|
||
|
||
Вторая: «пропустить» в движке закодировано числом ноль, а в Lua ложны только пустое значение и ложь, число ноль истинно. Поэтому выражение «nodrop и пропустить, иначе выбросить» при заданном nodrop даёт именно «пропустить». В прогоне функций nodrop равный нулю тоже дал «пропустить».
|
||
|
||
Теперь провод. Без nodrop журнал пишет «пакет выброшен», и на проводе два пакета, сто четыре и сто сорок шесть байт. Пакета в двести пятьдесят нет.
|
||
|
||
С nodrop журнал пишет «пакет пропущен без изменений». На проводе сначала две части, потом оригинал: двести пятьдесят байт с номером один. В нашем запуске он вышел следом за второй частью примерно через шесть сотых миллисекунды: ядро держит оригинал в очереди у программы (выпуск «Внутри программы») и выпускает его, только когда функция вернулась.
|
||
|
||
Что ответил сервер. После первой части: жду сто пятый. После оригинала: жду двести пятьдесят первый и дублирующий блок от единицы до двухсот пятьдесят первого не включая: сервер сообщает, что это у него уже было. Приложение прочитало сто четыре и сто сорок шесть, второй раз ничего не доставлено. С накладкой то же самое, и сервер даёт два дублирующих блока. Значит, nodrop стоит одного лишнего пакета и для нашего сервера ничего не меняет.
|
||
|
||
Тот, кто смотрит на каждый пакет, увидит и оригинал с целым именем. Через две сцены, среди шести игрушечных моделей, две при разрезе посреди имени находят имя с nodrop и не находят без него.
|
||
|
||
И ещё один аргумент отправки, «повторы», в записи repeats. В нашем тесте при repeats равном двум каждая часть уходит дважды подряд, через пять-шесть сотых миллисекунды. Приложение всё равно прочитало сто четыре и сто сорок шесть.
|
||
|
||
![[wiki-anim-video05-s21-verdict-wire.svg|Ответ движку и оригинал]]
|
||
|
||
Теперь случаи, когда резать нечего или что-то сломано. Игрушка, пример выдуманный. Курьер с пустым списком адресов отправляет посылку как есть. Курьер, у которого в списке слово вместо адреса, пишет диспетчеру «ошибка», и посылка тоже уходит как есть.
|
||
|
||
Первый путь, тихий. После разбора меток и вычёркивания единицы в списке не осталось позиций. В нашем тесте так вышло по пяти причинам: позиция ноль, пустой список, позиция за концом данных, позиция равна длине данных и метка не того протокола, например method на приветствии TLS. Во всех пяти журнал пишет «нет допустимых позиций» и «пакет пропущен без изменений».
|
||
|
||
Почему так? Перед первым байтом резать нельзя, единицу вычёркивают. Позиция должна быть меньше длины данных: не годятся ни позиция за концом, ни равная длине. Метка не того протокола ничего не находит. На проводе один пакет в двести пятьдесят байт, побайтно равный тому, что видел nfqws2.
|
||
|
||
Второй путь, громкий. Список меток с неизвестным словом: pos равно foo. Функция на Си сообщает «плохой список меток», и это ошибка Lua. Журнал: ошибка, потом «пропускаем пакет без изменений». На проводе тот же один пакет. Так же выглядели неверный seqovl и пропавший узор.
|
||
|
||
Снаружи пути не различить: разница в журнале и в судьбе других вызовов стратегии на этом пакете. Как движок складывает ответы, разбирает выпуск «Lua внутри программы»; главное: после ошибки Lua движок пропускает оригинал целым, даже если другой вызов уже сказал «выбросить». Тихий выход такого не делает, цепочка идёт дальше.
|
||
|
||
Третий путь, сбой отправки, показан только в прогоне функций, где отправку заменила заглушка. Если не удалась первая часть, функция отвечает «пропустить», на проводе ничего нет, оригинал уходит целым. Если частей четыре и удалась только первая, она уже на проводе, а оригинал идёт следом за ней целиком. Настоящий сбой сырого сокета мы не воспроизводили.
|
||
|
||
![[wiki-anim-video05-s21-three-paths.svg|Три пути оригинала целым]]
|
||
|
||
В двух первых путях в нашем тесте письмо дошло целым, ни ошибки, ни сброса: отсутствие ошибки не значит, что разрез случился.
|
||
|
||
Проверь себя. Аргумент pos равен двумстам сорока девяти, а приветствие в двести пятьдесят байт. Что будет на проводе?
|
||
|
||
Позиция меньше длины данных, значит допустима: два пакета, двести сорок девять байт и один. В нашем тесте так и вышло.
|
||
|
||
Итак. После частей функция отвечает «выбросить», а с nodrop «пропустить», и тогда оригинал идёт следом; повторы шлют каждую часть дважды. Нечего резать, неверный аргумент или сбой отправки: во всех трёх случаях оригинал уходит целым. Различаются журнал и судьба других вызовов, а проверять надо по проводу.
|
||
|
||
|
||
## Игрушечный проверяющий первого вида: правило в роутере смотрит на одно место
|
||
|
||
Сначала честное напоминание. В лаборатории нет настоящего ТСПУ. Проверяющие, которых мы покажем, это игрушки нашего проекта, и слово «сработало» в этих сценах значит только одно: игрушку обошли.
|
||
|
||
Вопрос сцены: что делает с нашими разрезами игрушечный проверяющий первого вида, правило в роутере? Это то самое правило из начала выпуска.
|
||
|
||
Вспомним игрушку, пример выдуманный: вахтёр с картонкой с окошком. Он читает каждый лист отдельно, смотрит только в окошко на одном и том же месте, листы не склеивает и ничего не помнит.
|
||
|
||
Правило устроено так же. Оно смотрит на пакеты к серверу, порт четыреста сорок три, и проверяет одно место. Назовём место смещением от начала данных, считая с нуля. Это смещение девяносто семь, то есть тридцать два байта заголовка TCP и ещё девяносто семь. Там в приветствии начинается имя сайта, пятнадцать байт. Если оно совпало, пакет выбрасывается или соединение сбрасывается, это два варианта правила. Счётчик считает совпавшие пакеты.
|
||
|
||
Откуда тридцать два: двадцать байт обычного заголовка и двенадцать байт опции времени. У всех пакетов нашего теста заголовок такой длины, поэтому место одно и то же.
|
||
|
||
![[wiki-anim-video05-s22-rule-offset.svg|Правило и одно место]]
|
||
|
||
Первая строка. Без zapret2 правило видит приветствие целиком, имя лежит на своём месте. В варианте с выбрасыванием счётчик шесть: оригинал и пять повторов клиента. Сервер получил ноль байт. В варианте со сбросом счётчик один, и клиент получил «соединение сброшено другой стороной».
|
||
|
||
Вторая: разрез midsld. Имя разрезано пополам, на нужном месте его нет. Счётчик ноль, сервер прочитал сто четыре и сто сорок шесть байт.
|
||
|
||
Третья: разрез после двух байт. Имя целиком, но во втором пакете, на смещении девяносто пять. Правило смотрит на девяносто седьмое, и счётчик ноль.
|
||
|
||
Четвёртая: разрез точно перед именем. Теперь второй пакет начинается с имени, смещение ноль. Счётчик снова ноль.
|
||
|
||
Пятая: разрез после имени. Имя целиком остаётся в первом пакете, на том же смещении девяносто семь. Правило срабатывает: счётчик шесть, сервер получил ноль байт, а клиент шлёт шесть одинаковых пар. Разрез тут есть, а толку нет.
|
||
|
||
Шестая: тот же разрез после имени, но с накладкой в пять байт. Имя по-прежнему цело в одном пакете, но подкладка сдвинула его на пять: смещение сто два. Счётчик ноль, сервер получил сто двенадцать и сто тридцать восемь байт. С вариантом правила со сбросом то же самое.
|
||
|
||
![[wiki-anim-video05-s22-six-runs.svg|Шесть опытов с правилом]]
|
||
|
||
Что тут можно прочесть. Правило обходится любым сдвигом имени с его места. Сдвинуть можно не только разрезом, но и накладкой: она помогла, хотя имя цело в одном пакете. А разрез, который оставляет имя на месте, не помогает. Это про нашу игрушку и ни про что больше.
|
||
|
||
Проверь себя. Разрез после имени и накладка в три байта. Что покажет счётчик?
|
||
|
||
Имя окажется на смещении сто, а не девяносто семь, поэтому ноль. Это счёт по устройству правила, именно такой опыт мы не запускали.
|
||
|
||
Итак. Первое игрушечное правило, без памяти, смотрит на одно место. Любой сдвиг имени с этого места обходит именно эту игрушку, а разрез, оставляющий имя на месте, нет.
|
||
|
||
|
||
## Игрушечные проверяющие второго вида: шесть моделей над записью и границы выводов
|
||
|
||
Игрушки второго вида. Напоминаем: это модели проекта, а не оборудование, и «сработало» значит «обошло игрушку». Вопрос сцены: что увидят шесть моделей, которым показали записанные пакеты?
|
||
|
||
Они читают запись пакетов, снятую в роутере, то есть ровно то, что дошло до проверяющего. Первая модель только ищет имя целиком в одном пакете. Вторая смотрит одно место, как правило из прошлой сцены, только над записью; во всех наших опытах, где есть оба, она совпала со счётчиком правила.
|
||
|
||
Третья складывает пакеты по номерам, как приёмник: всё левее ожидаемого номера она отбрасывает.
|
||
|
||
Остальные три разбирают приветствие TLS с начала потока. Четвёртая склеивает пакеты в порядке прихода и номеров не смотрит. Пятая номера учитывает, но началом потока считает первый увиденный пакет. Шестая как пятая, но идёт по заголовкам записей: тип, версия, длина.
|
||
|
||
![[wiki-anim-video05-s23-six-models.svg|Шесть игрушечных моделей]]
|
||
|
||
Первая строка таблицы, разрез midsld без накладки. Первая и вторая модели имя не находят: разрез прошёл по нему. Остальные четыре его читают: все части пришли, склеить их легко.
|
||
|
||
Вторая строка, накладка в пять байт с узором, который по виду заголовок записи. Третья модель, как приёмник, читает имя: подкладка лежит левее начала, и она её отбрасывает. Четвёртая и пятая не разбирают приветствие: у них поток начинается с подкладки. Шестая читает: заголовок с длиной ноль для неё пустая запись, она шагает через неё на пять байт и попадает на настоящее начало.
|
||
|
||
Третья строка, накладка из нулей. Третья модель читает, а четвёртая, пятая и шестая нет: первый байт нулей не тип записи, и шестая останавливается сразу. Результат тот же в опытах с узором а-бэ и цэ-дэ, с шестьюдесятью четырьмя байтами поддельного приветствия и с накладкой в тысячу четыреста байт.
|
||
|
||
Четвёртая строка, накладка с nodrop. Оригинал идёт следом за частями, и теперь первая и вторая модели находят имя: в оригинале оно на месте и целое. Так что nodrop возвращает проверяющему то, что разрез у него отнял.
|
||
|
||
Что можно сказать, и только про эти шесть игрушек. Третья, которая следит за номерами как приёмник, накладки не замечает. У четвёртой и пятой поток начинается с подкладки, и разбор ломается. Если бы они искали имя просто подстрокой, в их склейке оно нашлось бы: ломается именно разбор приветствия с начала потока. Шестая переживает подкладку в виде пустого заголовка записи, но не нули и не другие узоры, которые мы пробовали.
|
||
|
||
А теперь границы. Руководство проекта пишет: если проверяющий не следит за номерами, он может проглотить сегмент целиком и поверить ложным данным в его начале, а сервер их не примет. Мы этого на настоящем оборудовании не проверяли. Мы не знаем, какая из шести моделей похожа на настоящую, если вообще похожа. Про настоящий ТСПУ эти опыты не говорят ничего.
|
||
|
||
![[wiki-anim-video05-s23-results-table.svg|Результаты шести моделей]]
|
||
|
||
Проверь себя. Шестая модель читает приветствие, когда накладка набрана из байтов один-шесть, ноль-три, ноль-три, ноль-ноль, ноль-ноль, и не читает, когда накладка из нулей. Почему?
|
||
|
||
Шестая идёт по заголовкам записей: тип, версия, длина. У первого узора длина ноль, запись пустая, и она шагает дальше. У нулей первый байт не тип записи, и разбор останавливается. Это таблица нашего теста.
|
||
|
||
Итак. Шесть игрушечных моделей показывают, что именно меняет накладка. Нашему приёмнику на Linux она безразлична. Четвёртой и пятой, которые разбирают приветствие с начала потока, она ломает начало, а шестой только тогда, когда подкладка не похожа на пустую запись. Что из этого верно для настоящего оборудования, мы не знаем.
|
||
|
||
|
||
## Повторы клиента и ловушка подмены полей
|
||
|
||
Вопрос сцены: что делает стратегия, когда клиент отправляет приветствие снова? И почему подмена полей опасна именно в multisplit?
|
||
|
||
Сначала повторы. В этом опыте в роутере то же игрушечное правило: оно выбрасывает пакет, если на одном и том же месте лежит имя сайта. Разрез после имени, в записи endhost, оставляет имя целиком в первой части, на том же месте, и правило выбрасывает эту часть.
|
||
|
||
Клиентское ядро не получило подтверждения первых байт письма: сервер отвечает «жду первый байт» и сообщает, что вторая половина у него есть. О разрезе и о правиле ядро ничего не знает и позже отправляет оригинал целиком.
|
||
|
||
Каждая такая копия приходит в программу через очередь (о ней «Внутри программы») как новый пакет. Функция multisplit нигде не помнит, что уже резала это приветствие, и режет копию снова. В нашем тесте шесть пар: сто двенадцать байт и сто тридцать восемь. Шестая пара уходит через три тысячи триста сорок пять миллисекунд после первой.
|
||
|
||
Первые два промежутка между парами почти равны, около двухсот миллисекунд, а дальше каждый вдвое длиннее предыдущего: четыреста тридцать шесть, восемьсот тридцать два, тысяча шестьсот шестьдесят четыре. В контрольном опыте без nfqws2 расписание очень похожее. По общим сведениям, это обычное удвоение задержки при повторах в TCP. Приложение на сервере прочитало ноль байт, правило насчитало шесть выброшенных пакетов.
|
||
|
||
![[wiki-anim-video05-s24-retry-schedule.svg|Повторы клиента и расписание]]
|
||
|
||
Урок такой. Если проверяющий выбрасывает часть с именем, разрез не помог, и это не один неудачный пакет, а серия одинаковых пар.
|
||
|
||
Теперь ловушка. Игрушка, пример выдуманный. Курьеру велели ставить на каждую посылку штамп «доставить до первого поста». Он поставил, и ни одна посылка не дошла.
|
||
|
||
В коде аргументы подмены полей лежат в той же таблице, что и pos. К ним относятся TTL, флаги TCP, сдвиг номеров и другие. Функция отправки читает их и применяет к каждому пакету, который отправляет. А у multisplit нет отдельных «поддельных» аргументов: всё, что вы задали, получают настоящие части.
|
||
|
||
Опыт. Стратегия multisplit, pos равно midsld, и мы просим поставить TTL, счётчик пересылок, равный единице; в записи аргумент называется ip_ttl. По общим сведениям, каждый роутер вычитает из TTL единицу и выбрасывает пакет, у которого остался ноль, возвращая сообщение об ошибке. В нашем тесте на клиенте двенадцать пакетов, шесть пар, с TTL один. До сервера не дошёл ни один, а роутер вернул девять сообщений типа «время истекло». Почему не двенадцать, мы не выясняли. Приложение прочитало ноль байт, клиент остался без ответа.
|
||
|
||
Прогон функций показывает тот же механизм: при ip_ttl равном трём обе части получают TTL три.
|
||
|
||
![[wiki-anim-video05-s24-fooling-trap.svg|Подмена бьёт по частям]]
|
||
|
||
Ловушка в том, что ничего не выглядит сломанным. Журнал чистый, части уходят, а соединение просто не работает. Про подмену полей и поддельные пакеты расскажет выпуск «Фейк и его условия».
|
||
|
||
Проверь себя. Аргумент ip_ttl равен трём, а разрезов два: pos равно два, midsld. Какой TTL у третьей части?
|
||
|
||
Три. Подмена применяется к каждому пакету, который отправляет функция. Это счёт по коду: для двух частей прогон функций так и показал, три части с этим аргументом мы не запускали.
|
||
|
||
Итак. Копии оригинала от клиента режутся каждый раз заново. А аргумент подмены в multisplit бьёт по настоящим частям, а не по какому-то поддельному пакету.
|
||
|
||
|
||
## Итог выпуска: таблица строк, которые надо помнить, граница знаний и что дальше
|
||
|
||
Подведём итог выпуска.
|
||
|
||
Вот путь одной строки, каждый шаг мы читали в коде, а почти все повторили на пакетах нашей лаборатории. Мы написали multisplit с pos равно midsld. Функция проверила ворота: это TCP, есть данные, подходит направление и тип. Метка превратилась в число: сто четыре байта слева от разреза, в Lua это сто пять. Цикл разрезал приветствие на две части: сто четыре байта и сто сорок шесть.
|
||
|
||
Каждая часть ушла отдельным пакетом, с заголовками оригинала и своим порядковым номером. Если часть не помещалась в пакет, её нарезали по размеру сегмента. Перед первой частью, если просили, лежала подкладка с номерами левее начала: для midsld номер второй части больше на сто четыре, а накладка в пять байт сдвигает только первую. И в конце функция ответила движку: оригинал выбросить, а с nodrop пропустить.
|
||
|
||
![[wiki-anim-video05-s25-line-path.svg|Путь строки до провода]]
|
||
|
||
Восемь строк, которые стоит запомнить. Первая: pos это список меток, по умолчанию два, и если резать нечего, оригинал уходит целым. Вторая: плохая метка это ошибка, оригинал тоже уходит целым. Третья: каждая допустимая позиция даёт на одну часть больше. Позиции сортируются, повторы склеиваются, части идут по возрастанию, подряд и без ожидания между частями. Четвёртая: часть, которая не помещается в пакет, режется по размеру сегмента.
|
||
|
||
Пятая: seqovl, целое положительное число, подкладка только перед первой частью. Ноль и минус дают обычный разрез, текст срывает стратегию ошибкой, а дробь в нашей сборке портит поток. Шестая: nodrop, любое значение, оригинал идёт следом за частями. Седьмая: аргументы подмены в multisplit бьют по настоящим частям. Восьмая: повторы оригинала от клиента режутся заново, каждый раз.
|
||
|
||
Теперь о том, чего мы не знаем. Мы проверяли один приёмник, Linux, и не проверяли Windows, BSD и встроенные системы. Мы не воспроизводили настоящий сбой сырого сокета. Не запускали накладку при IP версии шесть. Для UDP и ICMP multisplit по коду сразу выходит, а на проводе мы этого не проверяли.
|
||
|
||
Не знаем, почему в примере документации стоит именно такой узор, и сколько разрезов выдержит чужой приёмник: наш сервер принял двадцать пять пакетов по десять байт. А про настоящий ТСПУ мы не знаем ничего. Проверяющие нашего выпуска это игрушки, и «сработало» значит «обошло игрушку».
|
||
|
||
Что дальше. Во всём этом выпуске части уходили по порядку: первая, потом вторая. Но какая разница приёмнику, в каком порядке они пришли, если номера сходятся? В следующем выпуске, «Перемешать: disorder», мы отправим те же разрезы в другом порядке, например, последней частью вперёд, и посмотрим, что сделают сервер и наши игрушки. А в стратегии multidisorder, которая отправляет части в другом порядке и разобрана в нём, накладка задаётся меткой, а не числом; у multisplit, как мы видели, только число.
|
||
|
||
![[wiki-anim-video05-s25-remember-unknown.svg|Помнить и граница знаний]]
|
||
|
||
Итак. Multisplit превращает одно приветствие в несколько пакетов: метки в числа, числа в части, части в пакеты с номерами. Накладка подкладывает лишнее слева от первой части, а ответ движка решает судьбу оригинала. И всё это мы проверили в коде и в нашей лаборатории, а не на настоящем оборудовании.
|
||
|
||
## См. также
|
||
|
||
- [[04-gde-rezat-metki|Где резать: метки]] — предыдущее видео серии
|
||
- [[multisplit]] — справочная статья о функции
|
||
- [[payload]] — типы данных и их распознавание
|
||
- [[blob]] — как объявлять данные для аргументов
|
||
- [[ts-and-fooling]] — подмена полей пакета
|
||
|
||
---
|
||
|
||
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
|
||
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git2.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/video/05-multisplit-rezat-na-chasti.md) · [скачать весь репозиторий одним zip-архивом](https://git2.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).
|