tcpseg — отправка TCP-сегмента из диапазона (zapret2 / nfqws2)

Коротко и по-простому

Когда браузер открывает сайт по HTTPS, первым сообщением с данными он отправляет TLS ClientHello («привет от клиента»). В нём браузер перечисляет, какое шифрование умеет, и открытым текстом называет нужный сайт: имя лежит в поле SNI (Server Name Indication, «указание имени сервера»). У обычного HTTP без шифрования то же имя стоит в строке Host: запроса. ТСПУ (Технические средства противодействия угрозам — оборудование DPI, то есть глубокой инспекции трафика, которое стоит у провайдера) ищет это имя в первом пакете соединения и по нему решает, что делать с соединением дальше.

HTTP-запросы и TLS-соединения идут поверх протокола TCP, а TCP отправляет данные кусками — сегментами. У каждого байта в соединении есть порядковый номер (sequence number, коротко seq), и сервер раскладывает принятые куски в свой буфер по этим номерам. Кроме того, сервер принимает только те байты, чьи номера попадают в его окно приёма (receive window): оно начинается с номера следующего байта, которого сервер ждёт. Всё, что лежит левее этой границы, TCP-стек молча отбрасывает как повтор или мусор — это штатное поведение, а не ошибка. Почему сборку по номерам гарантирует именно TCP, разобрано в нюансе multisplit «Работает только с TCP».

Движок Zapret 2 (nfqws2) перехватывает исходящий пакет с ClientHello или HTTP-запросом, а функция tcpseg берёт из его данных один кусок и отправляет его в сеть отдельным TCP-сегментом, который она собирает сама. Данные пакета без сетевых заголовков называют payload: это сам HTTP-запрос или сам ClientHello. Границы куска задаёт аргумент pos — ровно два маркера через запятую, начало и конец диапазона. Маркер — это число (0 — первый байт данных, -1 — последний) или название места в запросе, например midsld — середина домена второго уровня, то есть главной части имени сайта (у www.example.com это example). Запись pos=0,-1 означает «весь payload», а pos=0,midsld — «от начала до середины домена». В отличие от multisplit, у tcpseg нет значения pos по умолчанию: без него функция выдаёт ошибку. Все виды маркеров разобраны в разделе Маркеры позиций (pos) — диапазон из двух маркеров.

Кусок отправляют отдельным сегментом, который функция собирает сама, и его параметры задаются аргументами команды: какие байты берутся (pos), сколько байт фальшивки приклеивается слева и как сдвигается seq (seqovl), сколько раз сегмент отправляется (repeats) и какие заголовки в нём меняются (fooling, о нём ниже). Этим можно воспользоваться тремя способами. Первый — отправить весь ClientHello одним сегментом, но с фальшивым префиксом слева (приём seqovl, о нём ниже). Второй — многократно отправить начало запроса в расчёте на то, что буфер DPI окажется забит. Третий — поставить tcpseg кирпичиком в цепочку рядом с другими инстансами, то есть записями --lua-desync=… в профиле (наборе правил Zapret 2 для определённого трафика), например рядом с drop (отдельной функцией, которая выбрасывает оригинальный пакет). Подробно — в разделе Зачем нужен tcpseg.

К отправляемому куску можно добавить приём seqovl (Sequence Overlap, «перекрытие по номерам последовательности»). tcpseg дописывает слева N байт фальшивки — по умолчанию это нули, другой узор задаёт seqovl_pattern — и сдвигает seq сегмента назад на те же N. Для сервера эти байты оказываются левее границы окна приёма: он их выбрасывает и принимает остальное. DPI же может прочитать сегмент целиком и принять фальшивку за начало потока: так бывает, если он читает сегмент от начала и не сверяется с тем, какие байты примет сервер. Обычно фейк прячут от сервера иначе: отдельному пакету-обманке (fake) портят заголовки, например TTL (счётчик «времени жизни» пакета) или номера seq и ack (номер подтверждения), чтобы пакет не дошёл до сервера или сервер его отбросил, а DPI прочитал; такая порча называется fooling (Что такое fooling). У seqovl заголовки остаются правильными, портить их не приходится. В tcpseg seqovl относится к единственному отправляемому сегменту, поэтому весь ClientHello можно отправить одним сегментом с префиксом, без нарезки. Механика по байтам — в разделе seqovl — скрытый фейк внутри сегмента, а как seqovl устроен в multisplit — в его разделе.

Вердикт — это решение движка о судьбе перехваченного оригинального пакета: пропустить его как есть (VERDICT_PASS), выбросить (VERDICT_DROP) или заменить изменённым (VERDICT_MODIFY). Если функция вердикт не вернула, движок считает это за VERDICT_PASS. multisplit после отправки кусков сам выносит VERDICT_DROP. tcpseg не выносит вердикт вообще, поэтому оригинал остаётся жить: если после tcpseg в профиле нет других инстансов, отсутствие вердикта означает «пропустить как есть», оригинал уходит как обычно, и сервер получает данные дважды, из сегмента tcpseg и из оригинала. Вердикты всех инстансов профиля движок складывает, и VERDICT_DROP перебивает остальные, а инстансы после него всё равно вызываются. Поэтому, чтобы оригинал не ушёл, следом ставят отдельный инстанс drop: он только выносит VERDICT_DROP. Подробно — в разделе Вердикт и комбинация с drop, а как движок складывает вердикты нескольких инстансов — в стадии 8 жизненного цикла. Есть и нюанс с фильтрами: drop по умолчанию выбрасывает пакеты любого типа, а tcpseg работает только с распознанными протоколами, см. нюанс 5.

Связка из быстрого старта tcpseg:pos=0,-1:seqovl=1 + drop заменяет оригинальный пакет одним сегментом. Сам tcpseg вердикт не выносит, поэтому оригинал выбрасывает второй инстанс drop, а в сеть уходит тот же ClientHello с одним байтом мусора спереди и seq, сдвинутым на единицу назад. ТСПУ видит поток, который начинается с этого байта, и разбор ClientHello может сбиться; сервер отрезает байт, лежащий левее окна приёма (на схеме — левее вертикальной линии seq=0). Как читать схему: строки — пакеты в порядке отправки, подписанные в тех же обозначениях, что и таблицы в этой статье; по горизонтали — место данных в TCP-потоке (номер последовательности, seq); в колонке справа — что сделал с пакетом сервер; внизу — буфер сервера, в который принятые куски встают по своим seq.

tcpseg: ClientHello одним сегментом с seqovl Карта потока для tcpseg с pos=0,-1 и seqovl=1 в связке с drop: оригинальный пакет выбрасывается, вместо него уходит сегмент с seq=-1 и одним байтом мусора спереди. Сервер отрезает байт левее окна приёма и принимает ClientHello целиком. пакет в порядке отправки место в потоке (seq) → сервер −1 0 26 – ORIG seq=0 len=26 ···········youtube.com···· drop: не ушёл #1 SEG seq=-1 len=27 X···········youtube.com···· байт −1 отрезан буфер сервера ···········youtube.com···· поток собран DPI поток начинается на байт раньше, и первый байт — мусор: разбор ClientHello может сбиться сервер байт левее окна приёма отрезает, остальное принимает; оригинал выбросил инстанс drop

Самая короткая команда — та, что на схеме: весь payload (pos=0,-1), один нулевой байт спереди (seqovl=1) и drop следом. Строку --lua-desync=… пишут в профиль. Для HTTPS к ней добавляют фильтр --payload=tls_client_hello, чтобы функция срабатывала только на TLS ClientHello, как в практическом примере 1; остальные готовые команды собраны в разделах Быстрый старт и Практические примеры. Вот команда без фильтра:

--lua-desync=tcpseg:pos=0,-1:seqovl=1 --lua-desync=drop

tcpseg пригождается и без drop. Строка --payload=http_req --lua-desync=tcpseg:pos=0,method+2:ip_id=rnd:repeats=20 отправляет начало HTTP-запроса до маркера method+2 (что именно попадает в кусок, показано в примере 6) 20 раз подряд со случайным идентификатором IP-пакета (IP ID), а потом уходит и оригинал. Смысл в том, чтобы забить буфер DPI: он может переполниться, и тогда DPI перестанет анализировать поток. См. пример 6 и нюанс 11.

Готового рецепта на все случаи tcpseg не даёт. Расчёт seqovl — на DPI, который читает сегмент от начала и не сверяется с окном приёма сервера (так устроены не все DPI), а работает ли выбранная комбинация у конкретного провайдера, проверяют на практике по чек-листу честной проверки. tcpseg работает только с TCP и самостоятельной стратегией обычно не служит: это строительный блок, который ставят в связку с другими инстансами. Близкие соседи: multisplit режет весь payload на несколько сегментов и сам выбрасывает оригинал; multidisorder отправляет куски в обратном порядке; fakedsplit и fakeddisorder подмешивают к настоящим кускам фейки; fake — отдельный пакет-обманка. Если к tcpseg без drop добавить fooling, из-за которого сервер отбросит сам сегмент (например, tcp_ack=-66000), по смыслу он станет ближе к fake, чем к сегментации: об этом раздел Варианты tcpseg без drop из набора стратегий GUI. Все отличия от соседей сведены в таблицу раздела Отличия от других функций сегментации.


Оглавление


Справка: где функция в коде и родственники

Паспорт функции для тех, кто сверяется с исходниками zapret2: в каком файле и на какой строке лежит код, какой ключ первой версии zapret она заменяет и как объявлена.

Файл: lua/zapret-antidpi.lua:1043 nfqws1 эквивалент: отсутствует (NEW функция в nfqws2) Сигнатура: function tcpseg(ctx, desync)

tcpseg — функция отправки части текущего payload (или reasm, или blob), ограниченной двумя маркерами позиций (диапазоном), в виде TCP-сегмента. В отличие от multisplit, которая разрезает весь payload на множество сегментов по списку позиций, tcpseg вырезает и отправляет один конкретный диапазон данных. При этом tcpseg не выносит вердикт (ни VERDICT_DROP, ни VERDICT_PASS) — для замещения оригинального пакета необходимо комбинировать с функцией drop.

Проще говоря

Payload — полезные данные пакета без сетевых заголовков: сам HTTP-запрос или сам ClientHello. Reasm — те же данные, собранные движком из нескольких пакетов, если запрос в один не влез. Blob — заранее заготовленные данные, которые можно подсунуть вместо настоящих. Какой из трёх источников выбирается, разобрано в разделе Откуда берутся данные. Маркер — способ указать место в данных, диапазон — кусок между двумя маркерами (Маркеры позиций (pos) — диапазон из двух маркеров). VERDICT_DROP — решение «перехваченный пакет не отправлять», VERDICT_PASS — «пропустить как есть»; как движок складывает такие решения от разных функций, описано в стадии 8 жизненного цикла. Функция drop — отдельный инстанс, который только выносит VERDICT_DROP.

Родственные функции: multisplit (множественная сегментация), multidisorder (обратный порядок), fakedsplit (с фейками), fakeddisorder (фейки + обратный порядок), hostfakesplit (по hostname), oob (urgent byte).

Общий для всех техник дурения порядок работы — восемь стадий от отсева чужого транспорта до вердикта — разобран в жизненный цикл desync-функции; здесь описано только то, чем tcpseg от этого скелета отличается.


Зачем нужен tcpseg

tcpseg решает задачу, которую не решает multisplit: отправить конкретный фрагмент payload (или весь payload целиком) как отдельный TCP-сегмент, с опциональным seqovl, без автоматического вынесения вердикта.

Как seqovl устроен в multisplit, разобрано в его разделе; здесь он разобран для tcpseg в разделе seqovl — скрытый фейк внутри сегмента, а вердикт — в разделе Вердикт и комбинация с drop.

Основные сценарии использования:

  1. seqovl без сегментации. Отправить весь payload целиком с seqovl-префиксом, подменяющим начало потока для DPI. Маркеры pos=0,-1 задают диапазон “от начала до конца” — весь payload
  2. Повторная отправка начала потока. Отправить первые N байт payload многократно (repeats=N), чтобы забить буфер DPI мусором. Маркеры pos=0,method+2 или pos=0,midsld отправляют только начало
  3. Модульная композиция. Поскольку tcpseg не выносит вердикт, его можно комбинировать с другими инстансами (например, drop, luaexec) в цепочке

Первый сценарий в двух словах: весь ClientHello уходит одним сегментом, но с фальшивым префиксом слева, который для DPI выглядит как начало потока, а сервер его отбрасывает (пример: Минимальный seqovl без сегментации (TLS)). Второй: начало запроса отправляется много раз, и DPI, по описанию в примере Повторная отправка до midsld (TLS), может переполнить свой буфер и перестать анализировать поток. Третий: tcpseg сам оригинал не заменяет, поэтому его ставят в цепочку рядом с инстансом, который это сделает, например с drop, или с luaexec (он выполняет произвольный Lua-код, как в примере Динамический случайный seqovl через luaexec).

Ключевое отличие от multisplit: multisplit разрезает и отправляет весь payload, затем дропает оригинал. tcpseg отправляет часть (или весь) payload и не дропает оригинал — нужна отдельная команда drop.

Проще говоря

multisplit работает по схеме «разрезал всё, отправил куски, оригинал выбросил». tcpseg работает по схеме «вырезал один кусок и отправил, а оригинал остаётся на усмотрение остальных инстансов профиля». Если после tcpseg не поставить drop, оригинал уйдёт вслед за ним.


Быстрый старт

Ниже четыре готовые строки для профиля Zapret 2, от самой короткой к более сложным. Флаг --payload=… работает как фильтр для всех вызовов --lua-desync, которые идут после него (до следующего --payload или до конца профиля): они срабатывают только на данных указанного типа. Поэтому --payload пишут перед --lua-desync, а не после (подробно — в последовательность аргументов).

Минимальный seqovl (весь payload + 1 байт нулевого seqovl, dir=out, payload=known):

--lua-desync=tcpseg:pos=0,-1:seqovl=1 --lua-desync=drop

Здесь pos=0,-1 — весь payload, seqovl=1 — один нулевой байт слева, а второй инстанс drop выбрасывает оригинал. Значения dir=out и payload=known берутся по умолчанию: только исходящие пакеты и только распознанные протоколы; оба аргумента разобраны в разделах B) Standard direction и C) Standard payload.

seqovl с кастомным паттерном (TLS):

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,-1:seqovl=5:seqovl_pattern=0x1603030000 \
  --lua-desync=drop

Функция срабатывает только на TLS ClientHello, отправляет его целиком, а слева приклеивает 5 байт 0x1603030000, похожих на начало TLS record — заголовка записи TLS. Эти байты задаёт seqovl_pattern, см. раздел seqovl_pattern.

Повторная отправка начала HTTP-запроса:

--payload=http_req --lua-desync=tcpseg:pos=0,method+2:ip_id=rnd:repeats=20

Здесь отправляется начало запроса до method+2 (что именно попадает в кусок, показано в примере 6), ip_id=rnd задаёт случайные IP ID, а repeats=20 — 20 повторов. drop нет, поэтому оригинал тоже уходит. Разбор — в примере 6.

Динамический seqovl со случайным паттерном:

--lua-desync=luaexec:code='desync.rnd=brandom_az(math.random(5,10))' \
--lua-desync=tcpseg:pos=0,-1:seqovl=#rnd:seqovl_pattern=rnd \
--lua-desync=drop:payload=known

Перед tcpseg luaexec на каждый пакет генерирует случайную строку из букв a-z длиной от 5 до 10 символов; она служит и паттерном (seqovl_pattern=rnd), и источником длины (seqovl=#rnd — размер этого blob). drop:payload=known выбрасывает оригинал только для распознанных протоколов. Разбор — в примере 4.


Откуда берутся данные

Внутри tcpseg данные (data) выбираются в следующем порядке приоритетов:

1. blob_or_def(desync, desync.arg.blob)    -- если задан blob= и он существует
2. desync.reasm_data                        -- если есть реассемблированные данные (multi-packet payload)
3. desync.dis.payload                       -- текущий пакет (fallback)

Следствие: маркеры pos и seqovl применяются именно к тем данным, которые реально выбраны. Если вы задали blob=myblob, маркеры вроде midsld будут работать только если myblob содержит валидный TLS/HTTP payload, который zapret может распознать.

Разберём три источника. Первый — blob: заранее заготовленные данные, которые можно подсунуть вместо настоящих; что это такое и как их объявлять, описано в blob. Если задан blob=имя, функция берёт его содержимое и полностью игнорирует реальный payload. Второй — реассемблированные данные (reasm, от английского reassembly — «повторная сборка»): запрос, склеенный движком из нескольких TCP-пакетов, если он в один не влез; как такой запрос отправляется, описано в разделе Поведение при replay / reasm. Третий — payload текущего пакета, desync.dis.payload: здесь dis — диссект, то есть пакет, заранее разобранный движком на поля (его устройство — в структура desync и диссекта). Lua-оператор or возвращает первый истинный операнд, поэтому цепочка читается «сверху вниз, берём первое, что есть». Та же цепочка общая для большинства функций дурения и описана как стадия 4 жизненного цикла.

Если blob задан, а относительные маркеры вроде midsld не срабатывают, причина может быть в том, что движок не распознал содержимое blob как TLS или HTTP. Тип payload — это название распознанного протокола вроде tls_client_hello или http_req; как он определяется, описано в заметке о типах payload.

Проще говоря

Функция спрашивает: «мне подсунули blob? — беру его. Есть запрос, собранный из кусков? — беру его. Иначе беру тело текущего пакета». Маркеры и seqovl всегда работают по тем данным, которые выиграли этот выбор.


Маркеры позиций (pos) — диапазон из двух маркеров

pos — обязательный аргумент tcpseg. Если pos не задан, функция вызовет error("tcpseg: no pos specified"). Это отличает tcpseg от multisplit, где pos по умолчанию "2".

Задаётся как строка с ровно двумя маркерами через запятую. Первый маркер — начало диапазона, второй — конец.

Маркер — это «адрес» байта в данных: число или название места в запросе. Превратить маркер в конкретный номер байта — значит его разрешить; для диапазона это делает функция resolve_range. Диапазон — кусок данных между первым и вторым маркерами: именно он вырезается и отправляется. Как те же маркеры служат точками разреза в multisplit, разобрано в его разделе.

Проще говоря

pos=A,B значит «отправь кусок от A до B». Например, pos=0,-1 — весь запрос, а pos=0,midsld — от начала до середины домена второго уровня.

Отличие от multisplit: resolve_range vs resolve_multi_pos

Таблица показывает, чем tcpseg отличается от multisplit при разборе pos. «Функция разрешения» — та, что превращает названия маркеров в номера байтов; обе написаны на C и вызываются из Lua. Главная разница: multisplit получает список точек разреза, а tcpseg — начало и конец одного куска.

Аспектtcpsegmultisplit
Функция разрешенияresolve_range (C-код)resolve_multi_pos (C-код)
Количество маркеровРовно 2 (ошибка, если не 2)Любое количество
Семантика маркеровНачало и конец диапазонаТочки разреза
РезультатТаблица из 2 позиций {start, end} или nilМассив уникальных позиций
Что отправляетсяdata:sub(pos[1], pos[2]) — данные между маркерамиВесь payload, разрезанный по позициям

Типы маркеров

Маркеры бывают трёх видов. Абсолютные отсчитывают байты от начала или от конца данных и не зависят от содержимого. Относительные ищут место по смыслу — начало имени хоста, середину домена — и потому работают, только если движок распознал протокол; как он это делает, описано в заметке о типах payload. Те же маркеры с примерами для разрезов разобраны в multisplit.

ТипОписаниеПример
Абсолютный положительныйСмещение от начала payload. 0 = первый байт (в resolve_range используется zero-based нумерация на входе, но внутренне конвертируется в 1-based Lua)0, 5, 100
Абсолютный отрицательныйСмещение от конца payload. -1 = последний байт-1, -10, -50
ОтносительныйЛогическая позиция внутри распознанного payload. Привязана к структуре протоколаmidsld, host, sniext

Относительные маркеры

В правой колонке указано, в каких типах payload маркер имеет смысл: http_req — HTTP-запрос, tls_client_hello — TLS ClientHello. SLD (second-level domain) — домен второго уровня: в www.example.com это example. SNI extension — расширение ClientHello, в котором браузер передаёт имя сайта.

МаркерОписаниеДля каких payload
methodНачало HTTP-метода (GET, POST, HEAD, PUT и т.д.). Обычно позиция 0, но может стать 1-2 при использовании http_methodeolhttp_req
hostПервый байт имени хоста (Host: в HTTP, SNI в TLS)http_req, tls_client_hello
endhostБайт, следующий за последним байтом имени хоста. Т.е. host..endhost-1 = полный hostnamehttp_req, tls_client_hello
sldПервый байт домена второго уровня (SLD). Для www.example.com — это e в examplehttp_req, tls_client_hello
endsldБайт, следующий за последним байтом SLD. Для example.com — это . после examplehttp_req, tls_client_hello
midsldСередина SLD (самый популярный маркер). Для example (7 символов) — позиция 3-го или 4-го символаhttp_req, tls_client_hello
sniextНачало поля данных SNI extension в TLS ClientHello. Extension состоит из type (2 байта) + length (2 байта) + данные — sniext указывает на начало данныхtls_client_hello
extlenПоле длины всех TLS extensionstls_client_hello

Арифметика маркеров

Маркер можно сдвинуть на несколько байт вперёд или назад, если границу диапазона нужно поставить рядом с ним, а не точно в нём. К любому маркеру можно прибавить (+) или вычесть (-) целое число:

midsld+1      -- один байт ПОСЛЕ середины SLD
midsld-1      -- один байт ДО середины SLD
endhost-2     -- два байта до конца hostname
method+2      -- два байта после начала метода (для "GET /" -> диапазон 0..method+2 захватит "GE")
sniext+1      -- один байт после начала SNI extension data
host+3        -- три байта после начала hostname
0             -- начало payload
-1            -- последний байт payload

Поведение resolve_range при неразрешённых маркерах

Маркер называют неразрешённым, если движок не нашёл для него места в данных: например, midsld для unknown payload, то есть для данных, тип которых не распознан. Таблица ниже говорит, что делает resolve_range в таких случаях; «non-strict режим» означает, что функция вызвана без флага strict.

resolve_range реализована в C-коде (nfq2/lua.c:3281). Поведение при неразрешённых маркерах:

СитуацияПоведение (non-strict режим)
Оба маркера разрешеныВозвращает {pos[0], pos[1]} — нормальный диапазон
Первый маркер не разрешён, второй разрешёнПервый маркер заменяется на 0 (начало данных)
Первый маркер разрешён, второй не разрешёнВторой маркер заменяется на len-1 (конец данных)
Оба маркера не разрешеныВозвращает nil — диапазон не может быть определён
pos[0] > pos[1] (начало после конца)Возвращает nil — невалидный диапазон

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

Проще говоря

Если не нашёлся один маркер, диапазон растягивается до края данных: пропавший первый маркер становится началом данных, пропавший второй — концом. Если не нашлись оба или начало оказалось правее конца, tcpseg ничего не отправляет и пишет об этом в лог (подробнее — нюанс 10).

Важные нюансы pos

  • pos обязателен. В отличие от multisplit, где pos по умолчанию "2", в tcpseg отсутствие pos вызывает error(). Это жёсткое требование
  • Ровно 2 маркера. Если передать 1 или 3+ маркера, resolve_range вызовет luaL_error("resolve_range require 2 markers")
  • Маркер 0 — начало данных. В отличие от multisplit, где позиция 1 удаляется (delete_pos_1), в tcpseg маркер 0 — это валидное начало диапазона
  • Маркеры 0,-1 — весь payload. Самый частый паттерн: pos=0,-1 означает “от начала до конца” — весь payload целиком. Используется для seqovl без сегментации

Те же требования разобраны отдельно в нюансах 2, 3 и 6. Про удаление позиции в multisplit — нюанс 2 его статьи.


seqovl — скрытый фейк внутри сегмента

seqovl (Sequence Overlap) — техника скрытого замешивания фейковых данных в реальный TCP-сегмент через манипуляцию TCP sequence number. В tcpseg seqovl применяется к отправляемому сегменту (он всегда один).

Смысл приёма в том, что TCP-получатель принимает не все байты подряд. Он держит окно приёма (receive window) и принимает байты, номера которых попадают в него, начиная с номера следующего ожидаемого байта; всё левее этой границы отбрасывается как повтор или мусор. Если приклеить слева фальшивку и сдвинуть seq назад на её длину, фальшивка окажется левее границы: сервер её выбросит, а DPI, читающий сегмент от начала, увидит её вместе с остальным. Полное объяснение по шагам — в принципе работы seqovl в multisplit; ниже те же цифры для tcpseg, где сегмент один.

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

Принцип работы seqovl

Схема ниже показывает один сегмент с pos=0,-1 и seqovl=10. TCP seq: 1000 — номер, с которого начинались бы данные без seqovl; после seqovl он уменьшается на 10, а перед данными встают 10 байт паттерна. Что видит каждая из сторон, подписано внутри схемы.

Без seqovl (pos=0,-1):
  TCP seq: 1000
  Данные:  [ВЕСЬ_PAYLOAD]
  Сервер:  принимает [ВЕСЬ_PAYLOAD] целиком

С seqovl=10 (pos=0,-1):
  TCP seq: 990             (уменьшен на 10)
  Данные:  [PATTERN_10_БАЙТ][ВЕСЬ_PAYLOAD]

  Что видит DPI:
    Единый TCP-сегмент начиная с seq 990.
    DPI анализирует весь блок, включая PATTERN.
    Если PATTERN содержит ложный SNI -- DPI может принять его за настоящий.

  Что видит сервер (TCP-стек):
    TCP window начинается с seq 1000.
    Байты 990-999 выходят за левую границу window -> отбрасываются.
    Байты с 1000 (ВЕСЬ_PAYLOAD) -> принимаются.

Визуализация:

           TCP window boundary
                 |
  |  ОТБРОСИТЬ  | ПРИНЯТЬ             |
  | PATTERN(10) | ВЕСЬ_PAYLOAD        |
  ^seq=990       ^seq=1000

Визуализация с pos=0,midsld (частичная отправка):

  Payload: [====НАЧАЛО====|====midsld====|====КОНЕЦ====]
                           ^pos[1]

  Отправляется только [====НАЧАЛО====|====midsld====]
  (от начала до midsld включительно)

  С seqovl=5:
  TCP seq: -5 (относительно начала отправляемого куска)
  Данные:  [PAT 5][====НАЧАЛО====|====midsld====]

Во втором случае отправляется только часть payload — от начала до midsld; -5 в строке TCP seq — сдвиг относительно начала отправляемого куска.

Проще говоря

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

Зачем seqovl лучше обычного fooling

Обычный способ спрятать фейк от сервера — fooling: пакету-обманке портят заголовки (TTL, номера seq или ack, опцию md5sig), и сервер (или маршрутизатор по пути) отбрасывает такой пакет целиком. seqovl добивается похожего результата другим путём; таблица сравнивает оба подхода и совпадает с той, что приведена в multisplit.

КритерийОбычный fooling (TTL, badseq, md5sig)seqovl
ЗаголовкиМодифицируются (TTL, seq, ack, md5)Не модифицируются — пакет выглядит полностью легитимным
ОбнаружениеDPI может детектировать подозрительные заголовкиDPI видит “честный” сегмент с правильными заголовками
Механизм отбрасыванияСервер отбрасывает весь пакет из-за невалидных заголовковСервер отбрасывает только часть, выходящую за TCP window
НадёжностьЗависит от поведения конкретного стекаОснован на фундаментальном свойстве TCP

Вывод: seqovl — средство создания скрытых фейков, не требующее fooling. Это его ключевое преимущество. В tcpseg seqovl особенно силён в комбинации с pos=0,-1, где весь payload отправляется с фейковым префиксом.

seqovl_pattern

Паттерн, которым заполняется seqovl-область (N байт слева от реальных данных). По умолчанию — 0x00 (нули).

В tcpseg seqovl_pattern — это имя blob. Паттерн повторяется до нужной длины seqovl. Blob можно записать прямо в команде шестнадцатеричной строкой (inline hex, 0x…), заранее объявить флагом --blob и сослаться на него по имени или сгенерировать на лету через luaexec, как в третьем примере ниже; подробно про blob — в заметке blob.

# Inline hex blob (маскировка под начало TLS record)
--lua-desync=tcpseg:pos=0,-1:seqovl=5:seqovl_pattern=0x1603030000
 
# Предзагруженный blob
--blob=tlspat:0x1603030100 \
--lua-desync=tcpseg:pos=0,-1:seqovl=8:seqovl_pattern=tlspat
 
# Динамически сгенерированный blob через luaexec
--lua-desync=luaexec:code='desync.rnd=brandom_az(math.random(5,10))' \
--lua-desync=tcpseg:pos=0,-1:seqovl=#rnd:seqovl_pattern=rnd

Если optional задан и blob seqovl_pattern отсутствует — используется нулевой паттерн (операция не отменяется). Сам флаг описан в разделе A) Собственные аргументы tcpseg.

Важно в tcpseg: seqovl — только число, маркеры не поддерживаются (как и в multisplit, в отличие от multidisorder и fakeddisorder, где seqovl может быть маркером). Как seqovl задаётся маркером там, разобрано в разделах seqovl как маркер и seqovl — маркер, а не число.


Вердикт и комбинация с drop

Вердикт — решение движка о судьбе перехваченного оригинального пакета: VERDICT_PASS пропускает его как есть, VERDICT_DROP выбрасывает, VERDICT_MODIFY заменяет пакет изменённым; отсутствие вердикта приравнивается к VERDICT_PASS. Полная таблица и правила сложения вердиктов от разных инстансов — в стадии 8 жизненного цикла.

Ключевая особенность tcpseg: функция не выносит вердикт. Она не возвращает ни VERDICT_DROP, ни VERDICT_PASS. Это означает:

  1. После выполнения tcpseg оригинальный пакет не блокируется
  2. Следующие инстансы в цепочке продолжают обработку
  3. Если после tcpseg нет других инстансов — оригинальный пакет уйдёт как есть

Вердикты нескольких инстансов складываются по правилу «VERDICT_MODIFY перебивает VERDICT_PASS, VERDICT_DROP перебивает оба»: поэтому drop в профиле выбрасывает оригинал, даже если сам tcpseg ничего не решил. Дроп цепочку не обрывает: инстансы после него всё равно вызываются и видят тот же пакет.

Для замещения оригинального пакета tcpseg нужно комбинировать с drop:

# tcpseg отправляет payload с seqovl, drop блокирует оригинал
--lua-desync=tcpseg:pos=0,-1:seqovl=5 --lua-desync=drop

Нюанс с payload-фильтрами: по умолчанию tcpseg работает только с known payload, а drop — с all. Если вы хотите дропать только известные payload (чтобы неизвестные проходили без изменений), укажите:

--lua-desync=tcpseg:pos=0,-1:seqovl=5 --lua-desync=drop:payload=known

Здесь known — только распознанные протоколы (http_req, tls_client_hello и другие), а all — любой payload, включая unknown, то есть данные, тип которых движок не распознал. Подробнее — в разделе C) Standard payload и в заметке о типах payload.

Диаграмма прохождения пакета:

Пакет приходит
    |
    v
[tcpseg] -- отправляет TCP-сегмент (часть payload с seqovl)
    |       НЕ выносит вердикт
    v
[drop]   -- выносит VERDICT_DROP, блокируя оригинальный пакет
    |
    v
Оригинальный пакет заблокирован.
Сервер получает только то, что отправил tcpseg.

Без drop:

Пакет приходит
    |
    v
[tcpseg] -- отправляет TCP-сегмент
    |       НЕ выносит вердикт
    v
Нет больше инстансов -> VERDICT_PASS (по умолчанию)
    |
    v
Оригинальный пакет тоже уходит!
Сервер получает данные ДВАЖДЫ (tcpseg + оригинал).

Проще говоря

tcpseg только добавляет в сеть свой сегмент, а оригинал не трогает, пока этого не сделает другой инстанс. С drop данные уходят один раз, сегментом tcpseg; без drop дважды. Как multisplit решает то же самое сам и зачем ему для отладки аргумент nodrop, описано в его нюансе 4.


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

Аргументы делятся на собственные аргументы tcpseg (раздел A) и стандартные (разделы B–H): их понимают и другие функции дурения, а общий обзор есть в desync. Формат вызова:

--lua-desync=tcpseg[:arg1[=val1][:arg2[=val2]]...]

Все val приходят в Lua как строки. Если =val не указан, значение = пустая строка "" (в Lua это truthy, то есть считается истинным значением), поэтому флаги пишутся просто как :optional.

A) Собственные аргументы tcpseg

pos

  • Формат: pos=<marker1,marker2>
  • Тип: строка с ровно двумя маркерами через запятую
  • По умолчанию: нет (обязательный параметр; если не задан — error())
  • Описание: Диапазон данных для отправки. Первый маркер — начало, второй — конец (включительно). Из данных вырезается data:sub(pos[1], pos[2]) и отправляется как TCP-сегмент (в Lua sub берёт кусок строки от первого номера до второго). Подробно о маркерах — в разделе Маркеры позиций (pos) — диапазон из двух маркеров
  • Примеры:
    • pos=0,-1 — весь payload (от начала до конца)
    • pos=0,midsld — от начала до середины SLD
    • pos=0,method+2 — от начала до 2 байт после начала HTTP-метода
    • pos=host,endhost — hostname и ещё один байт сразу после него (endhost указывает на байт после имени, а конец диапазона включительный)
    • pos=0,1 — первые два байта (для TLS: первые два байта TLS record)
    • pos=sniext,endhost — от начала SNI extension data до конца hostname и один байт после него

Конец диапазона включительный — проверено по коду

resolve_range (nfq2/lua.c) переводит оба маркера в позиции и прибавляет единицу для Lua, а data:sub(pos[1], pos[2]) берёт оба конца. Поэтому байт, на который указывает второй маркер, тоже попадает в сегмент: числовой маркер 1 — это второй байт payload, и pos=0,1 отправляет два байта, а pos=0,method+2 для GET / отправляет GET, а не GE. Чтобы закончить диапазон ровно перед маркером, вычтите единицу: pos=0,method+1 даст GE, pos=host,endhost-1 — ровно hostname. Комментарий «захватит GE» в схеме арифметики маркеров, раздел «Пример без seqovl (pos=host,endhost)» и заголовок примера 10 «Только первый байт TLS record» написаны без учёта этой единицы.

seqovl

  • Формат: seqovl=N (где N > 0)
  • Тип: только число (маркеры не поддерживаются — как и в multisplit)
  • По умолчанию: не задан (нет seqovl)
  • Описание: К данным сегмента слева добавляется N байт seqovl_pattern, а TCP th_seq (поле номера последовательности в TCP-заголовке) уменьшается на N. Сервер отбросит левую часть, DPI — может не отбросить. Механика — в разделе seqovl — скрытый фейк внутри сегмента
  • Примеры:
    • seqovl=1 — 1 байт фейка слева (минимальный)
    • seqovl=5 — 5 байт фейка слева
    • seqovl=#rnd — размер blob rnd (C-подстановка через #)
    • seqovl=10000 — 10000 байт (автосегментация по MSS разобьёт на несколько TCP-сегментов)

seqovl_pattern

  • Формат: seqovl_pattern=<blobName>
  • Тип: имя blob-переменной
  • По умолчанию: один байт 0x00, повторяемый до длины seqovl
  • Описание: Данные для заполнения seqovl-области. Blob повторяется функцией pattern() до нужного размера. Подробно — в разделе seqovl_pattern
  • Поведение с optional: если optional задан и blob отсутствует — используется нулевой паттерн, seqovl не отменяется
  • Примеры:
    • seqovl_pattern=0x1603030000 — inline hex (маскировка под TLS)
    • seqovl_pattern=rnd — динамически сгенерированный blob
    • seqovl_pattern=fake_default_tls — стандартный TLS-фейк как паттерн

blob

  • Формат: blob=<blobName>
  • Тип: имя blob-переменной
  • По умолчанию: не задан
  • Описание: Заменить текущий payload/reasm на указанный blob и отправить из него диапазон pos. Используется для отправки произвольных данных. Что такое blob и как его объявить — в blob
  • Примеры:
    • blob=fake_default_tls — стандартный TLS-фейк
    • blob=0xDEADBEEF — inline hex
    • blob=my_custom_data — предзагруженный blob

optional

  • Формат: optional (флаг, без значения)
  • Описание: Мягкий режим:
    • Если задан blob=... и blob отсутствует — tcpseg ничего не делает (тихий skip, без ошибок)
    • Если задан seqovl_pattern=... и blob отсутствует — используется нулевой паттерн (seqovl не отменяется)
  • Использование: защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией). Та же проверка описана в стадии 3 жизненного цикла

B) Standard direction

ПараметрЗначенияПо умолчанию
dirin, out, anyout

Фильтр по направлению пакета. tcpseg по умолчанию работает только с исходящими (out).

  • dir=out — только исходящие (от клиента к серверу)
  • dir=in — только входящие (от сервера к клиенту)
  • dir=any — оба направления

При первом вызове с указанным dir функция делает direction_cutoff_opposite — отсекает себя от противоположного направления. Проще говоря, инстанс перестаёт получать пакеты не своего направления, и движок не тратит время на Lua-вызовы там, где они заведомо бесполезны; проверка направления текущего пакета (direction_check) выполняется позже, см. стадию 2 жизненного цикла.


C) Standard payload

ПараметрЗначенияПо умолчанию
payloadсписок типов через запятуюknown

Фильтр по типу payload на уровне Lua. Это дополнительный фильтр к --payload=... на уровне профиля. Тип payload — название протокола, который движок распознал в данных пакета; см. заметку о типах payload.

  • payload=known — только распознанные протоколы (http_req, tls_client_hello, quic_initial и т.д.)
  • payload=all — любой payload, включая unknown
  • payload=tls_client_hello,http_req — конкретные типы
  • payload=~unknown — инверсия: всё кроме unknown

Важно: лучше ставить --payload=... на уровне профиля (C-код, быстрее), а не полагаться только на Lua-фильтр.


D) Standard fooling

Модификации L3/L4 заголовков (то есть заголовков IP и TCP), то самое «портящее» вмешательство — fooling; общий разбор — в Что такое fooling и каталоге способов. В tcpseg применяются к отправляемому сегменту.

ПараметрОписаниеПример
ip_ttl=NУстановить IPv4 TTLip_ttl=6
ip6_ttl=NУстановить IPv6 Hop Limitip6_ttl=6
ip_autottl=delta,min-maxАвтоматический TTL (delta от серверного TTL)ip_autottl=-2,40-64
ip6_autottl=delta,min-maxАналогично для IPv6ip6_autottl=-2,40-64
ip6_hopbyhop[=HEX]Вставить extension header hop-by-hop (по умолчанию 6 нулей)ip6_hopbyhop
ip6_hopbyhop2[=HEX]Второй hop-by-hop headerip6_hopbyhop2
ip6_destopt[=HEX]Destination options headerip6_destopt
ip6_destopt2[=HEX]Второй destination optionsip6_destopt2
ip6_routing[=HEX]Routing headerip6_routing
ip6_ah[=HEX]Authentication headerip6_ah
tcp_seq=NСместить TCP sequence (+ или -)tcp_seq=-10000
tcp_ack=NСместить TCP ack (+ или -)tcp_ack=-66000
tcp_ts=NСместить TCP timestamptcp_ts=-100
tcp_md5[=HEX]Добавить TCP MD5 option (16 байт; по умолчанию случайные)tcp_md5
tcp_flags_set=LISTУстановить TCP-флагиtcp_flags_set=FIN,PUSH
tcp_flags_unset=LISTСнять TCP-флагиtcp_flags_unset=ACK
tcp_ts_upПоднять TCP timestamp option в начало заголовкаtcp_ts_up
tcp_nop_delУдалить все TCP NOP опцииtcp_nop_del
fool=<func>Кастомная Lua-функция foolingfool=my_fooler

Предупреждение: fooling в tcpseg применяется к реальному сегменту (не к фейку). Если задать tcp_ack=-66000, сервер отбросит сегмент. Fooling в tcpseg имеет смысл только для безопасных вещей: tcp_ts_up, ip_id, IPv6 extension headers.

Проще говоря

Fooling в tcpseg бьёт по тому самому сегменту, который должен дойти до сервера. Поэтому опции, из-за которых сервер отбросит пакет (например, tcp_ack=-66000), портят задуманное. Исключение — вариант из раздела Варианты tcpseg без drop из набора стратегий GUI, где сегмент с fooling нарочно превращается в фейк. Подробнее про сами опции — в ts-and-fooling.


E) Standard ipid

IP ID — идентификатор в заголовке IPv4-пакета. Эти параметры задают, какие значения он получит.

ПараметрОписаниеПо умолчанию
ip_id=seqПоследовательные IP IDseq
ip_id=rndСлучайные IP ID—
ip_id=zeroНулевые IP ID—
ip_id=noneНе менять IP ID—
ip_id_connСквозная нумерация IP ID в рамках соединения (требует tracking)—

ip_id применяется к отправляемому сегменту (включая под-сегменты при MSS-сегментации, см. Автосегментация по MSS).


F) Standard ipfrag

IP-фрагментация поверх TCP-сегмента. Отправленный TCP-сегмент дополнительно фрагментируется на уровне IP. Проще говоря: сегмент, уже вырезанный tcpseg, при отправке ещё раз делится на части, но теперь средствами протокола IP, а собирает их обратно получатель на уровне IP. Пример — seqovl без сегментации + IP-фрагментация.

ПараметрОписаниеПо умолчанию
ipfrag[=func]Включить IP-фрагментацию. Если без значения — ipfrag2—
ipfrag_disorderОтправить IP-фрагменты в обратном порядке—
ipfrag_pos_tcp=NПозиция фрагментации TCP (кратно 8)32
ipfrag_pos_udp=NПозиция фрагментации UDP (кратно 8). Для tcpseg бесполезно — он только TCP8
ipfrag_next=NIPv6: next protocol во 2-м фрагменте (penetration атака на фаерволы)—

G) Standard reconstruct

Реконструкция — сборка готового raw-пакета («сырого», со всеми заголовками) перед отправкой.

ПараметрОписание
badsumИспортить L4 (TCP) checksum при реконструкции raw-пакета. Сервер отбросит такой пакет

H) Standard rawsend

Rawsend — отправка готовых сегментов в сеть самой функцией; в tcpseg её выполняет rawsend_payload_segmented (см. Автосегментация по MSS). Эти параметры управляют отправкой.

ПараметрОписание
repeats=NОтправить сегмент N раз (идентичные повторы)
ifout=<iface>Интерфейс для отправки (по умолчанию определяется автоматически)
fwmark=NFirewall mark (только Linux, nftables/iptables)

Порядок отправки

tcpseg отправляет один сегмент (диапазон данных между двумя маркерами), опционально с seqovl-префиксом. Для сравнения: multisplit отправляет несколько сегментов и следит за их порядком (его раздел), а здесь сегмент один. Подпись seq= на схемах ниже — сдвиг номера последовательности первого байта сегмента: смещение первого байта сегмента относительно начала данных. В примере с host это host_offset. Если seqovl задан, смещение уменьшается на его длину; отрицательным оно получается в примерах pos=0,… (-10, -5), где кусок начинается с нулевого байта. В коде это третий аргумент вызова rawsend_payload_segmented, pos[1]-1-seqovl.

Пример без seqovl (pos=host,endhost)

Payload (600 байт):
[===ЗАГОЛОВКИ===][example.com][===ОСТАЛЬНОЕ===]
                 ^pos[1]=host  ^pos[2]=endhost

Отправляется:
  Сегмент: [example.com]  seq=host_offset  len=(endhost-host)

Оригинальный пакет: НЕ блокируется (нет вердикта)

Диапазон между маркерами host и endhost уходит отдельным сегментом, а оригинал остаётся нетронутым: у tcpseg вердикта нет.

Пример с seqovl=10 (pos=0,-1)

Payload (600 байт), pos=0,-1, seqovl=10:

  Сегмент: [PATTERN(10)][ВЕСЬ_PAYLOAD_600_БАЙТ]
           seq=-10   len=610

  Сервер: отбросит первые 10 байт, примет 600
  DPI: проанализирует все 610 байт

Проще говоря: tcpseg дописывает 10 байт паттерна спереди и сдвигает seq на −10. Сервер принимает 600 настоящих байт, а 10 байт паттерна отбрасывает как вышедшие за окно приёма; DPI видит все 610.

Пример с частичной отправкой и seqovl (pos=0,midsld, seqovl=5)

Payload: [====ДО_MIDSLD====][====ПОСЛЕ_MIDSLD====]
                             ^midsld

Отправляется:
  Сегмент: [PAT(5)][====ДО_MIDSLD====]
           seq=-5   len=(midsld+5)

Оригинальный пакет: НЕ блокируется (нужен drop)

Здесь отправляется только начало payload: от первого байта до midsld. Перед ним стоят 5 байт паттерна с seq на 5 меньше. Чтобы оригинал не ушёл следом, нужен drop.


Поведение при replay / reasm

Многопакетный запрос nfqws2 придерживает, собирает в буфер reasm_data и перепроигрывает через desync-функции (жизненный цикл desync-функции, стадия 6). tcpseg использует из этой развилки только первую половину: на первой части вырезает диапазон из собранного буфера и отправляет его, на остальных частях логирует «not acting on further replay pieces» и возвращает nil.

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

Важный нюанс: поскольку tcpseg не выносит вердикт, на последующих частях replay оригинальные пакеты пройдут без изменений. Если drop стоит после tcpseg — он заблокирует все пакеты (и первую часть, и последующие). Поэтому связка tcpseg + drop корректно обрабатывает reasm: tcpseg отправляет весь reasm при первой части, drop блокирует все оригинальные части.

Проще говоря

Если запрос пришёл несколькими пакетами, движок сначала собирает его целиком (reasm), а затем по очереди вызывает функцию на каждой придержанной части (replay — «перепроигрывание»). tcpseg отправляет свой кусок один раз, на первой части. Остальные части сам не дропает: их судьбу решает drop, если он стоит в профиле. multisplit устроен по-другому: он сам дропает остальные части, см. его раздел.


Автосегментация по MSS

MSS (Maximum Segment Size) — наибольший объём данных, который помещается в один TCP-сегмент данного соединения. О размерах TCP-сегментов думать не нужно. Функция rawsend_payload_segmented из zapret-lib.lua автоматически:

  1. Отслеживает MSS для каждого TCP-соединения
  2. Если отправляемая часть (включая seqovl) превышает MSS — дополнительно режет по MSS
  3. Каждый под-сегмент отправляется с корректным TCP sequence

Пример: если вы задали pos=0,-1:seqovl=10000, это не вызовет ошибку. rawsend_payload_segmented отправит несколько TCP-сегментов с начальным sequence -(10000), общим размером 10000 байт seqovl-pattern, и в последних сегментах — реальные данные payload.

Проще говоря

Слишком большой сегмент функция сама режет на допустимые куски и каждому ставит правильный seq; настраивать ничего не нужно. Так же это устроено в multisplit.


Место в общем скелете

Тело tcpseg построено по тому же шаблону, что и все остальные функции дурения: восемь стадий от отсева чужого транспорта до вердикта. Разобраны они один раз в жизненный цикл desync-функции — здесь только отклонения. Гвард (guard) — проверка, которая решает, работать ли функции с этим конкретным пакетом. Названия стадий в левой колонке — ссылки на их подробный разбор.

Стадия общего скелетаЧто делает tcpseg
1. Отсев транспортатолько TCP; на не-TCP делает instance_cutoff (кроме связанного ICMP)
2. Направлениеdir=out по умолчанию, отключается от входящего
3. Аргументыpos обязателен — без него error(); optional + отсутствующий blob → тихий выход
4. Данныештатная цепочка blob → reasm → payload
5. Гвардыштатные #data>0, direction_check, payload_check (по умолчанию known)
6. replayиспользует только первую половину развилки: работает на первой части, остальные не дропает
7. Своя техникаresolve_range разрешает ровно два маркера в диапазон, отправляется один сегмент с этим куском данных
8. Вердиктне выносит никакого — оригинальный пакет уходит как обычно

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

Проще говоря

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


Псевдокод алгоритма

Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в жизненный цикл desync-функции:

function tcpseg(ctx, desync)
    -- 1. Проверка: только TCP
    if not desync.dis.tcp then
        if not desync.dis.icmp then instance_cutoff_shim(ctx, desync) end
        return   -- без вердикта
    end
 
    -- 2. Cutoff противоположного направления
    direction_cutoff_opposite(ctx, desync)
 
    -- 3. pos ОБЯЗАТЕЛЕН
    if not desync.arg.pos then
        error("tcpseg: no pos specified")
    end
 
    -- 4. Проверка optional blob
    if optional and blob specified and blob not exists then
        DLOG("tcpseg: blob not found. skipped")
        return   -- без вердикта, тихий skip
    end
 
    -- 5. Выбор данных
    data = blob_or_def(blob) or reasm_data or dis.payload
 
    -- 6. Проверки: данные не пусты, направление OK, payload OK
    if #data > 0 and direction_check() and payload_check() then
 
        -- 7. Только первый replay
        if replay_first(desync) then
 
            -- 8. Разрешение ДИАПАЗОНА (ровно 2 маркера)
            pos = resolve_range(data, l7payload, pos_arg)
            -- pos = {start, end} или nil
 
            if pos then
                -- 9. Вырезать диапазон
                part = data:sub(pos[1], pos[2])
 
                -- 10. seqovl
                seqovl = 0
                if arg.seqovl and tonumber(arg.seqovl) > 0 then
                    seqovl = tonumber(arg.seqovl)
                    pat = "\x00"
                    if arg.seqovl_pattern then
                        if optional and not blob_exist(seqovl_pattern) then
                            -- используем нулевой паттерн
                        else
                            pat = blob(seqovl_pattern)
                        end
                    end
                    part = pattern(pat, 1, seqovl) .. part
                end
 
                -- 11. Отправка с автосегментацией
                rawsend_payload_segmented(desync, part, pos[1]-1-seqovl)
 
            else
                DLOG("tcpseg: range cannot be resolved")
            end
        else
            -- 12. Не первый replay -- ничего не делаем
            DLOG("tcpseg: not acting on further replay pieces")
        end
    end
 
    -- 13. ВЕРДИКТ НЕ ВЫНОСИТСЯ -- return без значения
end

Ключевые отличия от псевдокода multisplit:

  • Шаг 3: error() при отсутствии pos (в multisplit — дефолт "2")
  • Шаг 8: resolve_range вместо resolve_multi_pos (2 маркера, не список)
  • Шаг 9: data:sub(pos[1], pos[2]) — диапазон, не цикл по частям
  • Шаг 11: одна отправка (не цикл)
  • Шаг 13: нет return VERDICT_DROP / VERDICT_PASS — нет replay_drop_set(), нет nodrop

Проще говоря

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


Нюансы и подводные камни

1. Работает только с TCP

Если текущий пакет не TCP (UDP, ICMP и т.д.), tcpseg делает instance_cutoff_shim — отключает себя для этого потока навсегда. Исключение: связанные ICMP-пакеты (related icmp) не вызывают cutoff. Почему TCP-приёмы не переносятся на UDP, разобрано в нюансе multisplit, а сам instance_cutoff описан в стадии 1 жизненного цикла.

2. pos обязателен — нет дефолта

В отличие от multisplit (дефолт "2"), tcpseg без pos вызовет error(). Всегда указывайте pos. Это жёсткое требование: error() — ошибка Lua: nfqws2 при этом не падает, ядро пишет её в лог и прекращает обработку пакета, но профиль с таким инстансом фактически не работает (см. стадию 3 жизненного цикла).

3. Ровно 2 маркера — не больше и не меньше

resolve_range в C-коде проверяет ctm != 2 и вызывает luaL_error. Один маркер или три маркера — ошибка. Проще говоря: пишите ровно два маркера через запятую, например pos=0,-1 или pos=0,midsld.

4. Не выносит вердикт — нужен drop

Самая частая ошибка: забыть drop после tcpseg. Без drop оригинальный пакет уйдёт, и сервер получит данные дважды. Это может работать (TCP-стек дедуплицирует), но неоптимально и может путать DPI. Проще говоря: оригинал tcpseg не выбрасывает, потому что вердикта у него нет; выбрасывает drop. Подробно — в разделе Вердикт и комбинация с drop.

5. drop по умолчанию дропает ВСЕ payload

Функция drop по умолчанию работает с payload=all. Если tcpseg работает только с known, то drop без ограничений заблокирует и unknown payload. Здесь known — распознанные протоколы, unknown — данные, тип которых движок не распознал (подробнее о типах payload); фильтры описаны в разделе C) Standard payload. Решение:

--lua-desync=tcpseg:pos=0,-1:seqovl=5 --lua-desync=drop:payload=known

6. Маркер 0 — это начало, не “нулевая позиция Lua”

В resolve_range маркер 0 — это абсолютный маркер начала данных. Внутри C-кода pos[0]=0 соответствует первому байту. При возврате в Lua конвертируется в 1-based (pos[0]+1). Проще говоря: в командной строке 0 — первый байт данных, а пересчёт в нумерацию Lua с единицы функция делает сама.

7. Fooling применяется к реальному сегменту

В отличие от fakedsplit, где fooling идёт только на фейки, в tcpseg fooling модифицирует реальный отправляемый сегмент. Использование tcp_ack=-66000 приведёт к тому, что сервер отбросит сегмент. Безопасные fooling-опции: ip_id=rnd, tcp_ts_up, IPv6 extension headers. Fooling у соседей описан в разделах fakedsplit и fakeddisorder, а сами опции — в ts-and-fooling; параметры tcpseg — в разделе D) Standard fooling.

8. seqovl=10000 не вызовет ошибку

В отличие от nfqws1, где большие значения seqovl вызывали ошибку, nfqws2 автоматически сегментирует по MSS. Большой seqovl просто создаст много под-сегментов. Как они строятся, описано в разделе Автосегментация по MSS.

9. Нет nodrop — потому что нет drop

В tcpseg нет параметра nodrop, потому что функция и так никогда не дропает. Для управления вердиктом используйте отдельный инстанс drop. Проще говоря: nodrop нужен там, где функция сама выбрасывает оригинал и иногда этого не хочется; у tcpseg выбрасывать нечего. Как это выглядит у соседа — в нюансе 4 multisplit.

10. При неразрешённом диапазоне — тихий пропуск

Если оба маркера не разрешаются (например, pos=midsld,endhost для unknown payload), resolve_range вернёт nil, и tcpseg залогирует “range cannot be resolved” и ничего не сделает. Без ошибки, без вердикта. Проще говоря: если диапазон определить не удалось, ничего не отправляется, а ошибки в работе нет; о причине можно узнать из лога.

11. Повторная отправка (repeats) без drop — атака повторами

tcpseg с repeats=N без drop отправит N копий сегмента, а затем оригинальный пакет тоже уйдёт. Это осознанная стратегия для забивания буфера DPI (см. blockcheck2/15-misc.sh). Примеры повторов: 6, 7 и 10. Проще говоря: повторы вместе с оригиналом — задуманное поведение, а не ошибка.


Отличия от других функций сегментации

Пять функций ниже отправляют данные TCP-сегментами, но различаются числом маркеров, порядком отправки, наличием фейков и тем, как устроен seqovl. Подробности о соседях — в разделах multisplit, multidisorder, fakedsplit и fakeddisorder.

Аспектtcpsegmultisplitmultidisorderfakedsplitfakeddisorder
Количество маркеровРовно 2 (диапазон)Список (любое кол-во)Список (любое кол-во)ОднаОдна
Что отправляетсяЧасть payload (диапазон)Весь payload (разрезанный)Весь payload (разрезанный)Весь payloadВесь payload
Функция разрешения posresolve_rangeresolve_multi_posresolve_multi_posresolve_posresolve_pos
pos обязателенДа (error если нет)Нет (дефолт “2”)Нет (дефолт “2”)Нет (дефолт “2”)Нет (дефолт “2”)
ВердиктНет (не выносит)VERDICT_DROPVERDICT_DROPVERDICT_DROPVERDICT_DROP
nodropНет (не нужен)ДаДаДаДа
Порядок отправкиОдин сегментПрямой (1→2→3)Обратный (3→2→1)ПрямойОбратный
Фейковые сегментыНетНетНетДа (до 4 шт.)Да (до 4 шт.)
seqovl типТолько числоТолько числоМаркерТолько числоМаркер
Fooling кРеальному сегментуВсем сегментамВсем сегментамТолько к фейкамТолько к фейкам
ipfragДаДаДаНетНет
Нужен отдельный dropДаНет (встроен)Нет (встроен)Нет (встроен)Нет (встроен)

Проще говоря: multisplit, multidisorder, fakedsplit и fakeddisorder режут весь payload и сами выбрасывают оригинал, а tcpseg отправляет один выбранный кусок и оригинал не трогает, поэтому ему нужен отдельный drop.


Практические примеры

Каждый пример — готовая строка для профиля; под ней объяснено, что получится на выходе.

1. Минимальный seqovl без сегментации (TLS)

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,-1:seqovl=1 \
  --lua-desync=drop

Отправляет весь payload с 1 нулевым байтом seqovl слева. Drop блокирует оригинал. Проще говоря: pos=0,-1 — весь запрос, seqovl=1 — один нулевой байт слева, drop не даёт оригиналу уйти следом (раздел Вердикт и комбинация с drop).

2. seqovl с TLS-фейковым паттерном

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,-1:seqovl=5:seqovl_pattern=0x1603030000 \
  --lua-desync=drop

5-байтовый TLS record header как seqovl-prefix. DPI видит начало “нового” TLS record перед настоящим ClientHello. Паттерн задаёт seqovl_pattern, см. seqovl_pattern.

3. seqovl с предзагруженным фейком и tls_mod

--payload=tls_client_hello \
  --blob=seqovl_pat:@fake_tls.bin \
  --lua-init=seqovl_pat=tls_mod(seqovl_pat,'rnd') \
  --lua-desync=tcpseg:pos=0,-1:seqovl=#seqovl_pat:seqovl_pattern=seqovl_pat \
  --lua-desync=drop

Blob загружается из файла, рандомизируется через tls_mod, используется как seqovl_pattern с размером blob в качестве seqovl. Здесь tls_mod — набор изменений blob перед отправкой (он подстраивает ClientHello под текущее соединение; опции — в Опции tls_mod), --blob=seqovl_pat:@fake_tls.bin загружает blob из файла, а --lua-init готовит blob заранее.

4. Динамический случайный seqovl через luaexec

--lua-desync=luaexec:code='desync.rnd=brandom_az(math.random(5,10))' \
--lua-desync=tcpseg:pos=0,-1:seqovl=#rnd:seqovl_pattern=rnd \
--lua-desync=drop:payload=known

На каждый пакет генерируется случайная строка из букв a-z длиной 5-10 символов. Используется как seqovl_pattern, а её размер — как значение seqovl. Drop только для known payload. luaexec выполняет произвольный Lua-код (desync), а seqovl=#rnd берёт размер blob rnd.

5. seqovl с padencap и tls_mod (продвинутый)

--payload=tls_client_hello \
  --blob=seqovl_pat:@fake_tls.bin \
  --lua-desync=luaexec:code="desync.patmod=tls_mod(seqovl_pat,'rnd,dupsid,padencap',desync.reasm_data)" \
  --lua-desync=tcpseg:pos=0,-1:seqovl=#patmod:seqovl_pattern=patmod \
  --lua-desync=drop

Динамическая модификация фейка с учётом реального payload (через desync.reasm_data). Паттерн маскируется под TLS с padding, рандомизацией и дублированием session ID. padencap подкорректирует blob так, чтобы оригинальный payload стал частью padding extension — подробнее в разделе padencap — подробности; rnd и dupsid — другие опции tls_mod (Опции tls_mod).

6. Повторная отправка начала HTTP-запроса (без drop)

--payload=http_req \
  --lua-desync=tcpseg:pos=0,method+2:ip_id=rnd:repeats=20

Отправляет начало HTTP-запроса до маркера method+2 включительно — по коду это первые 3 байта, GET из GET / (почему не GE, объяснено в предупреждении к описанию аргумента pos) 20 раз со случайными IP ID. Оригинальный пакет тоже уходит (нет drop). Стратегия забивания буфера DPI. Здесь ip_id=rnd — случайные IP ID, а repeats=20 — 20 копий сегмента (параметры описаны в разделах E) Standard ipid и H) Standard rawsend).

7. Повторная отправка до midsld (TLS)

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=100

Отправляет начало ClientHello до середины SNI 100 раз. DPI может переполнить свой буфер и перестать анализировать поток.

8. Отправка только hostname

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=host,endhost

Вырезает и отправляет только hostname из SNI. Без drop и без seqovl — чисто для экспериментов или в составе сложной цепочки.

9. seqovl без сегментации + IP-фрагментация

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,-1:seqovl=5:ipfrag:ipfrag_disorder:ipfrag_pos_tcp=32 \
  --lua-desync=drop

Весь payload с seqovl отправляется, TCP-сегмент дополнительно фрагментируется на IP-уровне в обратном порядке. Параметры ipfrag описаны в разделе F) Standard ipfrag.

10. Только первый байт TLS record (повторы)

--payload=tls_client_hello \
  --lua-desync=tcpseg:pos=0,1:ip_id=rnd:repeats=260

Отправляет начало TLS record 260 раз. Несмотря на заголовок примера, по коду pos=0,1 захватывает первые два байта (см. предупреждение к описанию аргумента pos). Используется в blockcheck2 для тестирования misc-стратегий.

11. Комбинация: fake + tcpseg + drop (полная стратегия)

--payload=tls_client_hello \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
  --lua-desync=tcpseg:pos=0,-1:seqovl=5:seqovl_pattern=0x1603030000 \
  --lua-desync=drop
  1. fake отправляет фейковый TLS ClientHello с md5sig fooling
  2. tcpseg отправляет настоящий payload с seqovl
  3. drop блокирует оригинальный пакет

Здесь fake — отдельный пакет-обманка, который сервер отбрасывает из-за порчи заголовков (tcp_md5 — вариант fooling), а tls_mod подстраивает фейковый ClientHello. Инстансы выполняются в порядке записи: сначала fake, затем tcpseg, затем drop.

12. Защита от отсутствующего blob

--lua-desync=tcpseg:pos=0,-1:blob=maybe_missing:optional:seqovl=5

Если blob maybe_missing не существует — tcpseg тихо пропускает. Без ошибок. Флаг optional описан в разделе A) Собственные аргументы tcpseg.

13. HTTP: seqovl=1 + drop (минимальный рабочий пример)

--payload=http_req \
  --lua-desync=tcpseg:pos=0,-1:seqovl=1 \
  --lua-desync=drop

Один нулевой байт seqovl перед HTTP-запросом. Минимальная модификация, но может обмануть DPI, который ожидает HTTP-метод с первого байта TCP-потока.

14. wssize + tcpseg + drop (контроль TCP window)

--payload=tls_client_hello \
  --lua-desync=wssize:wsize=1:scale=6 \
  --lua-desync=tcpseg:pos=0,-1:seqovl=5:seqovl_pattern=fake_default_tls \
  --lua-desync=drop

Сначала wssize устанавливает маленький TCP window, затем tcpseg отправляет payload с seqovl, и drop блокирует оригинал. Здесь wssize подменяет размер TCP window — поле в заголовке TCP, которым получатель сообщает, сколько байт он готов принять (wsize=1:scale=6 — параметры окна); почему это крайняя мера, разобрано в отдельной заметке.


Варианты tcpseg без drop из набора стратегий GUI

Для каталога стратегий Zapret 2 GUI готовился набор «упрощённых» вариантов на tcpseg. Их идея: вместо того чтобы резать ClientHello на части, как multisplit, отправить его одним сегментом и спрятать перед ним чужие данные через seqovl. Набор полезен как иллюстрация того, как tcpseg ведёт себя без drop, но в его исходном описании были ошибки: он обещал «один пакет вместо двух» и меньшую нагрузку на сеть. Ниже варианты разобраны заново, с поправками по исходному коду lua/zapret-antidpi.lua.

Названия и состав могут отличаться от текущего GUI

Набор описан по черновику каталога стратегий. В каталоге GUI strategy_catalogs/winws2/tcp.txt этих вариантов на сентябрь 2026 нет, поэтому внутренние имена (tcpseg_226_simple и т.п.) и номера файлов tls_clienthello_*.bin проверяйте в своей версии программы. В черновике перед флагами стратегии стоял общий фильтр категории, который GUI подставляет сам, — ниже он опущен.

Общая часть всех вариантов

Каждый вариант строится из одних и тех же деталей. Сначала через --blob загружается готовый TLS ClientHello из файла программы, например --blob=bin_tls18:@bin\tls_clienthello_18.bin (что такое блоб — в blob). Затем идут фильтры: —payload=tls_client_hello ограничивает стратегию первым сообщением TLS, а —out-range=-d10 — первыми десятью пакетами с данными. После них вызывается сама функция:

--lua-desync=tcpseg:pos=0,-1:seqovl=226:seqovl_pattern=bin_tls18

Маркеры pos=0,-1 берут весь ClientHello целиком. seqovl=226 добавляет перед ним 226 байт из блоба bin_tls18 и сдвигает номер последовательности сегмента на 226 назад (механика — в разделе seqovl — скрытый фейк внутри сегмента). Сервер считает эти 226 байт уже пройденной частью потока и отбрасывает их, а DPI, который не учитывает перекрытие, видит в начале потока постороннее содержимое.

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

Главная поправка: оригинал тоже уходит

Во всех вариантах набора нет --lua-desync=drop. Как разобрано в нюансе «Не выносит вердикт — нужен drop», tcpseg не блокирует оригинальный пакет. Поэтому в сеть уходят два пакета: сегмент с seqovl и следом нетронутый оригинальный ClientHello с настоящим SNI. Утверждение «один пакет без резки» неверно, и экономии трафика по сравнению с multisplit нет: multisplit сам выносит VERDICT_DROP и отправляет только нарезанные части, а здесь к оригиналу добавляется ещё один полноразмерный сегмент.

Что это значит на практике, зависит от варианта:

  • без fooling сервер принимает данные из сегмента с seqovl, а оригинал отбрасывает как повтор. DPI видит оба пакета, и сработает ли обход, зависит от того, по какому из них DPI принимает решение;
  • с fooling (последние три строки таблицы ниже) сервер отбрасывает сам сегмент с seqovl, потому что fooling в tcpseg применяется к реальному сегменту (нюанс 7). Сегмент превращается в фейк, который видит только DPI, а соединение продолжается на оригинале. По смыслу это ближе к fake, чем к сегментации.

Оба поведения подтверждены на стенде (сентябрь 2026, nfqws2 и TLS-сервер на Linux): без fooling сервер принял данные из сегмента с seqovl, а оригинал пришёл повтором; с tcp_ack=-66000 или tcp_flags_unset=ack сервер сегмент отверг и принял оригинал. В обоих случаях соединение устанавливалось без задержки.

Чтобы получить классическую схему, где до сервера доходит только сегмент с seqovl, добавьте drop в конце, как в разделе «Быстрый старт»: --lua-desync=tcpseg:pos=0,-1:seqovl=226:seqovl_pattern=bin_tls18 --lua-desync=drop.

Таблица вариантов

ВариантЧто добавлено к общей частиФлаги (без загрузки блоба)
Простой seqovlПерекрытие 211, 226, 286 или 308 байт шаблонами tls_clienthello_5, _18, _11, _9 соответственно--lua-desync=tcpseg:pos=0,-1:seqovl=226:seqovl_pattern=bin_tls18
Дубль первого пакетаПеред tcpseg копии ClientHello на первом пакете с данными--out-range=-d1 --lua-desync=send:repeats=2 --out-range=-d10 --lua-desync=tcpseg:…
Дубль в первых трёх пакетахТо же, но окно считается по всем перехваченным пакетам--out-range=-n3 --lua-desync=send:repeats=2 --out-range=-d10 --lua-desync=tcpseg:…
Google SNI без файловШаблон собирается из встроенного fake_default_tls с SNI www.google.com--lua-init="tls_google = tls_mod(fake_default_tls,'sni=www.google.com')" и seqovl_pattern=tls_google
datanoackУ сегмента снят флаг ACK, сервер его отбросит…:seqovl_pattern=bin_tls18:tcp_flags_unset=ack
Фиксированный TTLСегмент живёт 5 хопов и не доходит до сервера…:seqovl_pattern=bin_tls18:ip_ttl=5:ip6_ttl=5
badseqНомер подтверждения сдвинут на −66000…:seqovl_pattern=bin_tls18:tcp_ack=-66000

Про строки с дублями. Функция send отправляет копию текущего пакета и сама его не блокирует, поэтому repeats=2 добавляет две копии поверх оригинала. Действует она только на ClientHello, потому что выше стоит --payload=tls_client_hello. В черновике вариант с -n3 описывался как «дублирование первых трёх пакетов», но это не так: n считает все перехваченные пакеты соединения, включая рукопожатие, а фильтр payload пропускает к send только ClientHello. Какой по счёту пакет окажется ClientHello, зависит от того, какие пакеты перехватывает WinDivert, — это разобрано в zapret2_start_cutoff. Формат диапазонов — в out-range.

Про строки с fooling. ip_ttl=5 жёстко задаёт время жизни: если сервер ближе пяти хопов, сегмент до него дойдёт и схема поведёт себя как вариант без fooling. tcp_ack=-66000 на Linux-серверах надёжно отбрасывается только вместе с tcp_ts_up. Подробнее обо всех обманках — в основные флаги и ts-and-fooling.

С чего начинать проверку

Разумный порядок — от простого к сложному: сначала вариант с Google SNI без дублей (ему не нужны файлы), затем тот же вариант с дублем, и только если оба не помогли — стратегии с резкой через multisplit или multidisorder. Проверять каждую ступень нужно новым соединением, а не обновлением вкладки: автоподбор и F5 легко показывают ложный успех, см. verify-strategy.


📚 См. также

  • desync — обзор всех функций --lua-desync и общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты
  • жизненный цикл desync-функции — восемь стадий, общих для всех техник дурения: отсев транспорта, выбор данных, replay, вердикты
  • структура desync и диссекта — устройство таблицы desync и диссекта пакета
  • multisplit — разрез всего payload на несколько сегментов с собственным вердиктом VERDICT_DROP; там же подробный разбор seqovl
  • multidisorder — тот же разрез, но сегменты уходят в обратном порядке; seqovl там работает иначе
  • fakedsplit · fakeddisorder — разрез по одной позиции с подмешиванием фейковых сегментов
  • hostfakesplit — разрез по границам имени хоста с подмешиванием фейкового имени
  • oob — сегментация с urgent-байтом вместо разреза
  • fake — отдельный фейковый пакет, с которым tcpseg комбинируют в полной стратегии
  • blob — как объявлять и передавать данные для аргументов blob и seqovl_pattern
  • payload — распознавание типов протоколов, от которого зависит работа относительных маркеров
  • ts-and-fooling — опции fooling и почему в tcpseg большинство из них вредны
  • verify-strategy — как честно проверить, работает ли подобранная комбинация
  • последовательность аргументов — как выстраивается цепочка инстансов в профиле
  • profile — где --lua-desync живёт среди остальных настроек
  • DPI и ТСПУ — как устроена инспекция трафика, против которой работает tcpseg

Источники: lua/zapret-antidpi.lua:1030-1076, nfq2/lua.c:3281-3328 (resolve_range), docs/manual.md:4248-4274, docs/readme.md:394-412, blockcheck2.d/standard/15-misc.sh, blockcheck2.d/standard/23-seqovl.sh из репозитория zapret2.


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

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