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.
Самая короткая команда — та, что на схеме: весь 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=droptcpseg пригождается и без 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. Все отличия от соседей сведены в таблицу раздела Отличия от других функций сегментации.
Оглавление
- Коротко и по-простому
- Справка: где функция в коде и родственники
- Зачем нужен tcpseg
- Быстрый старт
- Откуда берутся данные
- Маркеры позиций (pos) — диапазон из двух маркеров
- seqovl — скрытый фейк внутри сегмента
- Вердикт и комбинация с drop
- Полный список аргументов
- Порядок отправки
- Поведение при replay / reasm
- Автосегментация по MSS
- Место в общем скелете
- Псевдокод алгоритма
- Нюансы и подводные камни
- Отличия от других функций сегментации
- Практические примеры
- Варианты 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.
Основные сценарии использования:
- seqovl без сегментации. Отправить весь payload целиком с seqovl-префиксом, подменяющим начало потока для DPI. Маркеры
pos=0,-1задают диапазон “от начала до конца” — весь payload - Повторная отправка начала потока. Отправить первые N байт payload многократно (
repeats=N), чтобы забить буфер DPI мусором. Маркерыpos=0,method+2илиpos=0,midsldотправляют только начало - Модульная композиция. Поскольку
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 — начало и конец одного куска.
| Аспект | tcpseg | multisplit |
|---|---|---|
| Функция разрешения | 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_methodeol | http_req |
host | Первый байт имени хоста (Host: в HTTP, SNI в TLS) | http_req, tls_client_hello |
endhost | Байт, следующий за последним байтом имени хоста. Т.е. host..endhost-1 = полный hostname | http_req, tls_client_hello |
sld | Первый байт домена второго уровня (SLD). Для www.example.com — это e в example | http_req, tls_client_hello |
endsld | Байт, следующий за последним байтом SLD. Для example.com — это . после example | http_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 extensions | tls_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. Это означает:
- После выполнения
tcpsegоригинальный пакет не блокируется - Следующие инстансы в цепочке продолжают обработку
- Если после
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-сегмент (в Luasubберёт кусок строки от первого номера до второго). Подробно о маркерах — в разделе Маркеры позиций (pos) — диапазон из двух маркеров - Примеры:
pos=0,-1— весь payload (от начала до конца)pos=0,midsld— от начала до середины SLDpos=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, а TCPth_seq(поле номера последовательности в TCP-заголовке) уменьшается на N. Сервер отбросит левую часть, DPI — может не отбросить. Механика — в разделе seqovl — скрытый фейк внутри сегмента - Примеры:
seqovl=1— 1 байт фейка слева (минимальный)seqovl=5— 5 байт фейка слеваseqovl=#rnd— размер blobrnd(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— динамически сгенерированный blobseqovl_pattern=fake_default_tls— стандартный TLS-фейк как паттерн
blob
- Формат:
blob=<blobName> - Тип: имя blob-переменной
- По умолчанию: не задан
- Описание: Заменить текущий payload/reasm на указанный blob и отправить из него диапазон pos. Используется для отправки произвольных данных. Что такое blob и как его объявить — в blob
- Примеры:
blob=fake_default_tls— стандартный TLS-фейкblob=0xDEADBEEF— inline hexblob=my_custom_data— предзагруженный blob
optional
- Формат:
optional(флаг, без значения) - Описание: Мягкий режим:
- Если задан
blob=...и blob отсутствует — tcpseg ничего не делает (тихий skip, без ошибок) - Если задан
seqovl_pattern=...и blob отсутствует — используется нулевой паттерн (seqovl не отменяется)
- Если задан
- Использование: защита от ошибок при использовании blob, которые могут отсутствовать (например, если blob генерируется другой функцией). Та же проверка описана в стадии 3 жизненного цикла
B) Standard direction
| Параметр | Значения | По умолчанию |
|---|---|---|
dir | in, out, any | out |
Фильтр по направлению пакета. 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, включаяunknownpayload=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 TTL | ip_ttl=6 |
ip6_ttl=N | Установить IPv6 Hop Limit | ip6_ttl=6 |
ip_autottl=delta,min-max | Автоматический TTL (delta от серверного TTL) | ip_autottl=-2,40-64 |
ip6_autottl=delta,min-max | Аналогично для IPv6 | ip6_autottl=-2,40-64 |
ip6_hopbyhop[=HEX] | Вставить extension header hop-by-hop (по умолчанию 6 нулей) | ip6_hopbyhop |
ip6_hopbyhop2[=HEX] | Второй hop-by-hop header | ip6_hopbyhop2 |
ip6_destopt[=HEX] | Destination options header | ip6_destopt |
ip6_destopt2[=HEX] | Второй destination options | ip6_destopt2 |
ip6_routing[=HEX] | Routing header | ip6_routing |
ip6_ah[=HEX] | Authentication header | ip6_ah |
tcp_seq=N | Сместить TCP sequence (+ или -) | tcp_seq=-10000 |
tcp_ack=N | Сместить TCP ack (+ или -) | tcp_ack=-66000 |
tcp_ts=N | Сместить TCP timestamp | tcp_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-функция fooling | fool=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 ID | seq |
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 бесполезно — он только TCP | 8 |
ipfrag_next=N | IPv6: 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=N | Firewall 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 автоматически:
- Отслеживает MSS для каждого TCP-соединения
- Если отправляемая часть (включая seqovl) превышает MSS — дополнительно режет по MSS
- Каждый под-сегмент отправляется с корректным 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=known6. Маркер 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.
| Аспект | tcpseg | multisplit | multidisorder | fakedsplit | fakeddisorder |
|---|---|---|---|---|---|
| Количество маркеров | Ровно 2 (диапазон) | Список (любое кол-во) | Список (любое кол-во) | Одна | Одна |
| Что отправляется | Часть payload (диапазон) | Весь payload (разрезанный) | Весь payload (разрезанный) | Весь payload | Весь payload |
| Функция разрешения pos | resolve_range | resolve_multi_pos | resolve_multi_pos | resolve_pos | resolve_pos |
| pos обязателен | Да (error если нет) | Нет (дефолт “2”) | Нет (дефолт “2”) | Нет (дефолт “2”) | Нет (дефолт “2”) |
| Вердикт | Нет (не выносит) | VERDICT_DROP | VERDICT_DROP | VERDICT_DROP | VERDICT_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=drop5-байтовый 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=dropBlob загружается из файла, рандомизируется через 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=dropfakeотправляет фейковый TLS ClientHello с md5sig foolingtcpsegотправляет настоящий payload с seqovldropблокирует оригинальный пакет
Здесь 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-архивом.