О чём заметка

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

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

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

Повтор и затравка: разрезать слово посередине

Добро пожаловать в выпуск «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; как это делается, разберём в следующей части. Сервер складывает части и читает те же двести пятьдесят байт.

Повтор разреза по midsld Повтор: разрез по метке midsld превращает приветствие в 250 байт в два пакета, в 104 и 146 байт: имя www.example.com занимает байты с 97 по 111, метка midsld стоит на байте 104 (sld 101 плюс 7 поделить нацело на 2 это 3), первый пакет кончается на www.exa, второй начинается с mple.com, оригинал программа выбрасывает (DROP), а сервер складывает части и получает те же 250 байт; в захвате клиента у частей одинаковые заголовки, а различаются данные и два счётчика: относительный номер последовательности 1 и 105, идентификатор IP 1749 и 1750. повтор: одно приветствие, один разрез --payload=tls_client_hello--lua-desync=multisplit:pos=midsld ClientHello, 250 байтwww.example.comбайты 97–111 0250 midsld = 101 + 7 // 2 = 104семь, поделённое нацело на два, это три sld = 101 пакет 1: 104 байтакончается на www.exa пакет 2: 146 байтначинается с mple.com байты 0–103байты 104–249 оригинал: DROP сервер складывает части подряд: 250 байт, хэш совпал с оригиналом что скопировано, что различается (захват клиента, относительные номера) данныеномер последовательностиидентификатор IPфлаги, TTL, опции пакет 1104 байта117490x18, 64, равны пакет 2146 байт1 + 104 = 10517500x18, 64, равны Меняются данные и два счётчика, остальное скопировано; как это делается, разберём в следующей части.

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

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

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

Вахтёр и правило-игрушка Игрушечный вахтёр, который смотрит в одно и то же место каждого листа, и тот же приём в нашей лаборатории: фамилия Иванов целиком в окошке закрывает шлагбаум, а разрезанная на Ива и нов не закрывает, и получатель читает её подряд целиком; настоящий опыт с игрушечным правилом в роутере, которое ищет 15 байт www.example.com на смещении 97 в данных каждого пакета: без программы и без разреза счётчик правила показал 6 пакетов (оригинал и 5 повторных отправок на 0, 206, 414, 834, 1662 и 3326 мс), сервер получил 0 байт, а с разрезом multisplit по midsld счётчик остался нулевым и сервер получил все 250 байт; это игрушка проекта, а не оборудование. выдуманный пример: вахтёр с картонкойвыдуманный пример один листимяИвановокошко лежит на одном месте листашлагбаум закрыт два листаИвановшлагбаум поднятвахтёр читает каждый лист отдельно, не склеиваетполучатель читает листы подряд: Иванов настоящий опыт: игрушечное правило в роутереигрушка, а не оборудование @th,1032,120 0x7777772e6578616d706c652e636f6d counter drop15 байт www.example.com: 1032 бита = 129 байт от начала TCP = 32 заголовок + 97 данных без программы и без разрезаодин пакет, 250 байт, имя целиком в окошке 250025020625041425083425016622503326мс; не в масштабе; пакет каждый раз тот же счётчик правила: 6 пакетов, 1812 байторигинал и 5 повторных отправок клиента сервер: 0 байт, ответа нетвсе шесть пакетов правило выбросило multisplit:pos=midsldоригинал выброшен, на провод ушли части 104 байта146 байтправило не нашло имя ни в одном пакете счётчик правила: 0 пакетовправило не поймало ни одного пакета сервер: 250 байт, ответ полученчасти сложены подряд, как оригинал

План части: как стратегия вызывается и когда она ничего не делает; четыре главных условия мы назовём воротами. А потом посмотрим, как мост, то есть функция на Си, которую зовёт 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 внутри программы».

Карточка функции multisplit Карточка функции multisplit: она живёт в lua/zapret-antidpi.lua на строках 483-539 как глобальная функция multisplit(ctx, desync), с описанием аргументов в комментарии на строках 475-482; зовёт её движок на Си, который находит функцию по имени (nfq2/desync.c:1055) и вызывает с двумя входами под защитой lua_pcall (1076), а ошибка Lua отпускает пакет без изменений (1079-1083); вход ctx функция не читает, а передаёт помощникам отсечки (строки 486 и 489), вход desync даёт поля dis.tcp, dis.icmp, reasm_data, dis.payload, l7payload, outgoing, поля проигрыша и arg; аргументы по умолчанию: pos равен 2 (строка 497), blob нет, seqovl, nodrop и optional выключены, seqovl_pattern один нулевой байт, dir равен out, payload равен known, а подмена полей, ip_id, repeats и ipfrag применяются к каждому отправленному пакету; выходы: пакеты уходят сами через сырой сокет мимо очереди ядра (zapret-lib.lua:1282-1290, darkmagic.c:2032), ответ движку это DROP или PASS с nodrop (строка 527), а если функция ничего не вернула, это PASS, и ещё строки журнала. карточка функции multisplit где живётlua/zapret-antidpi.luamultisplit(ctx, desync)строки 483-539, глобальнаяописание аргументов: 475-482 кто зовёт: движок на Сиlua_getglobal(имя)desync.c:1055lua_pcall(L, 2, …):1076ошибка: пакет не тронут:1079-1083два входа: ctx и desync вход 1: ctxключ; сама функцияего не читает,передаёт помощникамотсечки: 486 и 489 вход 2: таблица desync, поля по одному dis.tcp484 dis.icmp486 reasm_data494 dis.payload494 l7payload500 outgoinglib 1294 replay*lib 1362 argниже tcp_mss сама функция не читает: его читает помощник отправки (lib 1240) аргументы стратегии: тексты после имени, значения по умолчанию что резатьpos"2"497blobнет494 накладка и ответ движкуseqovlвыключено509seqovl_pattern1 байт 00511nodropвыключено527optionalвыключено490 стандартныеdiroutlib 1295payloadknownlib 1312подменаlib 423 для каждого пакетаip_id, repeats, ipfrag остальные аргументы, кроме pos, blob, dir и payload,разберём, когда дойдём до них выходы пакетыкаждая часть уходит сама,через сырой сокет, а неответом ядру; в очередьчасти не попадаютlib 1282-1290 → darkmagic.c:2032 ответ движку (вердикт)после всех частей: DROPс nodrop: PASSничего не вернула: PASSreturn desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP : 527 строки журналасвои строки функции:метки, позиции,отправка частейпример: на следующейсхеме

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

Три роли в журнале Три роли функции multisplit в журнале пакета номер 4, самого приветствия: строка движка desync значит вход, строка instance cutoff с in=1 out=0 значит просьбу отключить экземпляр для встречного направления, строки LUA: multisplit это собственные строки функции (список меток midsld, найденная позиция 104, отправка частей 0-103 длиной 104 и 104-249 длиной 146), а последняя строка движка packet: id=4 drop это ответ; строки sending part не пакеты очереди: в журнале того же опыта 11 пакетов очереди с длинами 60, 60, 52, 302, 52, 66 и ещё пять по 52, и среди них нет ни длины 156, ни 198, с которыми части ушли на провод, потому что части отправлены через сырой сокет мимо очереди. журнал нашего теста, пакет номер 4: приветствие, перед ним три пакета рукопожатия * lua 'multisplit_1_2' : desyncвход: движок зовётdesync.c:1054 instance cutoff for 'multisplit_1_2' in=1 out=0просьба об отключении: след ctxlua.c:924 LUA: multisplit: split pos: midsldсвои строки функцииantidpi:499 LUA: multisplit: resolved split pos: 104свои строки функцииantidpi:501 LUA: multisplit: sending part 1 0-103 len=104 seqovl=0 : …свои строки функцииantidpi:521 rawsend_dissect repeats=1 size=156 badsum=0 ifout=c0 …помощник отправкиlua.c:3026 LUA: multisplit: sending part 2 104-249 len=146 seqovl=0 : …свои строки функцииantidpi:521 rawsend_dissect repeats=1 size=198 badsum=0 ifout=c0 …помощник отправкиlua.c:3026 packet: id=4 dropвыход: ответ движкуnfqws.c:333 функцию зовут, она режет и отправляет, движок получает ответ очередь ядра в том же опыте: 11 пакетов, номера подряд 160 260 352 4302 552 666 752 852 952 1052 1152 число в клетке: длина пакета в байтах; пакет 4 это приветствие 302 = 20 + 32 + 250 Частей среди пакетов очереди нет: ни 156, ни 198 (размеры частей на проводе).Строки sending part пишет сама функция; части уходят через сырой сокет мимо очереди.

Проверь себя. Мы запустили multisplit без единого аргумента на нашем приветствии. Что она сделает?

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

Итак, multisplit это глобальная функция Lua, которую зовёт движок с ключом ctx и таблицей desync. Она читает аргументы с умолчаниями: позиция два, направление наружу, известные типы. Части отправляет сама, а движку отвечает «выбросить» или, с nodrop, «пропустить».

Ворота функции: четыре вопроса и что бывает за закрытыми

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

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

Первый вопрос: есть ли у пакета заголовок TCP? Если нет, функция выходит сразу, а если это ещё и не ICMP (так по общим сведениям называют служебные сообщения сети, например ping), то по коду просит движок отключить стратегию для соединения; на проводе не проверяли.

Ещё до остальных вопросов функция отключает себя для встречного направления, то есть при направлении наружу для входящих пакетов. В журнале нашего теста это строка «instance cutoff», in равно один, out равно ноль.

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

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

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

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

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

Четыре ворота функции Ворота функции multisplit, четыре вопроса по порядку кода из lua/zapret-antidpi.lua: есть ли у пакета заголовок TCP (строка 484; не ICMP ещё и отключение стратегии для соединения, строка 486), какие данные резать и есть ли они (строка 494 берёт первое найденное из блоба, собранного приветствия reasm_data и данных пакета, а строка 495 требует, чтобы они были не пусты), то ли направление (direction_check, по умолчанию наружу, zapret-lib.lua:1294-1297) и подходит ли тип нагрузки (payload_check, по умолчанию известные типы, 1311-1327); до остальных вопросов строка 489 отключает функцию для встречного направления, и в журнале остаётся instance cutoff; на любое нет функция выходит, ничего не отправив и не вернув, движок читает молчание как PASS, и оригинал уходит как есть; фильтров типа два: снаружи опция payload у вызова решает, звать ли функцию, внутри аргумент payload решает функция. ворота: четыре вопроса до разреза, в порядке кода 1TCP ли пакет?484if not desync.dis.tcp then … return endнет: выходнет заголовка TCP: выход; не ICMP ещё и просит отключить стратегию для соединения (486, по коду) 489direction_cutoff_opposite(ctx, desync)до остальных вопросов: при dir=out закрывает встречное направление; журнал: instance cutoff … in=1 out=0 2какие данные режем и есть ли они?494блобarg.bloborсобранное приветствиеreasm_dataorданные пакетаdis.payloadif #data>0 and direction_check(desync) and payload_check(desync) then495режется первое найденное; пустой блоб считается найденным, но резать нечегопусто: выход 3то ли направление?495if #data>0 and direction_check(desync) …по умолчанию dir=out: только пакеты от клиента наружу; in входящие, any любыеzapret-lib.lua:1294-1297 4известный ли тип нагрузки?495if #data>0 and direction_check(desync) and payload_check(desync) thenпо умолчанию payload=known: любой тип, кроме unknown и empty; all пускает и ихzapret-lib.lua:1311-1327 нет: выход нет: выход за закрытыми воротами: ничего не отправлено и ничего не возвращенодвижок читает молчание как PASS (desync.c:849-850, 867-868): оригинал уходит как есть снаружи: опция payload у вызовапо умолчанию любой тип; решает движоквнутри: аргумент payload функциипо умолчанию known; решает функция

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

Аргумент payload равно all пускает и тип unknown, но midsld на неизвестных данных не находится, только простые числа. Прогон функций со списком «два, midsld»: части в два и двести сорок восемь байт.

Прогон функций через ворота Прогон функций через ворота multisplit: мы берём настоящую функцию, подаём ей готовые данные и смотрим ответ, а в сеть ничего не уходит; тип unknown при фильтре по умолчанию, dir=in на исходящем пакете, пустой блоб 0x и отсутствующий блоб с optional дают 0 пакетов и молчание, то есть PASS, payload=all на данных unknown с pos=2,midsld даёт позицию 2 и части в 2 и 248 байт, потому что midsld на неизвестных данных не находится, а блоб из восьми байт 01-08 с pos=2,4 режется на части в 2, 2 и 4 байта; в журнале нашего опыта внешний фильтр у вызова не пускает функцию на пакеты без данных (тип empty) и пускает на приветствие tls_client_hello. прогон функций: настоящая функция, готовые данные, в сеть ничего не уходит что подаличто вышло pos=2 тип unknown0 пакетов, ответ nil: PASSpayload_check: payload 'unknown' does not pass 'known' filterворота 4 dir=in,pos=midsld пакет исходящий0 пакетов, ответ nil: PASSстрок функции нет: ворота закрыты по направлениюворота 3 payload=all,pos=2,midsld тип unknownчасти 2 и 248 байт, DROPresolved split pos: 2 (midsld на unknown не находится)ворота 4 blob=0x,pos=2 блоб пустой0 пакетов, ответ nil: PASSпустой блоб найден, но данных 0: ворота 2ворота 2 blob=0x0102030405060708,pos=2,4 вместо пакетачасти 2, 2 и 4 байта, DROPresolved split pos: 2 4ворота 2 blob=nosuchblob,optional=,pos=midsld блоба нет0 пакетов, ответ nil: PASSblob 'nosuchblob' not found. skippedдо ворот блоб из восьми байт, pos=2,4 0102030405060708 2 байта2 байта4 байта ответ функции: DROP, оригинал выброшен журнал нашего теста: внешний фильтр пакет 1, запрос соединения, данных нет:payload_type 'empty' does not satisfy filterфункцию не зовут пакет 4, приветствие:payload_type 'tls_client_hello' satisfy filterдальше desync: функцию зовут внутренний фильтр known мы смотрели только в прогонефункций; в опытах снаружи стоит tls_client_hello

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

Есть ещё такой выход. Задан флаг «необязательно», в коде 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 не меняет.

Мост из Lua в Си Мост из Lua в Си: функция multisplit в строке 500 зовёт resolve_multi_pos, это функция на Си luacall_resolve_multi_pos (nfq2/lua.c:3321-3350), в которую идут данные, имя типа нагрузки и строка меток, а четвёртый необязательный аргумент включает нулевую нумерацию и multisplit его не передаёт, обратно возвращается таблица чисел; в коде проверка числа аргументов 3 или 4 (3324), ошибка bad payload type на строках 3335-3337, которая из multisplit недостижима, массив из 128 меток (3339), ошибка bad marker list на строках 3342-3343, и она достижима, потому что одна неизвестная метка отвергает весь список, разбор ResolveMultiPos (3344) и сборка таблицы (3346-3347); значение по умолчанию задаёт строка 497 через or 2, пустая строка pos= даёт пустой список и оригинал уходит целым, в прогоне функций 128 меток проходят, а 129 дают ошибку. мост: функция на Си, которую зовёт Lua Luamultisplitantidpi:500resolve_multi_pos(data, desync.l7payload, spos) Сиlua.c:3321-3350luacall_resolve_multi_posResolveMultiPos 1 данные: что режем 2 имя типа нагрузки 3 строка меток 4 нулевая нумерация (multisplit не передаёт) результат: таблица чисел код моста по строкам (nfq2/lua.c, сокращено) 3323// resolve_multi_pos(blob,l7payload_type,marker_list[,zero_based_pos])сигнатура 3324lua_check_argc_range(L,"resolve_multi_pos",3,4);число аргументов: 3-4 3330data = lua_reqlstring(L,1,&len);1: данные 3331sl7payload = lua_reqstring(L,2);2: имя типа 3332smarkers = lua_reqstring(L,3);3: строка меток 3333bZeroBased = argc>=4 && lua_toboolean(L,4);4: нулевая нумерация 3335-37if (l7payload==L7P_INVALID) luaL_error(L,"bad payload type")недостижима отсюда 3339struct proto_pos markers[128];предел: 128 меток 3342-43if (!posmarker_list_parse(…)) luaL_error(L,"bad marker list");достижима отсюда 3344ResolveMultiPos(data,len,l7payload,markers,ctm,pos,&ctpos);метки в числа 3346-47lua_newtable(L); for(…) lua_pushi_int(L,i+1,pos[i]+!bZeroBased);таблица: след. сцена pos не написанв строке 497: desync.arg.pos or "2"or берёт правую часть, если слева ничего pos= без значенияприходит пустая строка, а она значение, не nil0 меток, оригинал уходит целым предел меток128 меток прошли, 129: bad marker listметка длиннее 31 знака: ошибка, по коду pos=foo в нашем тестежурнал: bad marker list, пакет без измененийна проводе один пакет, 250 байт

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

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

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

Измеряется то, что режется Измеряется то, что режется: функция берёт данные по порядку, сначала блоб (в нашем тесте встроенное приветствие в 680 байт), потом собранное приветствие (в другом опыте 1484 байта), потом данные пакета (250 байт); с блобом метка midsld дала 133, а не 104, и части вышли в 133 и 547 байт; настоящее приветствие в 250 байт функция выбросила, поэтому в захвате клиента пакеты 1+133 и 134+547, а потом три раза по 134+117 через 220, 632 и 1464 мс, судя по захвату это повторы хвоста настоящего приветствия, байтов с 133 по 249, которые сервер в поток не принял: он ответил на них блоками D-SACK, то есть эти номера уже считал полученными; сервер собрал 680 байт с именем www.microsoft.com, настоящее приветствие одним пакетом на провод не вышло. что режем: источники данных по порядку (масштаб один на все три) 1 блоб: arg.blob680 байт 2 собранное приветствие: reasm_data1484 байта 250 байт3 данные пакета: dis.payload, запасной блоб 680 и собранные 1484 байта взяты из разных опытов; режется первое найденное блоб режется по midsld блоб, 680 байт; имя www.microsoft.com midsld = 133 в пакете 250 байт было бы 104: считается по тому, что режут 133 байта547 байт оригинал в 250 байт выброшен (DROP): на провод идут только куски блоба захват клиента: пакеты с данными, относительные номера; расстояния не в масштабе 0 мс1+133ACK 134 0.4 мс134+547ACK 681 220 мс134+117ответ с D-SACK 632 мс134+117ответ с D-SACK 1464 мс134+117ответ с D-SACK пакеты 3-5: те же 117 байт, байты 133-249 настоящего приветствия; судя по захвату, повторы сервер: 680 байт, имя www.microsoft.com; повторы в поток не принятынастоящее приветствие одним пакетом на провод не вышло; клиент ответ получил

Второй аргумент, тип нагрузки, идёт по имени. Для 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: функция, которая вырезает кусок строки, берёт номера с единицы, и именно ей позже отдадут эти числа.

Два счёта байтов Почему в журнале 104, а цикл работает со 105: Си считает байты с нуля, Lua с единицы, поэтому мост кладёт в таблицу каждое число плюс один, и midsld это 104 в Си и 105 в Lua; смещение с нуля m = 104 это число байт слева от разреза, а позиция для Lua p = m + 1 = 105 это номер первого байта новой части; в строке nfq2/lua.c:3347 два плюса единицы, i+1 выбирает номер ячейки, а +!bZeroBased сдвигает само число и отключается четвёртым аргументом, который multisplit не передаёт; список 2, host, midsld, endhost в Си это 2, 97, 104, 112, а в Lua 3, 98, 105, 113; зачем нужен сдвиг числа, в коде не сказано, судя по коду ради string.sub в строках 505-507 lua/zapret-antidpi.lua, которая берёт номера с единицы. один и тот же байт, два счёта Си: с 0Lua: с 1 w99100.100101e101102x102103a103104m104105p105106l106107e107108.108109c109110o110111 байты имени www.example.com: 99 это третья w слева m = 104 байта новая часть: с байта p = 105 p = m + 1 мост, строка nfq2/lua.c:3347: два плюса единицы 3347for(i=0;i<ctpos;i++) lua_pushi_int(L,i+1,pos[i]+!bZeroBased); i+1: номер ячейкив Lua первая ячейка таблицыимеет номер 1 (по общим сведениям) +!bZeroBased: сдвиг самого числаотключается четвёртым аргументом (3333);multisplit его не передаёт: сдвиг включён пример: pos=2,host,midsld,endhost 2+13 97+198 104+1105 112+1113 ответ Ситаблица для Lua зачем сдвиг числа, в коде не сказано; судя по коду, ради string.sub (antidpi:505-507)505 local pos_start = pos[i] or 1506 local pos_end = i<#pos and pos[i+1]-1 or #data507 local part = string.sub(data,pos_start,pos_end)

Дальше две маленькие функции. Игрушка, пример выдуманный. Закладки в книге. Закладка перед самой первой страницей ничего не делит, и редактор её вынимает. А в журнал архивариус пишет номера страниц по-архивному, с нуля. Книга одна, счёта два.

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

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

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

Минус один и вычёркивание Две маленькие функции после моста: в multisplit строка 501 печатает в журнал список, вычитая единицу обратно с помощью zero_based_pos (zapret-lib.lua:577-584), которая делает новый массив и не трогает исходный, а строка 502 убирает из списка единицу функцией delete_pos_1 (587-597), потому что Lua-позиция 1 это разрез перед нулевым байтом и первая часть вышла бы пустой; курсор идёт по массиву 1, 5, 1, 9 и после table.remove остаётся на месте, так что результат 5, 9; журнал печатается до вычёркивания, поэтому pos=0 оставляет в журнале 0, а массив 1 становится пустым, и функция пишет no valid split positions, на проводе остаётся один пакет в 250 байт; список 0 и midsld даёт в журнале 0 и 104, а на проводе части в 104 и 146 байт. в multisplit, строки 500-503: порядок важен 500local pos = resolve_multi_pos(data, desync.l7payload, spos)pos = {105} 501DLOG("…pos: "..table.concat(zero_based_pos(pos)," "))в журнал: 104 502delete_pos_1(pos) -- cannot split at the first byteдальше цикл: без единиц 503if #pos>0 then …пусто: «нет позиций» журнал печатается до вычёркивания: в нём число Си, а в массиве число Lua zero_based_poslib 577-584новый массив, в нём число минус один;не число копируется как есть 105104 21 былостало исходный цел:цикл работает спрежним, со 105 delete_pos_1 и table.removelib 587-597 i=11519равно 1: убрать, i стоит i=15195, не 1: i = 2 i=2519равно 1: убрать, i стоит i=2599, не 1: i = 3 i=359конец: 3 > 2, итог {5, 9} опыты нашей лаборатории pos=0журнал: 0массив {1}, после вычёркивания {}no valid split positions; на проводе один пакет 1+250 pos=0,midsldжурнал: 0 104массив {1, 105}, после {105}части 104 и 146, как при одном midsld

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

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

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

Что мы знаем к этому месту: от строки до готового списка позиций

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

Первое, строка стратегии. Имя multisplit, аргумент pos со списком меток, значение приходит в Lua текстом. Без pos берётся «два», а если pos записан дважды, побеждает первый.

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

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

Четвёртое, сдвиг. Для Lua каждое число получает плюс один, а в журнал оно идёт с минус один, поэтому журнал говорит так же, как Си.

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

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

Цепочка от строки до списка позиций Цепочка от строки pos до готового списка позиций в функции multisplit: строка (lua/zapret-antidpi.lua:497), ворота (484-495), данные (494-495), мост в Си (500), плюс один (nfq2/lua.c:3347), вычёркивание единицы (502-503) и седьмой блок, цикл частей (504-507), который разберёт следующая часть выпуска, а справа четыре исхода до отправки частей: ворота закрыты, плохой список меток, метки не нашлись и всё нашлось, причём в первых трёх оригинал уходит целым, а в четвёртом его выбросят после отправки частей. цепочка multisplit до цикла частейчто может пойти иначе 1строка стратегииzapret-antidpi.lua:497pos текстом; без pos берётся «2»; два pos: первый 2воротаzapret-antidpi.lua:484-495TCP? направление (обычно наружу)? тип известен? 3данныеzapret-antidpi.lua:494-495блоб, сборка или пакет; не пусты ли? 4мост в Сиzapret-antidpi.lua:500метки в смещения с нуля, ненайденные прочь,сортировка и склейка повторов 5плюс одинlua.c:3347в Lua каждое число +1, в журнал оно идёт с −1 6вычёркиваниеzapret-antidpi.lua:502-503единицу прочь; список пуст: резать нечего 7цикл частейzapret-antidpi.lua:504-507следующая часть выпуска ворота закрыты: оригинал уходитничего не отправлено плохой список меток: оригинал уходитошибка в журнале: bad marker list метки не нашлись: оригинал уходитпустой список: no valid split positions всё нашлось: список готоворигинал выбросят после отправки частей ошибки других аргументов и самойотправки: в следующих частях Закрытые ворота, плохой и пустой список: ни одного нового пакета, оригинал уходит как есть.

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

Второй пример, из нашего опыта: midsld, два, midsld, host, два. Си возвращает два, девяносто семь и сто четыре, повторы склеены. Lua получает три, девяносто восемь и сто пять. Журнал показывает два, девяносто семь, сто четыре. Единицы нет. Список готов: три разреза, а значит четыре части.

Проверь себя на всей цепочке. Список «host минус сто». Какой из этих четырёх исходов получится?

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

Два примера и проверка на бумаге Две цепочки чисел на нашем приветствии в 250 байт и проверка на бумаге: список host+3,endhost-2 даёт в Си 100 и 110, в Lua 101 и 111, в журнале 100 110, и на проводе три части 100, 10 и 140; список midsld,2,midsld,host,2 даёт в Си 2, 97 и 104 (повторы склеены), в Lua 3, 98 и 105, и на проводе четыре части 2, 95, 7 и 146; внизу проверка host-100: 97 минус 100 равно минус 3, метка не находится, список пуст, в журнале no valid split positions, оригинал уходит целым, это третий исход, посчитано по коду и не запускалось. шаг цепочкипример 1пример 2 список poshost+3,endhost-2midsld,2,midsld,host,2 Си: с нуля100, 1102, 97, 104 (повторы склеены) Lua: +1{101, 111}{3, 98, 105} журналresolved split pos: 100 110resolved split pos: 2 97 104 вычёркиваниеединицы нетединицы нет части на проводе100, 10, 1402, 95, 7, 146 пример 1: приветствие 250 байт, разрезы на 100 и 11010010140 пример 2: разрезы на 2, 97 и 1042957146 проверь себясписок host-100 на том же приветствии: какой из четырёх исходов? host − 10097 − 100 = −3 меткане нашлась списокпуст журналno valid split positions оригиналуходит целым Это третий исход: метки не нашлись. Счёт по коду (CheckPos, protocol.c:182-185), такой список мы не запускали.Для обоих примеров есть журнал и провод нашей лаборатории; host-100 только расчёт.

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

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

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

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

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

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

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

Второе, из выпуска «Где резать: метки». Где резать, говорит список меток, через запятую. Метка это разрез перед определённым байтом. Одни метки просто числа, другие имена. Имя host это начало имени сайта, midsld это середина слова example. На нашем приветствии, считая с нуля, host это девяносто седьмой байт, а midsld сто четвёртый. Ещё два имени: sld, начало слова example, сто первый байт, и endhost, место сразу за именем сайта, сто двенадцатый.

Третье, из первой части. Метки в числа превращает функция на языке Си. Что не нашлось, она тихо выбрасывает, остальное сортирует и убирает повторы.

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

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

Повтор: от метки до двух пакетов Повтор перед циклом на нашем приветствии в 250 байт: засечки считаются с нуля, host на 97, sld на 101, midsld на 104 и endhost на 112, список pos=midsld в Си даёт 104, в Lua 105, после вычёркивания единицы остаётся одно число 105, и на проводе приветствие уходит двумя пакетами по 104 и 146 байт с порядковыми номерами 1 и 105, а исходный пакет в 250 байт выбрасывается. приветствие ClientHello, 250 байт, счёт с нуля host 97 sld 101 midsld 104 endhost 112 www.example.comбайт 0байт 249 строкаpos=midsld Си: с нуля104 Lua: с единицы105 (104 + 1) вычёркивание единицыостаётся {105} результат в нашем опытеисходный пакет, 250 байт: выброшен (drop) часть 1: байты 0-103 часть 2: байты 104-249 104 байта, номер 1146 байт, номер 105 журнал: resolved split pos: 104; part 1 0-103 len=104; part 2 104-249 len=146; drop

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

Чёрный ящик и четыре шага Чёрный ящик между числом 105 и двумя пакетами: ядро держит исходный пакет и ждёт вердикта, программа получает копию через очередь, внутри функции multisplit лежит путь, который открывает эта часть, части уходят в сеть через сырой сокет мимо очереди, а вердикт drop возвращается ядру; пути четыре шага (цикл частей в lua/zapret-antidpi.lua:504-507, номер пакета в 522, часть длиннее предела в zapret-lib.lua:1246-1273, порядок и пауза), шаги 1 и 2 идут в одном цикле, а проверка по повтору для pos=endhost: 112 плюс 1 равно 113, один разрез, две части по 112 и 138 байт, расчёт без чистого опыта. где мы сейчасчёрный ящик: этот путь мы открываем ядрооригинал, 250 байтждёт вердикта копия черезочередь программа: функция Luamultisplit(ctx, desync)список {105} → ? → пакеты свои пакеты:сырой сокет в сеть104 байта146 байтв очередь не попадают вердикт: оригинал выбросить (drop) четыре шага этой части 1цикл превращает список позиций в частиzapret-antidpi.lua:504-507 2каждая часть становится пакетом с нужным номеромzapret-antidpi.lua:522 3что будет, если часть не влезает в один пакетzapret-lib.lua:1246-1273 4в каком порядке и с какой паузой уходят частиzapret-antidpi.lua:504-525 шаги 1 и 2в одном цикле проверь себя по повтору строкаpos=endhost для Lua112 + 1 = 113 разрезоводин: две части байты частей112 и 250 − 112 = 138 расчёт: чистым опытом этот список не запускали, только с игрушечным правилом на роутере (последняя часть).

Проверь себя по повтору. В строке написано просто endhost. Какое число придёт в Lua, сколько будет частей и какого размера?

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

Итак, мы знаем, как строка с метками становится списком позиций. Теперь выясним, как список становится частями, а части пакетами.

Цикл по частям: четыре строки, которые режут данные

Вопрос этой сцены: у нас на руках список чисел для Lua, например одно число, сто пять. Как из него получаются части данных?

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

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

Игрушка: лента из десяти клеток Игрушка, выдуманный пример: бумажная лента из десяти клеток, пронумерованных с единицы, и список разрезов 4 и 8 дают три части, клетки 1-3, 4-7 и 8-10; часть начинается там, где стоит её разрез, и кончается перед следующим разрезом (у первой своего разреза нет, она начинается с клетки 1, у последней следующего нет, она идёт до клетки 10), и разрезов на один меньше, чем частей. игрушка: выдуманный пример, не настоящий пакет 12345678910клетки пронумерованы с единицы список разрезов: 4, 8 разрез 4 разрез 8 клетки 1-3 клетки 4-7 клетки 8-10 правилоначало частиконец части часть 11: нет разреза, с начала ленты4 − 1 = 3 часть 24: здесь её разрез8 − 1 = 7 часть 38: здесь её разрез10: следующего разреза нет, до конца ленты Часть начинается там, где стоит её разрез, и кончается перед следующим разрезом.Два разреза, три части: разрезов на один меньше, чем частей.Дальше то же самое делает настоящий код, на настоящих байтах.

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

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

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

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

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

Цикл по частям: четыре строки Первые четыре строки цикла multisplit (lua/zapret-antidpi.lua:504-507) на данных в 250 байт и списке Lua из одного числа 105: цикл идёт от нуля до числа разрезов, значит два прохода; на проходе 0 начало 1 (элемента с номером ноль в списке нет), конец 105 минус 1 равно 104, и string.sub вырезает байты с 1 по 104, то есть 104 байта; на проходе 1 начало 105, конец равен длине данных 250, и вырезаются байты с 105 по 250, то есть 250 минус 105 плюс 1 равно 146 байт; 104 плюс 146 равно 250; журнал считает с нуля (строка 521) и пишет part 1 0-103 len=104 и part 2 104-249 len=146; проверка на списке из одного числа 250: части 1..249 и 250..250, в опыте с меткой 249 сервер получил 249 байт и потом 1. multisplit: цикл частей, lua/zapret-antidpi.lua:504-507данные 250 байт, список Lua {105} 504for i=0,#pos do505local pos_start = pos[i] or 1506local pos_end = i<#pos and pos[i+1]-1 or #data507local part = string.sub(data,pos_start,pos_end)508-525: журнал и отправка, дальше проход i=0проход i=1разрезов 1, проходов 2(цикл от 0 до #pos) pos[i]нет элемента105 pos_start1105 pos_end105 − 1 = 104#data = 250 вырезано104 байта146 байт лента 250 байт, номера Lua с единицы часть 1: байты 1..104разрез 105 часть 2: байты 105..250 104 + 146 = 250: ни один байт не потерян и не взят дважды журнал опыта: строка 521, внутри того же цикла, а за ней отправка (522) sending part 1 0-103 len=104sending part 2 104-249 len=146журнал считает с нуляи пишет оба края проверь себя: в Lua пришёл список {250}, данные те же, 250 байт проход 0: начало 1, конец 250 − 1 = 249 проход 1: начало 250, конец 250: один байт 249 и 1в нашем опыте с меткой 249: на проводе номера 1 и 250, сервер получил 249 байт и потом 1

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

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

Проверь себя. В списке, который пришёл в Lua, одно число, двести пятьдесят, данные те же, двести пятьдесят байт. Какие получатся части?

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

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

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

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

Журнал печатает позиции так, как их считает Си, с нуля: два, девяносто семь, сто четыре и сто двенадцать.

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

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

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

Третий проход, счётчик равен двум. Начало девяносто восемь. Конец сто пять минус один, сто четыре. Семь байт. Это «www точка exa», начало имени сайта.

Четвёртый проход. Начало сто пять, конец сто тринадцать минус один, сто двенадцать. Восемь байт, «mple точка com».

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

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

Пять проходов цикла по частям Пять проходов цикла на нашем приветствии в 250 байт со списком меток 2,host,midsld,endhost: Си считает с нуля 2, 97, 104, 112, Lua получает 3, 98, 105, 113, четыре разреза дают пять частей и пять проходов; проход 0 начинается с 1 и кончается на 3 минус 1 равно 2 (2 байта), проход 1 с 3 до 97 (95 байт), проход 2 с 98 до 104 (7 байт, www.exa), проход 3 с 105 до 112 (8 байт, mple.com), проход 4 с 113 до длины данных 250 (138 байт), а сумма 2 плюс 95 плюс 7 плюс 8 плюс 138 равна 250. метки pos=2,host,midsld,endhost Си считает с нуля: 2, 97, 104, 112 Lua получает (+1): 3, 98, 105, 113 Lua 3 Lua 98 Lua 105 Lua 113 29578138пять частей, размеры в байтах часть 3, 7 байт: 77 77 77 2E 65 78 61 = www.exa часть 4, 8 байт: 6D 70 6C 65 2E 63 6F 6D = mple.com проходpos_startpos_endбайт в части i = 013 − 1 = 22 − 1 + 1 = 2 i = 1398 − 1 = 9797 − 3 + 1 = 95 i = 298105 − 1 = 104104 − 98 + 1 = 7 i = 3105113 − 1 = 112112 − 105 + 1 = 8 i = 4113#data = 250250 − 113 + 1 = 138 сумма: 2 + 95 + 7 + 8 + 138 = 250ни один байт не потерян и не взят дважды

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

Помним договорённость из выпуска «Что видит наблюдатель»: номера считаем от начального номера, SYN получает номер ноль, а первый байт данных номер один.

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

Второй: номер три, длина девяносто пять, дальше девяносто восемь.

Третий: номер девяносто восемь, длина семь, дальше сто пять.

Четвёртый: номер сто пять, длина восемь, дальше сто тринадцать.

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

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

Номера пакетов на проводе Что ушло на провод при тех же пяти частях (пять пакетов, клиент, относительные номера от начального: SYN получает 0, первый байт данных 1): номер и длина 1 и 2, 3 и 95, 98 и 7, 105 и 8, 113 и 138, где номер каждого следующего пакета равен номеру плюс длина предыдущего (1+2=3, 3+95=98, 98+7=105, 105+8=113, 113+138=251), сервер подтвердил 251, а номер каждого пакета совпал с позицией Lua, с которой начинается его часть, потому что приветствие это первые данные соединения, а в общем случае номер равен номеру исходного пакета плюс позиция минус один (zapret-antidpi.lua:522, zapret-lib.lua:1287); внизу проверка на бумаге для sld,endsld: Lua 102 и 109, части 101, 7 (слово example) и 142, номера 1, 102 и 109, расчёт без опыта. договорённость: номера считаем от начального SYN: номер 0данные приветствия: байты с номерами 1 … 250251: следующий байт пять пакетов клиента, в порядке отправки пакет 1номер 1длина 21 + 2 = 3 пакет 2номер 3длина 953 + 95 = 98 пакет 3номер 98длина 798 + 7 = 105 пакет 4номер 105длина 8105 + 8 = 113 пакет 5номер 113длина 138113 + 138 = 251 =позиция Lua 1 =позиция Lua 3 =позиция Lua 98 =позиция Lua 105 =позиция Lua 113 в общем случае: номер исходного пакета + позиция − 1подтверждение сервера: 251 Совпало потому, что приветствие это первые данные соединения: номер его первого байта 1. zapret-antidpi.lua:522rawsend_payload_segmented(desync, part, pos_start-1-seqovl)zapret-lib.lua:1287dis.tcp.th_seq = dis.tcp.th_seq + seq проверь себя на бумаге: метки sld,endsldрасчёт: такой список в лаборатории не запускали Си: с нуля101, 108 Lua: +1102, 109 части101, 7, 142 номера1, 102, 109 Часть из 7 байт это ровно слово example (байты 101-107). 101 + 7 + 142 = 250.Номер части равен её позиции Lua: 1, 102 и 109.

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

Для Lua позиции сто два и сто девять. Две позиции, три части. Первая: сто один байт. Вторая: со сто второго по сто восьмой, семь байт, и это ровно слово «example». Третья: со сто девятого до конца, двести пятьдесят минус сто девять плюс один, сто сорок два. Номера пакетов: один, сто два и сто девять. Это расчёт: именно такой список в лаборатории не запускали.

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

Края, много разрезов и запрос HTTP: от одного байта до двадцати пяти частей

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

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

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

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

Края данных и семь имён подряд Края данных и семь меток подряд на нашем приветствии в 250 байт: pos=1 даёт части 1 и 249 байт с номерами 1 и 2, pos=249 даёт 249 и 1 с номерами 1 и 250, pos=-10 считается от конца (250 минус 10 равно 240) и даёт 240 и 10 с номерами 1 и 241, а семь меток extlen, sniext, host, sld, midsld, endsld и endhost с позициями 86, 92, 97, 101, 104, 108 и 112 дают семь разрезов и восемь пакетов по 86, 6, 5, 4, 3, 4, 4 и 138 байт, где самый маленький в 3 байта это exa; пустых частей не бывает, а коротких бывает сколько угодно. края данных (повтор): три опыта, приветствие 250 байт pos=1части 1 и 249 байтномера на проводе 1 и 2 pos=249части 249 и 1 байтномера на проводе 1 и 250 pos=-10250 − 10 = 240части 240 и 10 байтномера на проводе 1 и 241 Для цикла края ничего не значат: пустых частей не бывает, а коротких бывает сколько угодно. семь меток подряд: семь разрезов, восемь пакетов extlen 86 sniext 92 host 97 sld 101 midsld 104 endsld 108 endhost 112 86 6 5 4«www.» 3«exa» 4«mple» 4«.com» 138 86 + 6 + 5 + 4 + 3 + 4 + 4 + 138 = 250. Самый маленький пакет 3 байта (exa).Сервер нашей лаборатории принял все восемь. Смысл имён: выпуск «Где резать: метки».

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

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

Много частей и предел меток Много частей и предел: игрушечный батон с 24 надрезами даёт 25 кусков, всегда на один больше, а настоящий список 10,20 и так далее до 240 из 24 чисел режет приветствие в 250 байт на 25 пакетов по 10 байт, номера на проводе 1, 11, 21 и так до 241, и сервер нашей лаборатории прочитал 25 раз по 10 байт; в прогоне функций 128 меток дают 128 позиций, а 129 меток дают ошибку bad marker list (nfq2/lua.c:3339-3343, nfq2/protocol.c:149-180), значит частей не больше 129 (расчёт, в сети не пробовали); проверка на бумаге: метки 10,20 и так до 100 дают десять разрезов и одиннадцать частей, десять по 10 байт и последнюю 250 минус 100 равно 150. игрушка: выдуманный батон 24 надреза дают 25 кусков: всегда на один больше опыт: pos=10,20,…,240 (24 числа) на приветствии 250 байт 10101010101010101010101010101010101010101010101010 на проводе клиента 25 пакетов по 10 байт, номера 1, 11, 21, … 241сервер нашей лаборатории (Linux) прочитал 25 раз по 10 байт; 25 × 10 = 250 предел списка меток: прогон функций, в сети не пробовали метка 1метка 128метка 129 128 меток: 128 позицийчастей не больше 129 (расчёт) 129 меток: ошибка bad marker listnfq2/lua.c:3339-3343, nfq2/protocol.c:149-180 проверь себя: метки 10,20,…,100 (десять меток) 10101010101010101010250 − 100 = 150 десять разрезов, одиннадцать частей: десять по 10 байт и последняя 150.Цикл не знает, много разрезов или мало: он просто идёт по списку.

Те же метки на другом протоколе. Запрос HTTP на восемьдесят один байт: первая строка читается так: «гет, слэш, индекс точка эйч-ти-эм-эль, эйч-ти-ти-пи один точка один». Дальше строка Host с именем «www точка example точка com». Метки «host, midsld». Считая с нуля, host это тридцать второй байт, midsld тридцать девятый. Три части: тридцать два байта, семь и сорок два.

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

Те же метки на запросе HTTP Те же метки на запросе HTTP в 81 байт: запрос GET /index.html HTTP/1.1 с заголовком Host: www.example.com режется метками host и midsld, считая с нуля host это байт 32 (сразу после Host: с пробелом, перед именем), midsld это байт 39 (внутри слова example), получается три части по 32, 7 и 42 байта, средняя это www.exa, а третья начинается с mple.com; в журнале resolved split pos: 32 39, в Lua 33 и 40, на проводе клиента номера и длины 1 и 32, 33 и 7, 40 и 42, и сервер нашей лаборатории прочитал 32, 7 и 42 байта. запрос HTTP в нашей лаборатории, 81 байт, метки host,midsld GET /index.html HTTP/1.1··Host: www.example.com··User-Agent: lab··Accept: */*····байт 0байт 80 host 32 midsld 39 · это байты перевода строки (CR LF); цифры над засечками считаются с нуля часть 1: 32 байта 7 байт часть 3: 42 байта байты 0-31кончается на «Host: »номер 1 32-38«www.exa»номер 33 байты 39-80начинается с «mple.com»номер 40 журналresolved split pos: 32 39 Lua получает (+1)33, 40 провод клиента (номер+длина)1+32, 33+7, 40+42 сервер прочитал32, 7, 42 Первый разрез стоит сразу после «Host: », перед именем. Второй внутри слова example, как и в приветствии.

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

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

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

Что мы знаем: от строки pos до списка частей

Соберём то, что знаем к этой минуте, и пройдём цепочку целиком.

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

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

Три примера подряд. Строка «midsld»: Си даёт сто четыре, Lua сто пять, части сто четыре и сто сорок шесть. Строка «два, host, midsld, endhost»: Си даёт два, девяносто семь, сто четыре, сто двенадцать, Lua три, девяносто восемь, сто пять, сто тринадцать, части два, девяносто пять, семь, восемь и сто тридцать восемь. Номера на проводе в нашем тесте: у первой строки один и сто пять, у второй один, три, девяносто восемь, сто пять и сто тринадцать.

Проверь себя на целой цепочке. Строка «host, host, один, midsld». Что напечатает журнал, что придёт в Lua и какие части уйдут в сеть?

Повтор пропадает, метки сортируются. Журнал: один, девяносто семь, сто четыре. В Lua два, девяносто восемь, сто пять. Метку «один» не выбросили: это Lua-позиция два, а выбрасывается только позиция один. Три разреза, четыре части: один байт, девяносто шесть, семь и сто сорок шесть. Номера на проводе: один, два, девяносто восемь и сто пять. Это расчёт: такой список в лаборатории не запускали.

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

Четыре списка в цепочке Четыре списка меток проходят всю цепочку: midsld на приветствии в 250 байт даёт числа Си 104, позиции Lua {105}, части 104 и 146 и номера на проводе 1 и 105; список 2,host,midsld,endhost даёт 2 97 104 112, затем {3, 98, 105, 113}, части 2, 95, 7, 8 и 138 и номера 1, 3, 98, 105 и 113; на запросе HTTP в 81 байт метки host,midsld дают 32 и 39, Lua 33 и 40 по правилу плюс один, части 32, 7 и 42 и номера 1, 33 и 40; последняя строка, host,host,1,midsld, это расчёт на бумаге, который в лаборатории не запускали: 1 97 104, затем {2, 98, 105}, части 1, 96, 7 и 146 и номера 1, 2, 98 и 105, причём метка 1 остаётся, потому что выбрасывается только Lua-позиция 1. три настоящих запуска и один расчёт: числа Си, позиции Lua, части, провод midsldлаборатория, 250 байт Си, с нуля: 104Lua, плюс 1: {105}провод: 1, 105 104146части: 104 + 146 = 250 2,host,midsld,endhostлаборатория, 250 байт Си, с нуля: 2 97 104 112Lua, плюс 1: {3, 98, 105, 113}провод: 1, 3, 98, 105, 113 95138части: 2 + 95 + 7 + 8 + 138 = 250 host,midsldлаборатория, запрос HTTP, 81 байт Си, с нуля: 32 39Lua, плюс 1: {33, 40}*провод: 1, 33, 40 3242части: 32 + 7 + 42 = 81 * Lua-позиции по правилу +1: в прогоне функций на HTTP их не печатали host,host,1,midsldрасчёт: в лаборатории не запускали Си, с нуля: 1 97 104Lua, плюс 1: {2, 98, 105}провод: 1, 2, 98, 105 96146части: 1 + 96 + 7 + 146 = 250 метка 1 остаётся: это Lua-позиция 2; выбрасывается только Lua-позиция 1 полосы нарисованы в одном масштабе: 2,2 пикселя на байт

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

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

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

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

Отправщик части: копия пакета, новые данные, сдвиг номера

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

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

Конверт и поля пакета Выдуманный конверт-шаблон с окнами «от кого и кому», «штампы», «письмо» и «со страницы N» копируется, в копию кладут другой листок и исправляют номер страницы, остальное остаётся прежним, и то же самое делает отправщик части с полями пакета: адреса, порты, флаги TCP, окно, номер подтверждения и опции копируются с обрабатываемого пакета, меняются данные (часть) и порядковый номер (исходный номер плюс сдвиг), идентификатор IP ставится позже при отправке; для второй части нашего опыта копия несёт 146 байт данных и номер T + 104, а исходный пакет с 250 байтами и номером T не меняется. игрушка и настоящие поля: что копируется, что меняется выдуманный пример: конверт-шаблон шаблонот кого, комуштампыписьмосо страницы N копия копияот кого, кому: те жештампы: те жеписьмо: другой листоксо страницы N + 104 меняется письмо иномер страницы,остальное копия окно игрушкиполе настоящего пакетав копии от кого, комуадреса IP и портыкак у исходного штампыфлаги TCP, окно, номер подтверждения, опции TCPкак у исходного письмоданные пакетаизменено: часть со страницы Nпорядковый номер th_seqизменено: T + сдвиг нет в игрушкеидентификатор IP, ip_idставится при отправке вторая часть нашего опыта: 146 байт данных, сдвиг 104 исходный пакет: не меняетсяth_seq = T · данные: 250 байт копия для второй частиth_seq = T + 104 · данные: 146 байт

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

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

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

Вторая строка: если часть передана, она ставится в копию на место прежних данных. Всё остальное в копии прежнее: адреса, порты, флаги, окно, номер подтверждения, параметры TCP.

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

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

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

Отправщик части: по строкам Отправщик части rawsend_payload_segmented (lua/zapret-lib.lua:1282-1290, девять строк) по строкам: строка 1284 решает делать ли копию таблицы desync, если дана часть данных или сдвиг не ноль, строка 1285 кладёт часть в копию, строки 1286-1288 для TCP прибавляют сдвиг к порядковому номеру, строка 1289 передаёт копию функции с нарезкой без размера порции; сдвиг multisplit вычисляет как pos_start-1-seqovl, для второй части 105-1-0=104, для первой 1-1-0=0, а исходный пакет с 250 байтами и номером T остаётся целым; прогон функций с отправкой, заменённой записью, показал: три байта ABC со сдвигом 7 дают один пакет в 3 байта с номером T+7, вызов без данных и без сдвига повторяет исходные 250 байт, после вызовов исходная таблица цела, а если отправка не удалась, multisplit возвращает вердикт пропустить и уже отправленные части остаются на проводе. rawsend_payload_segmented · lua/zapret-lib.lua:1282-1290 1282function rawsend_payload_segmented(desync, payload, seq, options) 1283-- save some cpu and ram 1284local dis = (payload or seq and seq~=0) and deepcopy(desync.dis) or desync.dis 1285if payload then dis.payload = payload end 1286if dis.tcp and seq then 1287 dis.tcp.th_seq = dis.tcp.th_seq + seq 1288end 1289return rawsend_dissect_segmented(desync, dis, nil, options) 1290end 1 2 3 4 1копия нужна при данных илиненулевом сдвиге: исходнуютаблицу портить нельзя 2часть кладётся в копиювместо прежних данных 3только TCP: номер копиирастёт на сдвиг, простоесложение 4копия идёт в функцию снарезкой; размер порциией не передают что получилось для второй части нашего опыта исходный пакет: целth_seq = Tданные: 250 байт копия для части 2th_seq = T + 104данные: 146 байт сдвиг: pos_start - 1 - seqovlчасть 2: 105 − 1 − 0 = 104часть 1: 1 − 1 − 0 = 0 вызов в multisplit:lua/zapret-antidpi.lua:522 прогон функций: настоящая библиотека, отправка заменена записью «ABC», сдвиг 7один пакет: 3 байта, номер T + 7 без данных и без сдвигаповтор исходного пакета: 250 байт исходная таблица после вызовов250 байт и номер T: не тронута отправка не удалась: multisplit возвращает вердикт «пропустить» (zapret-antidpi.lua:523)ушедшие части остаются на проводе, исходный пакет идёт целым; видели только в прогоне функций

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

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

Проверь себя. Для второй части, от сто пятого байта, какой сдвиг получит отправщик?

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

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

Номера на проводе: от начального номера до подтверждения сервера

Вопрос этой сцены: какие номера стоят в пакетах частей, и как на них отвечает сервер? Разберём запись одного прогона с одним разрезом, на метке midsld.

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

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

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

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

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

Проверим другим путём. Семьсот семь плюс все двести пятьдесят байт тоже дают девятьсот пятьдесят семь.

Номера частей на проводе Номера двух пакетов опыта с одним разрезом midsld: на выдуманном письме из 250 страниц вторая пачка начинается со страницы 105, то есть 1 + 104, и получатель отвечает «жду страницу 105»; в настоящем опыте относительные номера такие: пакет 1 с номером 1 и длиной 104, пакет 2 с номером 105 и длиной 146, сервер подтвердил сначала 105, потом 251, то есть 1 + 250; в настоящих номерах у исходного пакета, части 1, части 2 и подтверждения общее начало 1 196 648, а меняются три последние цифры: 707, затем 707 + 104 = 811, затем 811 + 146 = 957, и проверка другим путём 707 + 250 = 957. номера в пакетах частей: один разрез, метка midsld, запись настоящего опыта выдуманный пример: письмо на 250 страниц пачка 1: страницы 1–104 пачка 2: со страницы 105 = 1 + 104 получатель: «жду страницу 105» настоящие пакеты: относительные номера, первый байт данных = 1 данные, байты 0–103данные, байты 104–249 пакет 1номер 1, длина 104 пакет 2номер 105, длина 146 сервер: подтверждение 105 сервер: подтверждение 251 (1 + 250) в записи постоянного опыта: после первой части 105, после второй 251 настоящие номера из того же теста: общее начало и три последние цифры 1 196 648общая частьу всех трёх чисел 707исходный пакет и часть 1 811часть 2: 707 + 104 = 811 957подтверждение сервера: 811 + 146 = 957 проверка вторым путём: 707 + 250 = 957 полные номера: 1196648707, 1196648811, 1196648957

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

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

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

Размер и край шкалы Размер пакета в журнале это 20 байт заголовка IP плюс 32 байта заголовка TCP (20 обязательных и 12 байт опций) плюс данные, поэтому у частей нашего опыта 20 + 32 + 104 = 156 и 20 + 32 + 146 = 198, именно эти размеры стоят в строках журнала size=156 и size=198; по общим сведениям порядковый номер TCP занимает 32 бита и после самого большого значения счёт идёт с нуля, как у минутной стрелки после 59, и в прогоне функций с номером исходного пакета 4294967280, на 16 меньше края шкалы, прибавка 104 даёт в Lua сумму 4294967384, а в собранном пакете стоит 88, потому что лишнее отбрасывает код на Си: 16 шагов до нуля плюс 88 после нуля равно 104, в живом опыте такого не было. размер пакета в журнале и переход номера через край размер пакета: IP + TCP + данные IPTCPданные 104 20 + 32 + 104 = 156в журнале: size=156 IPTCPданные 146 20 + 32 + 146 = 198в журнале: size=198 32 = 20 + 12 байт опций (из чего 12, в следующей сцене) выдуманный пример: минутная стрелка590после 59снова 0 номер TCP: 32 бита по общим сведениям, после наибольшего счёт идёт с нуля …429496729501 край шкалы 4294967280номер исходного пакета 16 шагов до нуля 88 после нуля88 в собранном пакете 4294967280 + 104 = 4294967384: в Lua сумма выходит за край (zapret-lib.lua:1287, без маски)16 + 88 = 104; лишнее отбрасывает код на Си, когда собирает пакет (nfq2/lua.c:2269)проверено только в прогоне функций, в живом опыте такого не было

Проверь себя на бумаге. Метки «host» и «endhost»: части в девяносто семь, пятнадцать и сто тридцать восемь байт. У первого пакета хвост номера семьсот семь. Какой хвост у второго и третьего и какое подтверждение придёт в конце?

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

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

Ворота размера: порция, опции и максимум данных в пакете

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

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

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

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

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

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

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

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

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

Порция и максимум данных Откуда берётся максимум данных в одном пакете: программа на Си считает tcp_mss как меньшее из двух чисел MSS, объявленных в SYN, а если одного нет, берёт запасное 1220, в нашей лаборатории оба SYN несут MSS=1460, по общим сведениям 1460 + 20 + 20 = 1500, и большое приветствие шло двумя пакетами по 1448 и 36 байт; функция считает лишние байты заголовков extra_len, это опции TCP, округлённые до кратного четырём, у нас две пустышки по байту и метка времени в 10 байт, итого 12, и вместе с 20 обязательными получается 32 байта заголовка TCP, максимум данных равен 1460 - 12 = 1448, ровно столько в полных пакетах нашей лаборатории; выдуманная коробка на десять мест, где два места занимает упаковка, показывает ту же арифметику: 10 - 2 = 8 мест под товар, и двадцать штук раскладываются как 8 + 8 + 4. откуда берётся максимум данных в одном пакете порция tcp_mss: считает программа на Сиnfq2/desync.c:1021-1030 · nfq2/params.h:46 SYN клиента: MSS=1460SYN сервера: MSS=1460 меньшее из двухtcp_mss = 1460 нет одного числа: запасное 1220 (DEFAULT_MSS) порция и размер пакета1460 + 20 + 20 = 1500по общим сведениям: привычный размер пакета в нашей лаборатории большое приветствиешло двумя пакетами данных:1448 и 36 выдуманный пример: коробка на десять мест коробка 1: 8 товаров коробка 2: 8 товаров коробка 3: 4 товара 10 − 2 = 8 мест, 20 штук: 8 + 8 + 4 лишние байты заголовков: extra_lenzapret-lib.lua:1164-1186 1 1 метка времени, 10 байт NOP NOP 1 + 1 + 10 = 12; с 20 обязательными: 32, заголовок TCP у NOP только байт вида, у остальныхвид, длина и данные; сумма округляетсядо кратного четырём максимум данных в пакетеmax_data = mss - extra_len (zapret-lib.lua:1243) 1460 − 12 = 1448 столько в полных пакетах нашей лаборатории;20 + 32 + 1448 = 1500 порция в речи: опции вычитаются из неё, то есть это наибольшее число байт данных в одном пакете

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

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

Ворота размера по строкам Ворота размера в отправщике с нарезкой rawsend_dissect_segmented (lua/zapret-lib.lua:1233-1279) по строкам: 1235-1236 рабочая копия и настройки из аргументов строки стратегии, 1237 подделки применяются один раз на каждую часть и до проверки размера, 1239-1240 порция tcp_mss, 1241 лишние байты опций extra_len, 1242-1243 если extra_len не меньше mss, возвращается ложь и ничего не отправляется, иначе max_data равно mss минус extra_len, а на строке 1246 развилка: часть не длиннее max_data уходит одним пакетом, длиннее режется на ломтики; в прогоне функций порция 12 при опциях 12 не отправила ни одного пакета и вернула вердикт пропустить, порция 13 дала 250 пакетов по одному байту, порция 100 дала максимум 88; расчёт на проверку: порция 60 даёт максимум 48, часть в 104 байта режется как 48, 48 и 8, часть в 146 байт как 48, 48, 48 и 2, всего семь пакетов, и в прогоне функций их было ровно семь. rawsend_dissect_segmented · lua/zapret-lib.lua:1233-1279 1235local discopy = deepcopy(dis) 1236options = options or desync_opts(desync) 1237apply_fooling(desync, discopy, options and options.fooling) 1239if dis.tcp then 1240 mss = mss or desync.tcp_mss 1241 local extra_len = l3l4_extra_len(discopy) 1242 if extra_len >= mss then return false end 1243 local max_data = mss - extra_len 1246 if #discopy.payload > max_data then 1 2 3 4 5 6 переменные mss1460 extra_len12 max_data1448 #payload104 или 146 1копия; настройки из аргументовстроки стратегии 2подделки (подмена полей): раз начасть, до проверки размера 3порция: меньшее из двух SYN,иначе запасное 1220 4опции TCP: 12 байт 5ворота и максимум данных 6развилка: часть длиннее max_data? три исхода у ворот extra_len >= mssreturn false: ничего неотправлено, multisplitвернёт «пропустить» часть не длиннее max_dataстроки 1276-1278:идентификатор IP, один пакет;так ушли обе наши части часть длиннее max_dataнарезка на ломтики поmax_data: следующая сцена прогон функций: порция задана вручную, отправка заменена записью порция 12, опций 12: 12 >= 12ни одного пакета, вердикт «пропустить» порция 13: 13 − 12 = 1250 пакетов по одному байту порция 100: 100 − 12 = 88части 104 и 146 режутся по 88 проверь себя: порция 60, опций 12максимум 60 − 12 = 48 часть 104484848 + 48 + 8 = 104 часть 14648484848 + 48 + 48 + 2 = 146 всего 7 пакетов: в прогоне их было ровно 7

А если часть не длиннее максимума? Тогда резать нечего. Ставится идентификатор IP, и часть уходит одним пакетом. В нашем тесте так уходят обе части.

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

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

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

Часть длиннее предела: нарезка, номера и идентификаторы IP

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

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

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

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

Цикл нарезки на ломтики Цикл нарезки в rawsend_dissect_segmented (lua/zapret-lib.lua:1246-1273): указатель pos ставится на первый байт части, пока он не вышел за конец, берётся len байт, сколько осталось, но не больше max_data, кусок вырезается через string.sub, ему назначается идентификатор IP через apply_ip_id, он отправляется, затем th_seq и pos сдвигаются на len, а одиннадцать строк про срочный флаг пропущены; на второй части нашего опыта в 1482 байта при максимуме 1448 первый проход берёт 1448 байт с номером 3 и идентификатором 24610, второй проход берёт остаток 34 байта с номером 1451 и идентификатором 24611, после чего pos равен 1483 и цикл кончается; выдуманный чемодан показывает то же: следующая сумка начинается с листа 3 + 1448 = 1451; по умолчанию apply_ip_id работает в режиме seq: первый пакет берёт идентификатор исходного, если раньше в цепочке ничего не отправляли, каждый следующий на единицу больше. цикл нарезки · lua/zapret-lib.lua:1246-1273 выдуманный пример: чемодан в 1482 листа, сумка вмещает 1448 сумка 1: листы 3–1450, всего 1448сумка 2: с листа 1451 следующая сумка начинается с того листа, где кончилась прошлая: 3 + 1448 = 1451 1247local pos=1 1250while pos <= #payload do 1251 len = #payload - pos + 1 1252 if len > max_data then len = max_data end 1253 срочный флаг: строки 1253-1263 пропускаем 1264 discopy.payload = string.sub(payload,pos,pos+len-1) 1265 apply_ip_id(desync, discopy, options and options.ipid) 1266 if not rawsend_dissect_ipfrag(discopy, options) then 1270 discopy.tcp.th_seq = u32add(discopy.tcp.th_seq, len) 1271 pos = pos + len 1272end 1 2 3 4 5 6 часть 2 нашего опыта1482 байта, max_data = 1448номера относительные проход 1pos = 1, len = 1448th_seq 3, ip_id 24610→ th_seq 1451, pos 1449 проход 2pos = 1449, len = 34th_seq 1451, ip_id 24611→ th_seq 1485, pos 1483 pos 1483 > 1482: выходпоследний ломтик: остаток apply_ip_id · zapret-lib.lua:1086-1107dis.ip.ip_id = desync.ip_id or dis.ip.ip_iddesync.ip_id = dis.ip.ip_id + 1строки 1098-1099, режим seq (по умолчанию) первый пакет берёт номер исходного, еслираньше в цепочке ничего не отправляли;каждый следующий на 1 большережимы zero и rnd не разбираем; только IPv4

Теперь реальный опыт. Большое приветствие на тысячу четыреста восемьдесят четыре байта, склеенное программой из нескольких пакетов; как она склеивает и проигрывает такие приветствия, разбирает выпуск «Сборка и повторный проигрыш». Стратегия: multisplit с позицией «два». Части: два байта и тысяча четыреста восемьдесят два.

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

На проводе три пакета. Первый: номер один, длина два.

Второй: номер три, длина тысяча четыреста сорок восемь; три это один плюс два.

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

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

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

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

Опыт и ломтики по максимуму Опыт с большим приветствием в 1484 байта и стратегией multisplit с позицией 2 даёт части 2 и 1482, в журнале для части 1 одна строка отправки с размером 54, а для части 2 две, с размерами 1500 и 86, то есть 20 + 32 + 1448 и 20 + 32 + 34; на проводе три пакета: номер 1 длина 2 идентификатор 24609, номер 3 длина 1448 идентификатор 24610, номер 1451 длина 34 идентификатор 24611, сервер подтвердил сначала 3, потом 1485, это 1451 + 34 и 1 + 1484, а приложение сервера прочитало данные тремя чтениями по 2, 1448 и 34 байта, и это поведение нашего сервера, потому что по общим сведениям границы чтения определяет стек получателя; прогоны функций на данных в 250 байт с разрезом на 104: при порции 100 и максимуме 88 получаются ломтики 88, 16, 88 и 58 со сдвигами 0, 88, 104 и 192 и идентификаторами 37472-37475, с подделкой, добавляющей опцию подписи, опции занимают 30 байт, с выравниванием 32, максимум 68, и ломтики 68, 36, 68, 68 и 10; расчёт на бумаге, который не запускали: порция 150, максимум 138, пакеты 104, 138 и 8 со сдвигами 0, 104 и 242. живой опыт: большое приветствие в 1484 байта, multisplit с позицией 2 части по метке: 2 и 1482; часть 2 длиннее максимума и режетсяне в масштабе 2часть 2: 1482 2ломтик 1448ломтик 34 журнал: строки отправки LUA: multisplit: sending part 1 0-1 len=2 … rawsend_dissect repeats=1 size=5420 + 32 + 2 = 54 LUA: multisplit: sending part 2 2-1483 len=1482 … rawsend_dissect repeats=1 size=150020 + 32 + 1448 = 1500 rawsend_dissect repeats=1 size=8620 + 32 + 34 = 86 провод: три пакета клиента (относительные номера) пакетномердлинаid IPответ сервера 11224609подтверждение 3 23144824610 314513424611подтверждение 1485 3 + 1448 = 1451; 1451 + 34 = 1485 = 1 + 1484: следующий байт после всех данных приложение сервера прочитало тремя чтениями: 2, 1448, 34границы чтений совпали с границами пакетов, а не частей по метке: это наш сервер;по общим сведениям, границы чтения определяет стек получателя прогон функций: данные 250 байт, разрез на 104, куски по максимумучисла под полосами: сдвиг куска от начала данных; настоящая библиотека, отправка заменена записью порция 100, опций 12: максимум 88id IP 37472–3747588016888810458192 подделка с опцией подписи: опции 30 → 32, 100 − 32 = 68id IP 37472–374766803668681046817210240 расчёт: порция 150, опций 12: максимум 138не запускали1040138104242

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

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

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

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

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

Порядок и пауза между частями; итог части

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

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

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

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

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

Порядок частей и синхронная отправка Порядок отправки частей в цикле multisplit (lua/zapret-antidpi.lua:504-525): сначала выдуманный пример, где почтальон кладёт в ящик два конверта подряд и пауза между ними зависит от того, чем он занят (толстый журнал или блокнот), а не от дороги; потом цикл от первой части к последней, где в каждом круге строка журнала sending part (521) стоит перед отправкой части (522), поэтому между двумя частями цикл успевает записать строку журнала; справа цепочка вызовов от rawsend_payload_segmented до sendto на сыром сокете (nfq2/darkmagic.c:2032), синхронная: возвращается, когда ядро приняло пакет, ожидания и сна на этом пути нет; внизу в опытах без накладки и повторов пакеты на проводе клиента шли с номерами по возрастанию, для пяти частей 1, 3, 98, 105, 113. порядок отправки частей: цикл идёт подряд, от первой части к последней выдуманный пример: почтальон кладёт в ящик два конверта, один за другим толстый журналконверт 1пишет в журнал: паузаконверт 2 маленький блокнотконверт 1конверт 2 пауза зависит от занятости почтальона, а не от дороги цикл частей в multisplitlua/zapret-antidpi.lua 504for i=0,#pos do507local part = string.sub(data,pos_start,pos_end)521DLOG("multisplit: sending part "..(i+1)...)522rawsend_payload_segmented(desync,part,pos_start-1-seqovl)525end круг 1: журналстрока sending part 1 круг 1: часть 1уходит в сеть круг 2: журналстрока sending part 2 круг 2: часть 2уходит в сеть между двумя частями цикл успевает записать строку журнала, если включена отладка строка 522: цепочка до sendto rawsend_payload_segmentedlua/zapret-lib.lua:1282 rawsend_dissect_segmentedlua/zapret-lib.lua:1233 rawsend_dissect_ipfraglua/zapret-lib.lua:1197 rawsend_dissect (Си)nfq2/lua.c:3000 sendto на сыром сокетеnfq2/darkmagic.c:2032 отправка синхронная: sendto возвращается, когда ядро приняло пакет и только потом цикл берёт следующую часть; ожидания и сна на этом пути в коде нет на проводе клиента, без накладки и повторов: номера по возрастанию пакет 1номер 1длина 2 пакет 2номер 3длина 95 пакет 3номер 98длина 7 пакет 4номер 105длина 8 пакет 5номер 113длина 138 обратный порядок: задача другой стратегии

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

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

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

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

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

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

Паузы между частями: диск и память Паузы между частями в двух наборах опытов на одной шкале времени: когда файл журнала лежит на диске Windows, подключённом к Linux, пауза между двумя частями 3,972 мс, между пятью частями четыре паузы 3,849, 3,976, 3,82 и 4,35 мс, то есть около 16 мс от первого до последнего пакета, и 4,75 мс с накладкой; когда журнал лежит в оперативной памяти, паузы 0,269 мс, затем 0,396, 0,201, 0,275 и 0,092 мс, меньше 1 мс от первого до последнего пакета, и 0,232 мс с накладкой; суммы посчитаны по уже измеренным паузам (3,849 + 3,976 + 3,82 + 4,35 = 15,995 и 0,396 + 0,201 + 0,275 + 0,092 = 0,964), вывод скорее всего такой: четыре миллисекунды это запись отладочного журнала на медленный диск, а не ожидание в пути отправки, но это сравнение двух наборов прогонов, где менялись путь журнала и каталог записи, а не отдельное измерение, и отладка была включена в обоих. паузы между частями в клиентском трафике: два набора опытов, одна шкала диск Windows, подключённый к Linuxлаборатория под Linux, файлы опытов на /mnt/j оперативная памятьте же аргументы, журнал и файлы в /dev/shm запись на диск Windows медленнее, чем в оперативную память; отладка включена в обоих наборах один разрез, две части диск Windows3,972 мс память0,269 мс пять частей, четыре паузы диск Windows3,8493,9763,824,3515,995 мс всего память0,396 + 0,201 + 0,275 + 0,092 мс с накладкой, две части диск Windows4,75 мс память0,232 мс шкала: 4 мс проверь себя на бумаге: пять частей, сколько от первого до последнего пакета журнал на диске Windows: около 16 мс3,849 + 3,976 + 3,82 + 4,35 = 15,995 журнал в памяти: меньше 1 мс0,396 + 0,201 + 0,275 + 0,092 = 0,964 это сумма уже измеренных пауз, нового опыта не было вывод: скорее всего, 4 мс это запись отладочного журнала на медленный дискв самом пути отправки ожидания нетоговорка: это сравнение двух наборов прогонов (менялись путь журнала и каталог записи),а не отдельное измерение

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

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

Накладка: лишние байты перед первой частью и как их строит код

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

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

Сервер получил два пакета и прочитал те же двести пятьдесят байт.

Отсюда вопрос части. Можно ли добавить в первый пакет что-то лишнее так, чтобы сервер этого не заметил? Он читает письмо по номерам, значит, у лишнего должны быть номера, которые ему не нужны.

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

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

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

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

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

Накладка: подкладка перед первой частью Идея накладки на выдуманном письме из пронумерованных страниц: перед первой страницей отправитель подкладывает три листа с номерами минус три, минус два и минус один, секретарь получателя начинает с номера один и выбрасывает листы с меньшими номерами в корзину, а помощник проверяющего, который номера не смотрит и читает всё в порядке прихода, начинает чтение с этих трёх листов, при этом настоящие страницы остаются на своих номерах; приём называется накладкой, его задаёт аргумент seqovl с числом байт, подложенных перед первой частью, по-видимому от английского sequence overlap, но в коде расшифровки нет, а сами подложенные байты называются подкладкой; внизу план части из трёх пунктов: накладка в коде, на проводе и в ответе сервера, затем ответ движку и случаи когда резать нечего, затем два игрушечных проверяющих с повторами клиента и итог выпуска. накладка: лишние байты с номерами левее начала письма midsld: разрез после 104 байт две части, оригинал выброшен сервер прочитал те же 250 байт вопрос части: можно ли добавить в первый пакет лишнее так, чтобы сервер не заметил? выдуманный пример: письмо из страниц, одна страница = один байт; секретарь начинает с номера 1 12345… -3-2-1 подкладка настоящие страницы секретарь получателясмотрит на номераначинает с номера 1 -3-2-1корзина: 3 листаменьше 1: выбрасывает собирает письмо:12345 помощник проверяющегономера не смотрит,читает по порядку прихода читает:-3-2-112345начинает с подкладки главное: подкладка слева, настоящие страницы на своих номерах, первая под 1 накладкаseqovl=NN: сколько байт подложить перед первой частьюпо-видимому, от английского sequence overlap («сиквенс оверлэп»); в коде расшифровки нетподкладка = сами подложенные байты; накладка = приём план части 1 · накладка: код, провод, ответсервера, значения 2 · ответ движку; когда резатьнечего 3 · игрушечные проверяющие,повторы клиента, итог

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

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

Сама накладка стоит внутри цикла по частям и сначала проверяет три вещи. Это первая часть? Задан ли аргумент seqovl? И больше ли нуля его число? Если хотя бы на один из трёх вопросов ответ «нет», накладки нет, и часть уходит как в обычном разрезе.

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

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

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

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

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

Проверь себя. А что с накладкой у второй части?

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

Как код строит накладку Как multisplit строит накладку, на нашем приветствии в 250 байт с pos=midsld и seqovl=5: помощница pattern (lua/zapret-lib.lua:565-574) повторяет узор и обрезает ровно по длине, так узор AB CD при длине 5 даёт AB CD AB CD AB, а пустой узор это ошибка; в цикле по частям пять шагов (lua/zapret-antidpi.lua:509-522): условие из трёх вопросов (первая часть, аргумент задан, число больше нуля), текст в число, узор по умолчанию один нулевой байт или из seqovl_pattern, склейка pattern(pat,1,seqovl) с частью, то есть 5 + 104 = 109 байт, и смещение номера pos_start-1-seqovl, для первой части 1 - 1 - 5 = -5; журнал показывает sending part 1 0-103 len=109 seqovl=5 и sending part 2 104-249 len=146 seqovl=0, где диапазон это настоящие байты с нуля, а длина уже вместе с накладкой; у второй части накладки нет, потому что условие требует первую часть, и смещение у неё такое же, как в обычном разрезе. как multisplit строит подкладку и сдвигает номер сначала помощница pattern: как рулон обоев, узор повторяется и обрезается ровно по длине выдуманный пример: продавец отрезал метр рулона, последний повтор узора обрезан pattern(AB CD, 1, 5) прогон функцийABCDABCDAB длина всегда та, что просили lua/zapret-lib.lua:565-574пустой узор: error"pattern: bad or empty pattern" offset здесь всегда 1: multisplit просит узор с первого байта пять шагов внутри цикла по частямlua/zapret-antidpi.lua 509if i==0 and desync.arg.seqovl and tonumber(desync.arg.seqovl)>0 then510 seqovl = tonumber(desync.arg.seqovl)511 local pat="\x00"516 pat = blob(desync,desync.arg.seqovl_pattern)519 part = pattern(pat,1,seqovl)..part522rawsend_payload_segmented(desync,part,pos_start-1-seqovl) 1 · три вопроса 2 · текст в число 3 · узор: нуль 3 · или из блоба 4 · склейка 5 · сдвиг номера строка 509:i==0: первая частьаргумент seqovl заданчисло больше нуля хотя бы одно «нет»: накладки нет, часть уходит как в обычном разрезе первая часть на проводе: подкладка + настоящие байты настоящие байты части 1: 1045 подкладка 5 + настоящих 104 = 109 журнал (строка 521), в нашем тесте sending part 1 0-103 len=109 seqovl=5диапазон = настоящие байты (с нуля)sending part 2 104-249 len=146 seqovl=0len = вместе с накладкой в журнале байты считаются с нуля, дальше в выпуске с единицы проверь себя: что с накладкой у второй части? её нет: условие i==0 ложно, seqovl=0, смещение как в обычном разрезе (pos_start − 1 = 104)лампочка «первая часть» гаснет

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

Накладка на проводе: номера от минус четырёх до ста пяти и ответ сервера

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

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

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

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

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

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

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

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

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

Номера пакетов с накладкой Какие порядковые номера получают пакеты с накладкой в пять байт на нашем приветствии в 250 байт с pos=midsld, номера от начала клиента, у SYN ноль, первый байт данных один: без накладки пакет 1 это номера 1-104, а пакет 2 начинается с 105; с накладкой левый край уходит влево на пять, 1 - 5 = -4, длина растёт до 5 + 104 = 109, правый край остаётся на месте, -4 + 109 = 105, и второй пакет не двигается; в нашем тесте пакеты клиента имеют номер -4 с длиной 109 и номер 105 с длиной 146, размер первого пакета в журнале 161 = 52 + 109, а во втором примере с разрезом после двух байт и накладкой в 100 выходит -99 и 102, затем второй пакет с номера 3; по кругу номеров, в прогоне функций с номером оригинала 3 и накладкой 5 сумма в Lua равна -2, а в собранном пакете 4294967294, на проводе такое не запускали; внизу проверка на бумаге для накладки 7: -6, 111 и 105, такой опыт не запускали. какие номера получат пакеты с накладкой в пять байт счёт от начала клиента: SYN = 0, первый байт данных = 1; настоящие номера больше, разности те же -4-3-2-1012…103104105106… шкала номеров: ряд клеток с пропуском посередине, показаны края без накладкипакет 1: номера 1…104пакет 2: с 105 с накладкой в 5 байтподкладка −4…0настоящие байты 1…104пакет 2: с 105 пакет 1 целиком: от −4, длина 109 левый край1 − 5 = −4 длина5 + 104 = 109 правый край−4 + 109 = 105 правый край = номер сразу за последним байтом; последний настоящий байт 104, с 105 начнётся второй пакет левый край уехал, правый остался на месте, второй пакет не двигается вообще в нашем тесте, клиент:номер −4, длина 109номер 105, длина 146размер первого пакета в журнале 161 = 52 + 10952 = 20 (IP) + 32 (TCP) пример 2: разрез после 2 байт, накладка 100:1 − 100 = −99; 100 + 2 = 102; −99 + 102 = 3 круг номеров: 32 бита, после наибольшего снова ноль 4294967293429496729442949672950123 пять шагов назад через ноль 3 − 5 = −2 в Lua; в собранном пакете 4294967294 прогон функций: номер оригинала 3 задан прогоном, на проводе не запускали проверь себя: накладка 7 байт, метка midsld. Где начнётся и где кончится первый пакет? начало: 1 − 7 = −6длина: 7 + 104 = 111правый край: −6 + 111 = 105второй пакет по-прежнему с 105это счёт, такой опыт не запускали

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

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

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

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

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

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

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

Заметим и то, чего не было: клиент ничего не отправлял повторно, соединение не сбрасывалось. И граница: всё это мы видели на одном приёмнике, Linux. Windows, BSD и встроенные системы мы не проверяли и ничего про них не утверждаем.

Ответ сервера на накладку Ответ нашего сервера на пакет с накладкой в пять байт: в выдуманном примере секретарь отвечает запиской «принято до 104 включительно, жду 105» и «листы от -4 до 0 лишние»; в настоящем опыте сервер ждёт номер 105, как и без накладки, и присылает блок выборочного подтверждения SACK от -4 до 1, единицу не включая, то есть номера -4, -3, -2, -1, 0, ровно пять подложенных байт из пяти, по общим сведениям такой блок левее ожидаемого номера называется дублирующим и сообщает о лишней копии, а обычный SACK сообщает о куске правее; приложение сервера прочитало 104 и 146 байт, всего 250, содержимое совпало с оригиналом, подкладки в данных нет; клиент ничего не повторял и соединение не сбрасывалось, но приёмник был один, Linux 6.18, а Windows, BSD и встроенные системы не проверяли. что сервер сделал с подложенными байтами выдуманный пример: секретарь отвечает запиской из двух строк«принято до номера 104 включительно, жду 105»«листы −4…0 пришли лишними, мне не нужны» настоящий ответ в нашем тесте -4-3-2-101…104105 пакет 1 клиента: подкладка −4…0, настоящие байты 1…104 сервер ждёт номер105без накладки тоже 105 блок SACK в подтверждении[−4, 1) запись [a, b): a входит, b нет−4, −3, −2, −1, 0пять номеров из пяти жду 105 по общим сведениям: обычный SACKсообщает о куске правее ожидаемого:листы 120…130 есть, 105…119 нет(выдуманный пример для записи [a, b)) по общим сведениям: блок левееожидаемого номера называется дублирующим:сообщение о лишней копии, описано встандарте (RFC 2883); в репозитории нет что прочитало приложение на сервере чтение 1: 104 байтачтение 2: 146 байтвсего 250, содержимое = оригинал подкладки в данных нет; границы чтений протокол не обещает, они зависят от скорости приложения наш сервер подкладку не посчитал и приложению не передал: называет её только в блоке SACKкак ядро это делает, не смотрели; по общим сведениям, оно отбрасывает всё левее ожидаемого номеравидим следствия: подтверждение, блок, данные; клиент не повторял, соединение не сбрасывалось граница: приёмник один, Linux 6.18Windows, BSD, встроенные системы не проверяли и ничего про них не утверждаем

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

Что подкладывать и сколько: узоры, размеры и нарезка по размеру пакета

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

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

Второй вариант: узор накладки, в записи seqovl_pattern, задан шестнадцатеричной записью из пяти байт: один-шесть, ноль-три, ноль-три, ноль-ноль, ноль-ноль. Числа те же, меняется только содержимое подкладки. По виду это начало заголовка записи TLS: тип, версия и длина ноль. Эту строку мы взяли из примера в документации zapret2. Почему в примере выбран именно такой узор, там не объяснено, и гадать мы не будем.

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

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

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

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

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

Во всех шести опытах сервер прочитал оригинал без изменений.

Ещё один опыт: в списке pos метки два, host и midsld дают четыре части, накладка пять. Накладка есть только у первого пакета, у остальных трёх её нет.

Что можно подложить Что можно подложить в накладке, шесть настоящих опытов на приветствии в 250 байт: seqovl=5 даёт пять нулевых байт и первый пакет с номером -4 и длиной 109; seqovl=5 с узором 0x1603030000 даёт 16 03 03 00 00 (по виду начало заголовка записи TLS, строка взята из примера в документации zapret2, почему такая, там не объяснено) с теми же -4 и 109; seqovl=10 с узором 0xABCD повторяет AB CD пять раз и даёт -9 и 114; seqovl=64 с блобом fake_default_tls берёт первые 64 байта из 680 и даёт -63 и 168; seqovl=1 даёт один нулевой байт, номер 0 и длину 105; pos=2 с seqovl=100 даёт сто нулей перед двумя настоящими байтами, -99 и 102, проверки что накладка меньше первой части нет; во всех шести опытах сервер прочитал оригинал без изменений; внизу опыт со списком pos=2,host,midsld и накладкой 5, где четыре пакета -4 и 7, 3 и 95, 98 и 7, 105 и 146 и подкладка есть только у первого. что подложить и сколько: шесть опытов, везде разрез midsld, кроме строки 6 аргументыподкладка, первые байтыпакет 1: номер, длина 1seqovl=500 00 00 00 00−4, 109узор по умолчанию: пять нулевых байт ✓ 2seqovl=5:seqovl_pattern=0x160303000016 03 03 00 00−4, 109по виду: тип записи, версия, длина 0; из примера в документации zapret2, почему такая, не объяснено ✓ 3seqovl=10:seqovl_pattern=0xABCDAB CD AB CD AB CD AB CD AB CD−9, 114узор короче накладки: AB CD повторён 5 раз ✓ 4seqovl=64:seqovl_pattern=fake_default_tls16 03 01 02 A3 01 00 02 …−63, 168имя готового блоба в 680 байт: берутся первые 64 ✓ 5seqovl=1000, 105самая короткая накладка: один нулевой байт ✓ 6pos=2:seqovl=10000 00 00 00 … (100 нулей)−99, 102подкладка больше настоящих данных (2 байта): проверки нет ✓ ✓: сервер прочитал оригинал без изменений во всех шести опытах (250 байт, содержимое совпало) длина узора не важна: короткий повторяется, длинный обрезается; накладка только перед первой частью ещё опыт: pos=2,host,midsld:seqovl=5, четыре части, подкладка только у первой пакет 1номер −4, длина 7подкладка 5 + часть 2 пакет 2номер 3, длина 95без подкладки пакет 3номер 98, длина 7без подкладки пакет 4номер 105, длина 146без подкладки в первом пакете 2 настоящих байта (разрез после 2) и подкладка: 5 + 2 = 7 длина накладки: целое положительное, пробовали от 1 до 1400 байтнашему серверу узор безразличен, а игрушечному проверяющему нет: это видно дальше в выпуске

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

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

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

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

Один, два, три, один, два, три, один. Узор повторяется и обрезается ровно по длине накладки, как рулон обоев. Это счёт по коду и по прогону функции pattern, с таким узором мы опыт не запускали.

Нарезка части по размеру сегмента Нарезка части по размеру пакета: в рукопожатии оба конца объявили размер сегмента 1460, код берёт меньший из двух, опции съедают 12 байт, остаётся 1448 байт данных (lua/zapret-lib.lua:1243), поэтому при накладке в 1400 байт первая часть вместе с подкладкой 1400 + 104 = 1504 байта не помещается и отправка режет её на два куска (lua/zapret-lib.lua:1246-1273): первый кусок 1448 байт, это 1400 байт подкладки и 48 настоящих, второй 56 настоящих; в нашем тесте номера и длины такие: -1399 и 1448, затем 49 и 56, затем вторая часть как раньше 105 и 146, сервер ждёт сначала 49, потом 105, а приложение читает 48, 56 и 146 байт, то есть те же 250 байт оригинала, куски у приложения следуют за нарезкой, а не за частями; внизу проверка на бумаге: узор 01 02 03 и накладка 7 дают байты 01 02 03 01 02 03 01, с таким узором опыт не запускали. накладка в 1400 байт: часть длиннее коробки, отправка режет её сколько данных помещается в один пакетMSS 1460 (объявили оба конца) − 12 байт опций = 1448код берёт меньший MSS из двух SYN, иначе 1220 (desync.c:1021-1030, params.h:46)max_data = mss − extra_len (zapret-lib.lua:1243) первая часть вместе с подкладкой: 1400 + 104 = 1504 байта, это больше 1448 подкладка 1400 настоящие 104 разрез на отметке 1448 отправка режет часть на два куска кусок 1: 1448 байт1400 подкладки + 48 настоящихкусок 2: 56 настоящих48 + 56 = 104, вся часть нарезка: zapret-lib.lua:1246-1273 в нашем тесте: клиент, сервер и приложениеразмеры в журнале 1500 и 108 пакет 1: −1399, длина 1448сервер ждёт 49чтение 48 пакет 2: 49, длина 56сервер ждёт 105чтение 56 пакет 3: 105, длина 146вторая часть, как раньшечтение 146 приложение прочитало 48, 56 и 146 = 250 байт, содержимое совпало с оригиналом куски у приложения повторяют нарезку, а не частиграницы чтений зависят от скорости приложения; так вышло в нашем тесте проверь себя: узор из трёх байт 01 02 03, накладка 7. Какие семь байт подложены? 01020301020301 узор повторяется и обрезается ровно по длине, как рулон обоевсчёт по коду и прогону pattern; с таким узором опыт не запускали

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

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

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

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

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

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

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

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

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

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

Неверные значения накладки Что будет при неверных значениях аргументов накладки на нашем приветствии в 250 байт: seqovl=abc или пустое значение даёт ошибку Lua attempt to compare number with nil, движок пишет desync ERROR passing packet unmodified и оригинал уходит целым одним пакетом 1 и 250; seqovl=0 или seqovl=-5 молча не делают накладку и дают обычный разрез 104 и 146; seqovl=0x10 читается как 16 и даёт номер -15 и длину 120; seqovl_pattern=nosuchblob даёт ошибку blob unavailable (lua/zapret-lib.lua:554) и целый оригинал, а с флагом optional строку using zero pattern, нулевой узор и обычную накладку -4 и 109; нечётная шестнадцатеричная запись 0xABC и пустое имя узора дают ошибки invalid hex string и empty blob name в прогоне функций; общий вывод: ноль и минус дают обычный разрез, а текст, пустое значение и пропавший узор срывают всю стратегию, оригинал уходит целым, и это видно только в журнале и по числу пакетов. что будет, если аргумент накладки неверный выдуманный пример: в наряде вместо числа слово «много», считать нечем, заготовка уходит целой аргументчто делает кодчто на проводе seqovl=abcили пустое значениеtonumber вернул пусто, «пусто > 0»:ошибка Lua (zapret-antidpi.lua:509)один пакет 1 + 250:оригинал целым seqovl=0или seqovl=-5условие «больше нуля» не выполнено,накладки нет, ошибки нетобычный разрез:1 + 104, 105 + 146 seqovl=0x10шестнадцатеричное число,Lua читает как 16−15 + 120(16 нулей перед частью) seqovl_pattern=nosuchblobошибка blob unavailable(zapret-lib.lua:554)один пакет 1 + 250:оригинал целым то же, с флагом optionalстрока «using zero pattern»,узор нулевой, накладка идёт−4 + 109, 105 + 146как обычная накладка seqovl_pattern=0xABCили пустое имя узораошибки invalid hex string и empty blob namezapret-lib.lua:546 и 539прогон функций:пакетов 0, опыта не было путь ошибки внутри стратегии: оригинал не трогают ошибка Luaв строке 509 или 554 nfq2/desync.c:1079-1083lua_pcall не удался nfq2/desync.c:1142-1146«desync ERROR. passingpacket unmodified.» nfq2/nfqws.c:337«packet: id=4 passunmodified» в журнале перед ошибкой стоит «resolved split pos: 104»: метки прочитаны, а накладка сломалась общий вывод: неверный аргумент не рвёт соединение, но результат разныйноль и минус: молча обычный разрезтекст, пустое значение, пропавший узор: ошибка Lua, ни накладки, ни разреза; флаг optional спасает узорзаметно только в журнале и по числу пакетов на проводедробное значение: ошибки нет, но поток испорчен (следующая схема)

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

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

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

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

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

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

Проверь себя. Накладка три с половиной. Сколько байт подкладки и на сколько сдвинется номер?

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

Дробная накладка портит поток Ловушка дробной накладки seqovl=2.5 на нашем приветствии в 250 байт с pos=midsld: в выдуманном примере бригадир подкладывает два листа из двух с половиной, а новый номер считает как прежний минус 2,5 с округлением вниз, то есть минус три, и вся стопка съезжает на один номер; в коде число 2,5 используется дважды, pattern(pat,1,seqovl) на строке 519 берёт целую часть и делает 2 байта подкладки, а смещение pos_start-1-seqovl на строке 522 несёт -2,5 как есть, в прогоне функций сумма с дробью …704,5 при сборке пакета отбрасывается вниз до …704, то есть минус 3 к номеру оригинала; в нашей сборке стоит LuaJIT, в обычном Lua 5.3 и новее дробная длина дала бы ошибку уже в divint внутри pattern, такую сборку не запускали; в настоящем опыте первый пакет идёт с номера -2 и длиной 106, первый настоящий байт съехал на номер 0, сервер отбрасывает всё левее 1 и читает 103 байта, второй пакет с 105 оставляет дыру на номере 104, клиентское ядро присылает заново 147 байт с номера 104, приложение прочитало 103 и 147 байт, всего 250, но первого байта нет, а байт на границе повторён, имя сайта прочесть нельзя; для накладки 3,5 по счёту выходит 3 байта подкладки и сдвиг 4, такой опыт не запускали. значение, которое ошибки не даёт: seqovl=2.5 выдуманный пример: наряд «подложить два с половиной листа»листов бригадир кладёт 2 (целых), а новый номер считает: прежний − 2,5, вниз = −3листов два, сдвиг три: вся стопка съехала на один номер то же число дваждыlua/zapret-antidpi.lua519part = pattern(pat,1,seqovl)..part522rawsend_payload_segmented(desync,part,pos_start-1-seqovl) целая часть: 2 байта идёт как есть: −2,5 прогон функций, исходный номер X:в Lua X − 2,5 = …704,5дробь при сборке отбрасывается внизна проводе X − 3 = …704 по общим сведениям: Lua бывает двух видов, обычный интерпретатор и LuaJIT (ускоренный)в нашей сборке при запуске: LUA v5.1 LuaJIT 2.1.1737090214 OpenResty в обычном Lua 5.3 и новее дробная длина дала бы ошибку уже при подсчёте повторов узора (divint в pattern):по общим сведениям, такую сборку не запускали настоящий опыт: накладка 2,5, метка midsld −2−1012…102103104105106…250 пакет 1: номер −2, длина 106 (2 подкладки + 104 настоящих)подкладкабайт 0настоящие байты первый настоящий байт на 0, а должен на 1 сервер: всё левее 1 отбрасывает, читает 103 байта, ждёт 104отброшенопрочитано 1…103жду пакет 2 с 105: дыра в один байт на номере 104, сервер: блок SACK [105, 251)дырапакет 2: 105…250 клиент шлёт заново с 104, 147 байт (по журналу ядро, не nfqws2)104…250 приложение прочитало 103 + 147 = 250 байт, но не тепервого байта нет, байт на границе повторён; содержимое ≠ оригинал, имя сайта не прочестьошибки нет, соединение живо, а поток испорчен: накладка должна быть целой, больше нуля проверь себя: накладка 3,5. Сколько байт подкладки и на сколько сдвинется номер? 3 байта подкладки; номер X − 3,5 вниз = X − 4, первый настоящий байт опять на один левееэто счёт по коду, такой опыт не запускали

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

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

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

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

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

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

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

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

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

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

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

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

Ответ движку и оригинал Ответ функции движку после частей: строка 526 только ставит служебную отметку для повторного проигрыша, строка 527 возвращает «пропустить», если задан аргумент nodrop, и «выбросить» во всех остальных случаях (lua/zapret-antidpi.lua:526-527), а так как в Lua ложны только nil и false и число 0 истинно, любое значение nodrop, даже 0 или пустая строка, даёт «пропустить»; в нашем опыте без nodrop журнал пишет packet: id=4 drop и на проводе две части 1+104 и 105+146, а с nodrop журнал пишет pass unmodified и примерно через 0.06 мс после второй части уходит оригинал 1+250, сервер отвечает подтверждением 105, затем 251 с дублирующим блоком от 1 до 251 не включая, а приложение читает 104 и 146 байт; при накладке в 5 байт порядок тот же (-4+109, 105+146, 1+250) и сервер даёт два дублирующих блока; при repeats=2 каждая часть уходит дважды подряд, и приложение всё равно читает 104 и 146 байт. ответ движку после частей · lua/zapret-antidpi.lua:526-527 526replay_drop_set(desync)для проигрыша: отметка 527return desync.arg.nodrop and VERDICT_PASS or VERDICT_DROP 1 2 по умолчанию оригинал выбрасывается вердикт движкуnodrop нет → DROP (2)packet: id=4 dropnodrop задан → PASS (0)packet: id=4 pass unmodified 0в Lua ложны только nil и false: число 0 истинно,выражение с VERDICT_PASS = 0 всё равно даёт «пропустить» прогон функций (метка)nodrop=0 → returned 0 (PASS)nodrop= пусто → тоже PASS провод у клиента · номер+длина, относительные номера (метка «в нашем тесте») без nodrop1+104105+1461+250оригинал в корзине: пакета в 250 байт нет с nodrop1+104105+1461+250оригинал следом, через ≈0.06 мс:ядро держит его в очереди у программы ответ серверажду 105жду 251, дубль блока [1, 251)приложение: 104 + 146 дубль блока: эти байты у сервера уже были; второй раз ничего не доставлено nodrop+ накладка 5-4+109105+1461+250сервер: два дублирующих блока[-4, 1) и [1, 251) repeats=2без nodrop1+1041+104105+146105+146каждая часть дважды подряд,пауза ≈0.05 мс; приложение 104, 146 nodrop стоит одного лишнего пакета, для нашего сервера он ничего не меняеттот, кто смотрит на каждый пакет, увидит и оригинал с целым именем:две игрушечные модели находят его в сцене про шесть моделейметка «игрушка проекта»: это не оборудование

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

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

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

Второй путь, громкий. Список меток с неизвестным словом: pos равно foo. Функция на Си сообщает «плохой список меток», и это ошибка Lua. Журнал: ошибка, потом «пропускаем пакет без изменений». На проводе тот же один пакет. Так же выглядели неверный seqovl и пропавший узор.

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

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

Три пути оригинала целым Три пути, при которых оригинал уходит целым: тихий, громкий и сбой отправки; тихий путь, когда после разбора меток и вычёркивания единицы позиций не осталось, в нашем тесте дали pos=0 (перед первым байтом резать нельзя), пустой pos, pos=1220 и pos=250 при данных в 250 байт (позиция должна быть меньше длины данных) и pos=method на приветствии TLS (метка не того протокола), во всех пяти журнал пишет multisplit: no valid split positions и packet: id=4 pass unmodified, на проводе один пакет 1+250, побайтно равный тому, что видел nfqws2, а сервер прочитал 250 байт, равные оригиналу; громкий путь, pos=foo, даёт ошибку Lua bad marker list в строке 500 с записями LUA ERROR и desync ERROR. passing packet unmodified, на проводе тот же один пакет, так же выглядят неверный seqovl и пропавший узор, и после такой ошибки движок пропускает оригинал целым даже если другой вызов уже сказал «выбросить», а тихий выход цепочку не прерывает; третий путь виден только в прогоне функций с заглушкой вместо отправки: если не ушла первая часть, вердикт «пропустить» и пакетов 0, а при pos=2,midsld,endhost с частями 2, 102, 8 и 138 первая ушла, вторая отказала, вердикт «пропустить» и оригинал идёт следом за первой частью, настоящий сбой сырого сокета мы не воспроизводили; проверка: pos=249 на приветствии в 250 байт допустима, потому что меньше длины данных, и на проводе два пакета 1+249 и 250+1. нечего резать или всё сломано: оригинал уходит целым 1путь 1, тихий: после разбора меток позиций не осталось pos=0перед первымбайтом резатьнельзя: единицувычёркивают pos= (пусто)в списке нетни одной метки pos=1220позиция должнабыть меньшедлины данных pos=250данные 250 байт:позиция равнадлине, не меньше pos=methodметка не тогопротокола ничегоне находит на TLS журнал (во всех пяти):multisplit: no valid split positionspacket: id=4 pass unmodified провод и сервер:1+250один пакет, побайтно равен тому,что видел nfqws2сервер: одно чтение 250 байт, равно оригиналу выход без значения: движок считает это «пропустить»desync.c:849-850, 867-868 · nfqws.c:337 2путь 2, громкий: неверный аргумент pos=foo resolve_multi_pos: bad marker list LUA ERROR: …zapret-antidpi.lua:500: bad marker listdesync ERROR. passing packet unmodified.packet: id=4 pass unmodified 1+250один пакеттак же выглядят:неверный seqovl,пропавший узор громкий: после ошибки движок пропускает оригинал целым, даже если раньше был DROPтихий: цепочка стратегии идёт дальше 3путь 3, сбой отправкипрогон функций: отправка заменена записью первая часть не ушла (pos=midsld)часть 1→ PASS, пакетов 0, оригинал целый pos=2,midsld,endhost: четыре части 2, 102, 8, 138; вторая отказала 21028138вердикт PASS: оригинал идёт следом за первой частью настоящий сбой сырого сокета мы не воспроизводилиantidpi:522-524 проверь себя: pos=249 на приветствии в 250 байт, что на проводе? 1+249250+1два пакета: позиция 249 меньше длины 250, допустимав нашем тесте так и вышло: сервер прочитал 249 и 1

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

Проверь себя. Аргумент pos равен двумстам сорока девяти, а приветствие в двести пятьдесят байт. Что будет на проводе?

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

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

Игрушечный проверяющий первого вида: правило в роутере смотрит на одно место

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

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

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

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

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

Правило и одно место Игрушечное правило в роутере (игрушка проекта, а не настоящее оборудование) смотрит на каждый пакет к серверу 10.2.0.2, порт 443, по отдельности и сравнивает ровно 15 байт www.example.com с одним фиксированным местом: смещение 97 от начала данных, считая с нуля, то есть 32 байта заголовка TCP плюс 97, это смещение 129 от начала заголовка и 1032 бита в записи правила; 32 байта заголовка это 20 обычных и 12 байт опции времени, и у всех клиентских пакетов нашего теста заголовок такой длины, поэтому место одно и то же; при совпадении пакет выбрасывается или соединение сбрасывается, счётчик считает совпавшие пакеты, а выдуманный вахтёр с картонкой с окошком читает каждый лист отдельно, только в окошко на одном месте, ничего не склеивает и не помнит. игрушечный проверяющий первого вида: правило в роутере в лаборатории нет настоящего ТСПУ: это игрушки проекта, «сработало» = «обошло игрушку» выдуманный примервахтёр с картонкой с окошкомчитает каждый лист отдельно,только в окошко на одном месте,ничего не склеивает и не помнит правило nft в роутере адрессервер 10.2.0.2, порт 443 местосмещение 97 от начала данных, считая с нуля ищет15 байт www.example.com делаетвыбрасывает (drop) или сбрасывает соединение (reset) счётчиксколько пакетов совпало само правило: ip daddr 10.2.0.2 tcp dport 443 @th,1032,120 0x7777772e6578616d706c652e636f6d counter drop один пакет клиента: заголовок и данные TCP: 32данные приветствия, 250 байт имя окошко 097112250смещение данных: 32 + 97 = 129 байт от начала TCP = 1032 бита 77 77 77 2e 65 78 61 6d 70 6c 65 2e 63 6f 6d= www.example.com, 15 байт, смещения 97–111 32 = 20 байт обычного заголовка + 12 байт опции времени:у всех клиентских пакетов нашего теста заголовок такой длины, поэтому место одно и то же

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

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

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

Четвёртая: разрез точно перед именем. Теперь второй пакет начинается с имени, смещение ноль. Счётчик снова ноль.

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

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

Шесть опытов с правилом Шесть опытов с игрушечным правилом, которое ищет 15 байт имени на смещении 97 каждого пакета: без nfqws2 имя лежит на своём месте, счётчик 6 (оригинал и пять повторов) и сервер получил 0 байт, а в варианте со сбросом счётчик 1 и клиент получил Connection reset by peer; при pos=midsld имя разрезано, счётчик 0, сервер получил 104 и 146 байт; при pos=2 имя целиком во втором пакете на смещении 95, счётчик 0, сервер получил 2 и 248; при pos=host имя с начала второго пакета, смещение 0, счётчик 0, сервер получил 97 и 153; при pos=endhost имя целиком остаётся в первом пакете на смещении 97, счётчик 6, сервер получил 0 байт, а клиент шлёт шесть одинаковых пар; при pos=endhost с seqovl=5 накладка сдвигает имя на смещение 102, счётчик 0 и сервер получил 112 и 138 байт, с вариантом правила со сбросом тоже 0; по устройству правила при seqovl=3 имя окажется на смещении 100 и счётчик будет 0, но такой опыт мы не запускали; вывод только про нашу игрушку: правило обходит любой сдвиг имени с места, разрез, оставляющий имя на месте, не помогает, накладка помогает. шесть опытов с одним правилом · метка «в нашем тесте» стратегиячасти пакета; красная черта: окошко 97счётчиксервер получил без nfqws2250оригинал и пять повторов; вариант reset: счётчик 1, Connection reset by peer60 байт pos=midsld104146имя разрезано: в окошке первой части только 7 из 15 байт0104 + 146 pos=2248имя целиком во второй части, но на смещении 9502 + 248 pos=host97153имя с начала второй части: смещение 0; в первой окошко за концом097 + 153 pos=endhost112138имя целиком в первой части на своём месте; шесть одинаковых пар60 байт pos=endhostseqovl=5117138накладка сдвинула имя на 5: смещение 102; вариант reset: тоже 00112 + 138 это про нашу игрушку и ни про что большеправило обходит любой сдвиг имени с места; разрез, оставляющий имя на месте, не помогает;накладка помогает, хотя имя цело в одном пакете проверь себя: pos=endhost:seqovl=3 — имя на смещении 100, а не 97, значит счётчик 0счёт по устройству правила: именно такой опыт мы не запускали

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

Проверь себя. Разрез после имени и накладка в три байта. Что покажет счётчик?

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

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

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

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

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

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

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

Шесть игрушечных моделей Шесть игрушечных моделей проекта, а не настоящего оборудования, читают запись пакетов, снятую в роутере: первая ищет имя целиком в одном пакете, вторая смотрит одно место со смещением 97, как правило из прошлой сцены, третья собирает пакеты по номерам как приёмник и всё левее ожидаемого номера отбрасывает, четвёртая склеивает пакеты в порядке прихода, не глядя на номера, и разбирает приветствие TLS с начала потока, пятая учитывает номера, но началом потока считает первый увиденный пакет, шестая как пятая идёт по заголовкам записей: тип, версия, длина; при накладке с узором 16 03 03 00 00 заголовок записи имеет длину 0, пятая не находит ClientHello, а шестая переступает пустую запись на 5 байт и читает настоящее начало, а при накладке из нулей первый байт не равен типу записи 16 и шестая останавливается сразу. игрушечные проверяющие второго вида: модели над записью пакетов игрушки проекта, не оборудование: «сработало» = «обошло игрушку» они читают запись, снятую в роутере: ровно то, что дошло до проверяющего Aищет имя целиком в одном пакетеподстрока: разрез внутри имени её сбивает Bсмотрит одно место: смещение 97, как правило из прошлой сценыв нашем тесте совпала со счётчиком правила в 12 опытах из 12 Cсобирает пакеты по номерам, как приёмниквсё левее ожидаемого номера отбрасывает Dсклеивает пакеты в порядке прихода, номера не смотритприветствие TLS разбирает с начала потока Eномера учитывает, но началом потока считает первый увиденный пакетприветствие TLS разбирает с начала потока Fкак E, но идёт по заголовкам записей: тип, версия, длинапустую запись (длина 0) переступает шагом накладка в 5 байт: что получает разбор с начала потока (метка «в нашем тесте») узор 16 03 03 00 0016 03 03 00 0016 03 01 00 F5 01 00 00 F1 03 03 … D, E: поток начинается с подкладки, а не с записи приветствия: нет разбора F: заголовок «тип 16, версия 3.3, длина 0» это пустая запись, шаг на 5 байт F попадает на настоящее начало и читает имя нули00 00 00 00 0016 03 01 00 F5 01 00 00 F1 03 03 … F: первый байт 00 не тип записи 16, разбор останавливаетсяC накладку отбрасывает: всё левее начала потока приёмник не берёт

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

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

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

Четвёртая строка, накладка с nodrop. Оригинал идёт следом за частями, и теперь первая и вторая модели находят имя: в оригинале оно на месте и целое. Так что nodrop возвращает проверяющему то, что разрез у него отнял.

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

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

Результаты шести моделей Таблица результатов шести игрушечных моделей в нашем тесте, где плюс значит, что модель нашла имя, минус что не нашла, SNI что она разобрала приветствие и прочла имя, а нет что разбор с начала потока не удался: при разрезе midsld без накладки первая и вторая не находят имя, а с третьей по шестую читают SNI; при накладке в 5 байт с узором 16 03 03 00 00 третья и шестая читают SNI, а четвёртая и пятая не разбирают; при накладке из нулей, при узоре AB CD, при 64 байтах начала поддельного приветствия и при накладке в 1400 байт читает только третья; при том же узоре 16 03 03 00 00 вместе с nodrop первая и вторая находят имя в оригинале, третья и шестая читают SNI, четвёртая и пятая не разбирают, а при midsld с nodrop без накладки имя находят все шесть; это выводы только про шесть игрушек, руководство проекта пишет, что проверяющий без слежения за номерами может проглотить сегмент целиком и поверить ложным данным в его начале, а сервер их не примет, на настоящем оборудовании мы это не проверяли, и мы не знаем, какая из моделей похожа на настоящую. что увидели шесть игрушечных моделей · метка «в нашем тесте» A B C D E F midsld, без накладки−−SNISNISNISNI накладка 5, узор 16 03 03 00 00−−SNIнетнетSNI накладка 5, нули−−SNIнетнетнет накладка 10, узор AB CD AB CD …−−SNIнетнетнет накладка 64 байтаначало встроенного поддельного приветствия−−SNIнетнетнет накладка 1400 байтузор нулей, части по размеру сегмента−−SNIнетнетнет накладка 5, узор 16 03 03 00 00, nodropоригинал идёт следом за частями++SNIнетнетSNI midsld, nodrop, без накладкиоригинал идёт следом за частями++SNISNISNISNI модель нашла имя (проверяющий видит)не нашла: игрушку обошли+ / −: имя найдено / не найдено; SNI: приветствие разобрано, имя прочитанонет: разбор приветствия с начала потока не удался только про эти шесть игрушекC, которая следит за номерами как приёмник, накладки не замечает;у D и E поток начинается с подкладки, и ломается разбор приветствия с начала потока;F переживает подкладку в виде пустой записи, но не нули и не другие узоры руководство проекта пишет (docs/manual.en.md:4049), близко к тексту:если проверяющий не следит за номерами, он может проглотить сегмент целиком и поверитьложным данным в его начале, а сервер их не примет; на настоящем оборудовании мы это не проверяли не знаем: какая из шести моделей похожа на настоящее оборудование и похожа ли вообще;как ведут себя другие приёмники. Про настоящий ТСПУ эти опыты не говорят ничего

Проверь себя. Шестая модель читает приветствие, когда накладка набрана из байтов один-шесть, ноль-три, ноль-три, ноль-ноль, ноль-ноль, и не читает, когда накладка из нулей. Почему?

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

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

Повторы клиента и ловушка подмены полей

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

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

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

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

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

Повторы клиента и расписание Повторы клиента при игрушечном правиле, которое выбрасывает пакет с именем на смещении 97: при pos=endhost имя целиком остаётся в первой части из 112 байт и правило её выбрасывает, сервер отвечает, что ждёт первый байт, а вторая половина у него уже есть, поэтому клиентское ядро позже отправляет оригинал снова, он приходит в программу через очередь как новый пакет, и multisplit, не помнящий, что уже резал это приветствие, режет его заново, в журнале шесть раз sending part 1; в нашем тесте шесть пар 1+112 и 113+138 уходят на отметках 0, 205, 413, 849, 1681 и 3345 мс с промежутками 205, 208, 436, 832 и 1664 мс, первые два почти равны, дальше каждый вдвое длиннее, счётчик правила 6, приложение сервера прочитало 0 байт, клиент остался без ответа; в контрольном опыте без nfqws2 копии 1+250 уходят на отметках 0, 206, 414, 834, 1662 и 3326 мс с промежутками 206, 208, 420, 828 и 1664, расписание очень похожее, и по общим сведениям это обычная задержка повторов TCP с удвоением. повторы клиента · опыт с правилом, которое выбрасывает пакет с именем на месте 97 то же игрушечное правило: выбрасывает пакет, у которого на месте 97 лежит имя сайта pos=endhost: приветствие 250 байт режется после имениимячасть 1: 112часть 2: 138 часть 1 с именем на своём месте: правило выбрасывает её, часть 2 проходит сервер отвечает: жду байт 1,вторая половина, байты 113–250, у меняядро клиента потом шлёт оригинал снова копия приходит в программу как новый пакетmultisplit не помнит, что уже резал: режет сноважурнал: шесть раз sending part 1 0-111 len=112 с nfqws2, шесть пар 1+112 и 113+138 · мс от первой пары · промежутки над осью 0205413849168133452052084368321664 первые два промежутка почти равны, дальше каждый вдвое длиннее предыдущего контроль без nfqws2, копии оригинала 1+250 · те же правило и сервер 0206414834166233262062084208281664 расписание очень похожее: третий промежуток 420 против 436по общим сведениям, это обычная задержка повторов TCP с удвоением счётчик правила 6; приложение сервера прочитало 0 байт; клиент без ответа

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

Теперь ловушка. Игрушка, пример выдуманный. Курьеру велели ставить на каждую посылку штамп «доставить до первого поста». Он поставил, и ни одна посылка не дошла.

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

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

Прогон функций показывает тот же механизм: при ip_ttl равном трём обе части получают TTL три.

Подмена бьёт по частям Ловушка подмены полей в multisplit: функция desync_opts (lua/zapret-lib.lua:423-431) раздаёт одну таблицу desync.arg, где лежат и pos, и аргументы подмены, по пяти веткам rawsend, reconstruct, ipfrag, ipid и fooling, а отправка в строке 1237 вызывает apply_fooling на каждый вызов, поэтому всё заданное получают настоящие части, как выдуманный курьер, который ставит штамп «доставить до первого поста» на каждую посылку, и ни одна не доходит; в опыте multisplit:pos=midsld:ip_ttl=1 клиент отправил 12 пакетов, шесть пар 1+104 и 105+146, с TTL 1, до сервера не дошёл ни один, роутер вернул 9 сообщений ICMP типа 11 на 12 пакетов, почему не 12, не выясняли, приложение сервера прочитало 0 байт и клиент остался без ответа; прогон функций с ip_ttl=3 показал TTL 3 у обеих частей, размеры 156 и 198; в multisplit нет отдельных поддельных аргументов, подмена бьёт по настоящим данным, а для pos=2,midsld и ip_ttl=3 по коду у третьей части тоже будет TTL 3, хотя три части с этим аргументом мы не запускали. подмена полей в multisplit · lua/zapret-lib.lua:423-431 и 1237 выдуманный примеркурьеру велели ставить на каждую посылку штамп «доставить до первого поста»:он поставил, и ни одна посылка не дошла 423function desync_opts(desync) 424return { 425rawsend = rawsend_opts(desync), 426reconstruct = reconstruct_opts(desync), 427ipfrag = desync.arg, 428ipid = desync.arg, 429fooling = desync.arg 430} 431end таблица desync.argpos, seqovl, nodrop, ip_ttl, …расходится на пять веток: rawsendreconstructipfragipidfooling ← подмена zapret-lib.lua:1237 в rawsend_dissect_segmented, на каждый вызов отправки:apply_fooling(desync, discopy, options and options.fooling) apply_fooling читает ip_ttl, флаги TCP, сдвиги номеров и другие; часть ли перед ним, он не различает опыт: multisplit:pos=midsld:ip_ttl=1 · метка «в нашем тесте» 1+104105+1460 мс1+104105+146206 мс1+104105+146414 мс1+104105+146834 мсответов нет1+104105+1461666 мс1+104105+1463334 мс TTL 1 у всех 12 пакетов; красные точки: ответ роутера ICMP «время истекло» (тип 11), всего 9 на 12 пакетовпочему не 12, мы не выясняли до сервера не дошёл ни один пакетприложение 0 байт; клиент без ответа прогон функций (метка): ip_ttl=3ttl 3: размеры 156 и 198 в multisplit нет отдельных «поддельных» аргументов: подмена бьёт по настоящим даннымжурнал чистый, части уходят, а соединение просто не работает проверь себя: ip_ttl=3 и pos=2,midsld (три части): какой TTL у третьей?три, по коду; для двух частей прогон функций это показал, три части мы не запускали

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

Проверь себя. Аргумент ip_ttl равен трём, а разрезов два: pos равно два, midsld. Какой TTL у третьей части?

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

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

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

Подведём итог выпуска.

Вот путь одной строки, каждый шаг мы читали в коде, а почти все повторили на пакетах нашей лаборатории. Мы написали multisplit с pos равно midsld. Функция проверила ворота: это TCP, есть данные, подходит направление и тип. Метка превратилась в число: сто четыре байта слева от разреза, в Lua это сто пять. Цикл разрезал приветствие на две части: сто четыре байта и сто сорок шесть.

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

Путь строки до провода Путь одной строки multisplit с pos=midsld от записи до провода, шесть шагов с номерами строк в lua/zapret-antidpi.lua: ворота (строки 484-495: это TCP, есть данные, подходят направление и тип), метки в числа (строка 497 задаёт pos по умолчанию 2, строка 500 вызывает resolve_multi_pos на Си, строки 501-502 пишут журнал и вычёркивают единицу), цикл по частям (строки 504-507: каждая допустимая позиция даёт на одну часть больше), отправка (строка 522 и rawsend_payload_segmented в lua/zapret-lib.lua:1282-1290, затем rawsend_dissect_segmented в строках 1233-1279: заголовки оригинала, свой номер, нарезка по размеру сегмента), накладка (строки 508-520: узор только перед первой частью, номер меньше на seqovl) и ответ движку (строки 526-527: выбросить, а с nodrop пропустить); на примере из нашего теста midsld даёт 104, затем {105}, части 104 и 146, на проводе 1+104 и 105+146 и ответ DROP, а с накладкой в 5 байт первая часть уходит как -4+109. путь строки multisplit:pos=midsld · lua/zapret-antidpi.lua 1строка и воротаэто TCP? есть данные? подходятнаправление и тип?484-488 не TCP: сразу выход494-495 данные, direction_check,payload_checkвсе четыре «да» 2метки → числаСи считает с нуля, Luaприбавляет 1, единицувычёркивают497 pos по умолчанию "2"500 resolve_multi_pos (Си)501-502 журнал, delete_pos_1midsld → 104 → {105} 3цикл по частямкаждая допустимая позиция даётна одну часть больше504-507 for i=0,#pos dopos_start, pos_end, string.sub529 позиций нет: оригинал целыйчасти 104 и 146 байт 4отправказаголовки оригинала, свойномер, нарезка по размерусегмента522 rawsend_payload_segmented(…)сдвиг номера pos_start-1-seqovlzapret-lib.lua:1282-1290, 1233-1279на проводе 1+104 и105+146 5накладкаузор перед первой частью,номер меньше на seqovl508-520 только i == 0, если seqovl > 0519 part = pattern(pat,1,seqovl)..partseqovl=5: перваячасть -4+109 6ответ движкувыбросить; с nodropпропустить, оригинал следом526 replay_drop_set(desync)527 … nodrop and VERDICT_PASSor VERDICT_DROPответ DROP (2); сnodrop PASS (0) пример из нашего теста, одной строкой midsld→104→{105}→104 + 146→1+104, 105+146→DROP с nodrop последний шаг PASS: оригинал 1+250 идёт следом за частями

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

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

Теперь о том, чего мы не знаем. Мы проверяли один приёмник, Linux, и не проверяли Windows, BSD и встроенные системы. Мы не воспроизводили настоящий сбой сырого сокета. Не запускали накладку при IP версии шесть. Для UDP и ICMP multisplit по коду сразу выходит, а на проводе мы этого не проверяли.

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

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

Помнить и граница знаний Восемь строк, которые стоит запомнить про multisplit: pos это список меток, по умолчанию 2, и если резать нечего, оригинал уходит целым; плохая метка даёт ошибку, оригинал тоже уходит целым; каждая допустимая позиция даёт на одну часть больше, позиции идут по возрастанию, повторы склеены, части уходят подряд без ожидания между ними; часть, которая не помещается в пакет, режется по размеру сегмента; seqovl это целое положительное число и подкладка только перед первой частью, ноль и минус дают обычный разрез, текст срывает стратегию ошибкой, а дробь в нашей сборке портит поток; nodrop с любым значением оставляет оригинал следом за частями; аргументы подмены бьют по настоящим частям; повторы оригинала от клиента режутся заново каждый раз; не проверяли приёмники кроме Linux, настоящий сбой сырого сокета, накладку при IPv6, UDP и ICMP на проводе (по коду multisplit для не TCP сразу выходит), причину выбора узора в примере документации, сколько разрезов выдержит чужой приёмник (наш сервер принял 25 пакетов по 10 байт) и всё о настоящем ТСПУ; в следующем выпуске те же разрезы уйдут в другом порядке, например последней частью вперёд, а у multidisorder накладка задаётся меткой, у multisplit только числом. итог выпуска · восемь строк, которые стоит запомнить 1pos: список меток, по умолчанию 2резать нечего: оригинал уходит целым 2плохая метка: ошибка Luaоригинал тоже уходит целым 3каждая допустимая позиция: на одну часть большепо возрастанию, повторы склеены,подряд, без ожидания между частями 4часть больше пакета (MSS минус опции)режется по размеру сегмента 5seqovl: целое положительное числоподкладка только перед первой частью; ноль и минус:обычный разрез; текст: ошибка; дробь портит поток 6nodrop: любое значениеоригинал идёт следом за частями 7подмена полейбьёт по настоящим частям 8повторы оригинала от клиентарежутся заново, каждый раз не проверяли приёмники кроме Linux:Windows, BSD, встроенные настоящий сбой сырого сокета накладка при IPv6 UDP и ICMP на проводе: по кодудля не-TCP функция выходит почему в примере документацииименно такой узор сколько разрезов терпит чужойприёмник: наш сервер принял25 пакетов по 10 байт всё о настоящем ТСПУ что дальше: следующий выпуск, те же разрезы в другом порядке в этом выпуске: сначала часть 1, потом часть 2часть 1: 1+104часть 2: 105+146 в следующем: последняя часть первой (пример порядка)часть 2: 105+146часть 1: 1+104а если поменять порядок? сервер и игрушки покажут multidisorder (части в другом порядке): накладка задаётся меткой, antidpi:592, 620multisplit: накладка только число, tonumber(desync.arg.seqovl), antidpi:509-510стратегия следующего выпуска; здесь мы её не разбираем

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

См. также

  • Где резать: метки — предыдущее видео серии
  • multisplit — справочная статья о функции
  • payload — типы данных и их распознавание
  • blob — как объявлять данные для аргументов
  • ts-and-fooling — подмена полей пакета

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

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