todo/Zapret2/desync/oob.md
loop-uh baf0091fa3
Some checks failed
Published content check / validate (push) Failing after 17s
git.zapret.moe → git2.zapret.moe во всём хранилище, защита от возврата старых имён
git.zapret.moe в России заблокирован по имени, как и wiki.zapret.moe. Ссылки
на Forgejo во всех заметках, футерах, wizard.html и главной заменены на
git2.zapret.moe (включая закодированные адреса значков shields.io).

Какое имя рабочее, теперь решают реестры хоста /etc/zapret-*-domains.conf через
zapret-site-names: pre-commit хук vault отклоняет добавленные строки с
заблокированным именем, сборка Quartz сама переводит ссылки и baseUrl на
рабочие имена. Правило в AGENTS.md переписано под этот механизм.

scripts/wiki-stats.py: переходы с любых имён вики и git (wikiN, gitN) больше
не считаются внешними сайтами. content-check: проверка зеркал сторонних
проектов узнаёт любое имя git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:51:32 -04:00

91 KiB
Raw Permalink Blame History

date tags aliases description
2026-02-17
zapret
zapret2
nfqws2
lua
lua-desync
antidpi
tcp
oob
urgent
oob
Техника oob в zapret2: вставка TCP Out-of-Band байта с флагом URG ломает сигнатуры DPI. Режимы urp, полный список аргументов, подводные камни, примеры.

[!mirror] Резервное зеркало Актуальная версия этой страницы — на основной вики: wiki2.zapret.moe/Zapret2/desync/oob

oob -- TCP Out-of-Band десинхронизация (zapret2 / nfqws2)

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

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

HTTP-запросы и TLS-соединения идут по протоколу TCP. Соединение начинается с рукопожатия: первым пакетом клиент отправляет SYN («хочу соединиться») и называет в нём стартовый номер — sequence number, коротко seq. Дальше у каждого байта соединения есть порядковый номер, а сервер раскладывает принятые данные в свой буфер строго по этим номерам. Стартовый номер из SYN он запоминает и ждёт первый байт данных сразу за ним: если клиент начал с номера 999, то данные ожидаются с 1000 (999 плюс единица за сам SYN). Как с такими номерами считать в Lua (они 32-битные беззнаковые и заворачиваются по кругу) — в Zapret2/структура desync и диссекта#Работа с sequence numbers.

Кроме обычных данных, в TCP есть редко используемый механизм срочных данных, по-английски Out-of-Band, сокращённо OOB («вне основного потока»). Отправитель ставит в заголовке пакета флаг URG («urgent», «срочно») и записывает в поле th_urp (urgent pointer, «указатель срочности») положение срочного байта. TCP-стек операционной системы сервера — та её часть, что принимает пакеты и собирает из них поток для программ, — этот байт из потока выбрасывает, и программа получает чистые данные без вставки. DPI/DPI устроен иначе: он может не понимать механизм срочных данных и разбирать поток вместе с лишним байтом. Как именно два стандарта указывают на срочный байт, разобрано в разделе #Два стандарта th_urp.

Функция oob в Zapret2/Zapret2 (движок nfqws2) использует этот механизм, чтобы подложить DPI запрос с посторонним байтом внутри. Она делает четыре шага:

  1. Ловит соединение с самого первого пакета — SYN — и уменьшает в нём стартовый номер seq на 1; то же делает в первом ACK («подтверждаю», пакете-подтверждении) — следующем исходящем пакете после SYN (по коду это пустой ACK или уже пакет с данными). Так в нумерации байтов освобождается место под один лишний байт.
  2. Во входящих пакетах начала соединения (прежде всего в SYN-ACK — ответе сервера на SYN) увеличивает номер подтверждения ack на 1, чтобы операционная система клиента не запуталась: она ждёт подтверждений по своей исходной нумерации. Если входящий пакет оказывается на неожиданной позиции (по коду — дальше начала соединения, позиция больше 1), oob отключается.
  3. Когда уходит первый пакет с данными (например, ClientHello или HTTP-запрос), вставляет в него один лишний байт — по умолчанию 0x00 — в место, которое задаёт аргумент urp, ставит флаг URG и заполняет указатель th_urp. Готовый пакет отправляется вместо перехваченного оригинала, а оригинал выбрасывается.
  4. Отключается для этого соединения. Такое отключение называют cutoff («отсечка»): движок больше не вызывает функцию на пакетах этого соединения, и они проходят без изменений.

Соединение нужно ловить именно с SYN, потому что сдвиг нумерации начинается там, а отказаться от него по дороге нельзя: если oob не вставит байт, нумерации клиента и сервера разойдутся и соединение сломается. Поэтому функция обязана видеть рукопожатие с первого пакета; если первым она увидела не SYN, она отключается. Проще говоря: в самом начале клиент сообщает серверу «мои номера начинаются на единицу раньше», в освободившееся место встаёт лишний байт, а сервер выбрасывает его как срочный. Подробно — в разделе #Принцип работы и в нюансах #9. Соединение должно быть перехвачено с SYN и #7. Длящаяся десинхронизация и переключение профилей.

Сервер получает поток, из которого срочный байт вынут, и отвечает как обычно: приложение видит ровно тот запрос, который отправил браузер. DPI, который не вынимает срочные байты, читает поток с лишним байтом посередине (например, exa\x00mple.com вместо example.com) и не находит имя сайта. Всё держится на слове «может»: приём рассчитан на DPI, который срочный байт не вынимает; на другом DPI он может не сработать. Чем oob отличается от простого разреза на куски, разобрано в разделе #Зачем нужен oob.

Команда из быстрого старта --in-range=-s1 --lua-desync=oob:urp=midsld сначала уменьшает seq в SYN на единицу, резервируя место под будущий байт, а затем вставляет в первый пакет с данными посередине имени сайта байт срочных данных (Out-of-Band) с флагом URG; на схеме он обведён. ТСПУ, который не вынимает такой байт из потока, видит имя с лишним байтом внутри; TCP-стек сервера этот байт вынимает, и имя в буфере смыкается. Как читать схему: строки — пакеты в порядке отправки, подписанные в тех же обозначениях, что и таблицы в этой статье; по горизонтали — место данных в TCP-потоке (номер последовательности, seq); в колонке справа — что сделал с пакетом сервер; внизу — буфер сервера, в который принятые куски встают по своим seq.

!desync-anim-oob.svg

Самая короткая команда включает oob со значениями по умолчанию: лишний байт 0x00 ставится перед самым первым байтом данных (urp=b). Строку пишут в Zapret2/profile#Анатомия профиля — набор правил Zapret 2 для определённого трафика. Для HTTP и TLS флаг --in-range=-s1 обязателен: он задаёт, какие входящие пакеты вообще доходят до функции (формат разобран в заметке Zapret2/out-range), и без него oob не увидит рукопожатие и не включится; подробнее — в нюансе .

--in-range=-s1 --lua-desync=oob

Режим urp=b работает только на Linux-серверах: Windows и BSD с нулевым th_urp ломаются (нюанс 3). Самый популярный вариант — urp=midsld, как на схеме: байт ставится посередине домена второго уровня (SLD, главной части имени сайта: у www.youtube.com это youtube); все режимы и маркеры разобраны в разделе #Режимы urp, готовые строки — в #Быстрый старт и #Практические примеры.

oob не режет данные на куски, как Zapret2/desync/multisplit#Зачем нужен multisplit, и не посылает отдельных фейковых пакетов, как Zapret2/desync/fake#Зачем нужен fake (фейк — пакет-обманка с ложными данными, который нарочно портят приёмом fooling, чтобы сервер его отбросил, а DPI прочитал; см. Zapret2/ts-and-fooling#Что такое fooling): лишний байт вставляется внутрь единого настоящего потока. Поэтому oob нельзя сочетать в одном потоке с multisplit, fakedsplit и другими функциями, которые пересылают данные, — они отправят копии без срочного байта и без учёта сдвига (#6. Несовместимость с multisplit, fakedsplit и другими функциями отправки payload). Соседи по приёму: Zapret2/desync/syndata#Зачем нужен syndata кладёт данные прямо в SYN, а Zapret2/desync/tcpseg#Зачем нужен tcpseg отправляет диапазон данных между двумя маркерами. Есть и другие ограничения. У oob нет фильтров dir и payload (#4. Нет фильтрации по payload), поэтому отбирать соединения приходится на уровне профиля. Ей нужен conntrack — отслеживание состояния соединений в системе (#8. Требуется conntrack (tracking)). Режим urp=e как правило бесполезен для обхода DPI (почему). Сдвиг seq на SYN, отключение при подключении «на ходу» и переключение профилей — в нюансах 7 и 9. Подходящую позицию urp подбирают перебором и проверяют по Zapret2/verify-strategy#Короткий чек-лист честной проверки; пример перебора — #11. Перебор позиций urp (как в blockcheck2).


Оглавление


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

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

Файл: lua/zapret-antidpi.lua:1097 nfqws1 эквивалент: отсутствует (новая функция zapret2) tpws аналог: --split-pos=.. --oob (близкий, но не идентичный механизм) Сигнатура: function oob(ctx, desync)

oob -- функция TCP-десинхронизации через механизм Out-of-Band (Urgent) данных. Она перехватывает TCP handshake с самого начала (SYN), сдвигает sequence на 1 байт влево, а затем вставляет 1 байт OOB (помеченный флагом TH_URG) в первый исходящий payload. TCP-стек получателя выбрасывает OOB-байт из потока, но DPI может этого не делать -- и тогда DPI видит payload со вставленным посторонним байтом, что ломает распознавание сигнатур.

После отработки функция уходит в instance_cutoff по обоим направлениям.

[!note] Проще говоря Out-of-Band (OOB, «вне основного потока») — срочные данные TCP; флаг TH_URG — отметка «в этом пакете есть срочный байт», а th_urp — поле-указатель на него. SYN — первый пакет соединения, с которого oob обязана начать работу; sequence — порядковый номер байта в потоке. instance_cutoff — команда движку «больше не вызывай эту функцию на пакетах данного соединения»; как она устроена, описано в Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff. Поля th_flags и th_urp — части диссекта, то есть пакета, разобранного на поля, — описаны в Zapret2/структура desync и диссекта. Чем oob отличается от tpws --oob, разобрано в разделе #Сравнение с tpws --oob.

Родственные функции: multisplit (TCP-сегментация), fakedsplit (с фейками), fake (фейковые пакеты), syndata (данные в SYN), tcpseg (диапазон).

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


Зачем нужен oob

DPI/DPI анализирует TCP-поток, ищет сигнатуры — заранее известные строки, по которым узнаётся сайт (hostname в HTTP, SNI в TLS). Если вставить в поток лишний байт, помеченный как "urgent" (Out-of-Band), происходит следующее:

  1. Сервер (TCP-стек ОС): выбрасывает OOB-байт из потока. Приложение получает чистые данные без вставки
  2. DPI: может не понимать OOB-механизм и анализировать поток вместе со вставленным байтом. Если байт вставлен посередине hostname/SNI, сигнатура разрушается

Ключевое отличие от сегментации: oob не разрезает данные на части -- он вставляет лишний байт, который принимающая ОС удаляет, а DPI -- нет.

Ключевое отличие от фейков: oob не отправляет отдельных фейковых пакетов -- вставка происходит внутри единого легитимного потока. Это делает oob труднодетектируемым.

[!note] Проще говоря Обычно приём против DPI либо режет запрос на части (сегменты), либо посылает отдельные фейковые пакеты. oob делает третье: запрос остаётся целым, но в него добавляется один байт с пометкой «срочный». Сервер выбрасывает этот байт из потока, а DPI, если не знает такой пометки, читает его как часть имени сайта: exa\x00mple.com вместо example.com.

Соседние приёмы разобраны в Zapret2/desync/multisplit#Зачем нужен multisplit (разрез на сегменты), Zapret2/desync/fake#Зачем нужен fake и Zapret2/ts-and-fooling#Что такое fooling (фейковые пакеты и их порча). Что такое --lua-desync и как движок вызывает такие функции — в Zapret2/desync.


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

Три готовые строки для Zapret2/profile#Анатомия профиля Zapret 2, от самой короткой к более гибким. Во всех есть --in-range=-s1; зачем он нужен, сказано сразу под примерами. Значения urp, char и byte разобраны в разделах #Режимы urp и #A) Собственные аргументы oob.

Минимально (urp=b по умолчанию, OOB-байт = 0x00):

--in-range=-s1 --lua-desync=oob

С маркером по середине SLD:

--in-range=-s1 --lua-desync=oob:urp=midsld

С конкретным OOB-символом:

--in-range=-s1 --lua-desync=oob:char=X

Важно: --in-range=-s1 обязателен для HTTP/TLS (протоколов, где сервер ждёт запроса клиента). Без него oob не увидит SYN-пакет и не активируется.

[!note] Проще говоря --in-range задаёт, какие входящие пакеты движок передаёт функциям, а запись -s1 читается как «от начала соединения до seq 1» (по Zapret2/out-range: s — относительный sequence number, разделитель - включает конечную позицию). Так до oob доходят пакеты самого начала соединения, по статье — в их числе входящий SYN-ACK от сервера; без этого функция не видит рукопожатие и не включается. HTTP и TLS — протоколы, в которых сервер ждёт запроса клиента; протоколы, где сервер заговаривает первым (SMTP, FTP, SSH), описаны в нюансе #2. Для server-first протоколов нужен --wf-tcp-in (Windows).


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

Работа идёт в три фазы, а параллельно с ними — обработка входящих пакетов: #Фаза 1: перехват SYN со сдвигом seq, #Фаза 2: вставка OOB-байта срочного байта в первый пакет с данными, #Фаза 3: cutoff и #Обработка входящих пакетов в пакетах от сервера. Как эти шаги вписаны в общую схему всех функций, показано в разделе #Место в общем скелете.

Фаза 1: перехват SYN

oob должен начать работу с самого первого пакета TCP-соединения -- SYN. Функция проверяет флаг TH_SYN и запоминает, что соединение перехвачено с начала.

На этапе SYN (pos=0) и первого ACK (pos=1) функция уменьшает sequence на 1:

Оригинальный SYN:
  th_seq = 1000

После oob:
  th_seq = 999    (уменьшен на 1)

Это "резервирует" место для будущего OOB-байта. Сервер запоминает начальный sequence = 999, и ожидает данные начиная с позиции 1000 (999 + 1 за SYN).

[!note] Проще говоря Пометка pos — относительная позиция пакета в потоке по seq: pos=0 у SYN, pos=1 у первого пакета после него (по коду — пустой ACK или первый пакет с данными); в псевдокоде ниже её возвращает pos_get. Для этих двух позиций oob вычитает из seq единицу. Сервер поэтому считает стартовым номер 999 и ждёт данные с 1000, а сдвиг на единицу резервирует место под один лишний байт. Сдвиг, однажды начатый, должен дойти до конца: почему «соскочить» нельзя, объяснено в нюансе #4. Нет фильтрации по payload. Как с такими номерами считать в Lua (они 32-битные беззнаковые и заворачиваются по кругу) — Zapret2/структура desync и диссекта#Работа с sequence numbers.

Фаза 2: вставка OOB-байта

Когда приходит первый исходящий пакет с данными (pos >= 1, payload непуст), oob:

  1. Берёт payload (или reasm, если многопакетный)
  2. Определяет OOB-байт: char= или byte= или 0x00 по умолчанию
  3. Определяет позицию вставки по urp
  4. Вставляет OOB-байт в payload по этой позиции
  5. Устанавливает флаг TH_URG и значение th_urp
  6. Отправляет модифицированный пакет через rawsend_dissect_segmented
  7. Дропает оригинальный пакет (VERDICT_DROP)

Payload — полезные данные пакета без сетевых заголовков; reasm — те же данные, собранные движком из нескольких пакетов, если запрос в один пакет не влез (Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload). rawsend_dissect_segmented отправляет пакет, разобранный на поля (диссект), и сама разбивает его на сегменты по MSS — наибольшему объёму данных в одном TCP-сегменте (#Поведение при reasm (многопакетный payload)); среди способов выпустить пакет она описана в разделе Zapret2/жизненный цикл desync-функции#Четыре способа выпустить пакет. VERDICT_DROP — решение «перехваченный пакет не отправлять» (Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация).

Визуализация вставки (urp=b, th_urp=0):

!wiki-anim-z2-oob-bytes.svg

Квадратные скобки — по одному байту; OOB — вставленный срочный байт, ^ указывает на него, а th_urp=0 и TH_URG=1 — значения полей заголовка. DPI, не вынимающий срочный байт, видит 0x00 перед GET: сигнатура сдвинута и сломана. Режим b подробнее — в разделе .

Визуализация вставки (urp=midsld, TLS SNI):

Payload (TLS ClientHello), SNI = "example.com", midsld указывает на "m" в "example":

Оригинал:
  ...[e][x][a][m][p][l][e][.][c][o][m]...

После вставки OOB (0x00) в позицию midsld:
  ...[e][x][a][OOB][m][p][l][e][.][c][o][m]...
              ^
              th_urp = (позиция midsld + 1 по RFC)

DPI видит: "exa\x00mple.com" -- нет совпадения с "example.com"
Сервер видит: "example.com" -- OOB-байт удалён

Здесь midsld — маркер «середина домена второго уровня» (у example.com домен второго уровня — example), а «по RFC» означает стандарт, по которому th_urp указывает на байт, следующий за срочным (#Два стандарта th_urp). Маркеры разобраны в Zapret2/desync/multisplit#Типы маркеров.

Фаза 3: cutoff

После успешной отправки OOB-пакета (если это не replay) функция выполняет instance_cutoff_shim -- отключает себя от обоих направлений данного соединения. Дальнейшие пакеты проходят без модификации.

При replay (многопакетный payload): все replay-части дропаются, cutoff выполняется после последней части.

[!note] Проще говоря Cutoff — отключение функции для этого соединения: instance_cutoff_shim говорит движку не вызывать её на следующих пакетах, и они проходят как есть (Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff). Replay («перепроигрывание») — это когда движок придержал части большого запроса, собрал их и прогоняет по очереди через функции: oob при этом дропает каждую придержанную часть, а отключается только на последней (Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)).

Обработка входящих пакетов

Для входящих пакетов (от сервера) oob увеличивает th_ack на 1, компенсируя сдвиг sequence:

Входящий от сервера:
  th_ack = 999   (сервер подтверждает sequence, сдвинутый на 1)

oob модифицирует:
  th_ack = 1000  (возвращает ожидаемое значение для клиента)

Это необходимо, чтобы клиентская ОС не запуталась -- она ожидает ack на основе своего оригинального sequence.

[!note] Проще говоря ack — номер, которым получатель подтверждает, до какого байта принял данные. Сервер считает от сдвинутого номера, а клиентская система — от своего исходного, поэтому oob во входящих пакетах начала соединения (прежде всего в SYN-ACK) прибавляет к ack единицу, и подтверждения совпадают с тем, что клиент ожидает; если входящий пакет оказывается на неожиданной позиции (по коду — дальше начала соединения), она отключается (cutoff). Это «входящая ветка» функции: она только правит пакет и отпускает его дальше (Zapret2/жизненный цикл desync-функции#Три типа функций).


Два стандарта th_urp

RFC (Request for Comments) — документы, в которых описаны стандарты интернета. Поле th_urp в TCP-заголовке — указатель срочности: он подсказывает получателю, где в данных срочный байт. Два документа понимают его по-разному, и это расхождение определяет выбор режима urp.

Существуют два RFC-интерпретации поля th_urp (Urgent Pointer) в TCP-заголовке:

Стандарт th_urp указывает на Поддержка
RFC 793 (оригинальный) Сам OOB-байт (0-based) Старые реализации
RFC 1122 (уточнённый) Байт, следующий за OOB (1-based) Современные ОС (Linux, Windows, BSD)

Из-за этого расхождения значение th_urp=0 невалидно по одному из стандартов, но может работать на практике.

В коде oob:

  • Для urp=b: th_urp устанавливается в 0 (согласно RFC 793)
  • Для urp=e и маркеров: th_urp устанавливается как позиция + 1 (согласно RFC 1122)

[!note] Проще говоря Старый стандарт указывает на сам срочный байт и считает от нуля, новый — на байт сразу за ним. Ноль в th_urp невалиден по одному из стандартов, но может работать на практике: Linux его принимает, а Windows и BSD ломаются, поэтому режим b с его нулём годится только для Linux-серверов (нюанс 3). Для e и маркеров oob пишет позицию плюс единица.


Режимы urp

Аргумент urp отвечает на вопрос «в какое место данных вставить лишний байт»: b — в самое начало, e — в самый конец, маркер — в произвольную позицию (Zapret2/desync/multisplit#Маркеры позиций (pos)). От выбора зависит и значение th_urp (#Два стандарта th_urp).

urp=b -- начало (по умолчанию)

OOB-байт вставляется перед первым байтом payload. th_urp = 0.

th_urp = 0
Payload: [OOB][оригинальные данные...]

Ограничение: работает только на Linux-серверах. Windows и BSD серверы ломаются при th_urp=0, поскольку интерпретируют его как невалидный по RFC 1122.

Это наиболее эффективный режим для обхода DPI, т.к. OOB-байт вставляется в самое начало, ломая распознавание протокола (HTTP-метод, TLS record header).

[!note] Проще говоря HTTP-метод — слово в начале запроса (GET, POST), а TLS record header — заголовок записи, с которого начинается ClientHello. Лишний байт перед ними сдвигает всё начало, и DPI, не вынимающий срочный байт, может не узнать протокол. Цена — только Linux-серверы (th_urp=0 невалиден по RFC 1122; нюанс 3); для не-Linux серверов статья называет более безопасным urp=0 (пример 2).

urp=e -- конец

OOB-байт вставляется после последнего байта payload. th_urp = len(payload) + 1.

th_urp = len+1
Payload: [оригинальные данные...][OOB]

Как правило бесполезен для обхода DPI: DPI получает весь оригинальный payload целиком, OOB-байт идёт после него и не мешает анализу.

urp=маркер -- произвольная позиция

urp может быть любым Zapret2/desync, поддерживаемым функцией resolve_pos. OOB-байт вставляется в позицию, куда разрешается маркер. th_urp = позиция + 1 (по RFC 1122).

Маркер — это не число, а имя места в запросе: программа сама находит, где в данном ClientHello или HTTP-запросе начинается имя хоста, где оно кончается, где середина домена второго уровня, и подставляет позицию в байтах (resolve_pos; Zapret2/desync/multisplit#Как маркеры разрешаются в коде). Домен второго уровня (SLD) — главная часть имени сайта: у www.youtube.com это youtube. Расширение SNI (sniext) — место в ClientHello, где лежит имя сервера. Таблица ниже перечисляет маркеры и типы данных, для которых они определены; сами типы (http_req, tls_client_hello) разобраны в заметке payload.

Поддерживаемые маркеры (те же, что в multisplit):

Маркер Описание Для каких payload
host Первый байт имени хоста http_req, tls_client_hello
endhost Байт после последнего байта hostname http_req, tls_client_hello
sld Первый байт домена второго уровня http_req, tls_client_hello
endsld Байт после SLD http_req, tls_client_hello
midsld Середина SLD http_req, tls_client_hello
sniext Начало данных SNI extension tls_client_hello
method Начало HTTP-метода http_req
Числа Абсолютная позиция (1-based) Любой

Арифметика маркеров поддерживается: midsld+1, host-1, sniext+2 и т.д.

Проще говоря: midsld+1 — «на байт правее середины SLD», host-1 — «на байт левее начала имени хоста». Подробнее — в разделах Zapret2/desync/multisplit#Арифметика маркеров и Zapret2/desync/multisplit#Важные нюансы pos.

Пример: urp=midsld -- OOB-байт вставляется посередине SLD. DPI видит домен с лишним байтом посередине.

Если маркер не разрешается (например, midsld для unknown payload), функция логирует ошибку и выполняет instance_cutoff.

[!note] Проще говоря «Не разрешается» значит, что в данных нет места, на которое указывает маркер: у пакета неизвестного протокола нет имени хоста, а значит нет и его середины. Тогда функция пишет ошибку в лог и отключается (cutoff — см. #Фаза 3: cutoff).


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

Формат вызова:

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

Важно: у oob нет фильтров dir и payload. Функция не может быть отфильтрована по payload, поскольку после начала модификации TCP handshake (сдвиг sequence) соскок невозможен -- иначе поедут sequence numbers. Направление обрабатывается автоматически: исходящие -- вставка OOB, входящие -- коррекция ack.

[!note] Проще говоря Фильтр dir («в какую сторону») и фильтр payload («для какого типа данных») в других функциях описаны в Zapret2/desync/multisplit#B) Standard direction и Zapret2/desync/multisplit#C) Standard payload у multisplit. У oob их нет, потому что после сдвига seq на SYN она обязана довести соединение до конца. Направление она определяет сама: исходящие — вставка байта, входящие — правка ack. Отбирать соединения приходится профилем (Zapret2/filter), см. #4. Нет фильтрации по payload.

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

Три аргумента: char и byte задают сам лишний байт, urp — место, куда он вставляется. Если не задать ничего, в начало данных вставляется 0x00.

char

  • Формат: char=<символ>
  • Тип: строка длиной 1 байт
  • По умолчанию: 0x00
  • Описание: Символ, используемый как OOB-байт. Должен быть ровно 1 байт
  • Приоритет: char проверяется первым, затем byte, затем 0x00
  • Примеры:
    • char=X -- OOB-байт = ASCII 'X'
    • char=A -- OOB-байт = ASCII 'A'

byte

  • Формат: byte=<число 0..255>
  • Тип: числовое значение байта
  • По умолчанию: не задан
  • Описание: Числовое значение OOB-байта. Используется, если char не задан
  • Примеры:
    • byte=0 -- OOB-байт = 0x00
    • byte=255 -- OOB-байт = 0xFF
    • byte=65 -- OOB-байт = 0x41 (ASCII 'A')

urp

  • Формат: urp=<маркер> или urp=b или urp=e
  • Тип: строка -- специальное значение (b, e) или Zapret2/desync
  • По умолчанию: b
  • Описание: Позиция, куда будет вставлен OOB-байт
    • b -- начало payload (th_urp=0). Только для Linux-серверов
    • e -- конец payload (th_urp=len+1). Бесполезно для обхода DPI
    • маркер -- произвольная позиция. th_urp = позиция + 1 (RFC 1122)
  • Примеры:
    • urp=b -- перед первым байтом (дефолт)
    • urp=e -- после последнего байта
    • urp=midsld -- середина SLD
    • urp=host -- начало hostname
    • urp=2 -- после первого байта payload
    • urp=sniext+1 -- один байт после начала SNI extension data

B) Standard fooling

Модификации L3/L4 заголовков. Применяются к отправляемому OOB-пакету.

Fooling («портить») — приём, которым обычно портят фейковый пакет, чтобы сервер его отбросил, а DPI прочитал (Zapret2/ts-and-fooling#Что такое fooling). L3 — сетевой уровень (заголовок IP), L4 — транспортный (заголовок TCP). Тот же набор параметров описан в Zapret2/desync/multisplit#D) Standard fooling у multisplit.

Параметр Описание Пример
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 в oob имеет ограниченный смысл. В отличие от fakedsplit, где fooling применяется к фейковым пакетам (чтобы сервер их отбросил), в oob fooling применяется к реальному OOB-пакету. Если сервер отбросит этот пакет из-за fooling -- данные не дойдут. Используйте fooling осторожно (например, tcp_ts_up, IPv6 extension headers).

[!note] Проще говоря В других техниках порча безопасна, потому что портят фейк, а настоящие данные идут отдельно. В oob портить можно только единственный настоящий пакет с данными: если сервер его отбросит, запрос до него просто не дойдёт.


C) Standard ipid

IP ID — числовой идентификатор пакета в IP-заголовке; аргументы решают, как его нумеровать. Тот же набор описан в Zapret2/desync/multisplit#E) Standard ipid у multisplit.

Параметр Описание По умолчанию
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).


D) Standard ipfrag

IP-фрагментация. Каждый отправляемый TCP-сегмент дополнительно фрагментируется на уровне IP.

IP-фрагментация — деление одного пакета на несколько на уровне IP, в отличие от сегментации TCP. Тот же набор описан в Zapret2/desync/multisplit#F) Standard ipfrag у multisplit.

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

E) Standard reconstruct

Reconstruct — сборка сырого пакета из полей перед отправкой. Тот же набор описан в Zapret2/desync/multisplit#G) Standard reconstruct у multisplit.

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

Предупреждение: badsum в oob сделает OOB-пакет невалидным -- сервер его отбросит, OOB-байт не будет вставлен, а sequence останутся сдвинутыми. TCP-соединение будет сломано. Не используйте badsum в oob в боевых профилях.

[!note] Проще говоря Checksum — контрольная сумма, по которой получатель проверяет целость TCP-пакета; badsum портит её нарочно. В фейках это удобно: сервер отбрасывает обманку. У oob отдельного фейка нет, и порча суммы убила бы единственный настоящий пакет.


F) Standard rawsend

Rawsend — отправка сформированного «сырого» пакета в сеть помимо обычной очереди перехваченных пакетов. Тот же набор описан в Zapret2/desync/multisplit#H) Standard rawsend у multisplit.

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

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

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

Стадия общего скелета Что делает oob
1. Отсев транспорта самая строгая проверка из всех: нужен TCP, нужен работающий conntrack (desync.track), и первый увиденный пакет обязан быть SYN — иначе instance_cutoff
2. Направление отклонение: аргумента dir нет; вместо направления функция смотрит позицию пакета в соединении (pos_get) и по-разному обрабатывает исходящие и входящие
3. Аргументы обязательных нет; char/byte задают OOB-байт, urp — указатель
4. Данные reasm_data → payload, без поддержки blob
5. Гварды отклонение: штатных гвардов нет, вместо них — проверка позиции пакета и непустого payload
6. replay отклонение: своя логика — части replay дропаются, а на последней инстанс отключается
7. Своя техника сдвиг sequence на SYN и первом ACK, вставка OOB-байта с флагом URG и указателем th_urp
8. Вердикт VERDICT_MODIFY на служебных пакетах (сдвиг seq/ack), VERDICT_DROP после отправки OOB-сегмента

oob — самая непохожая на остальные функция набора: она вмешивается в соединение с самого рукопожатия и держит собственное состояние в track.lua_state. Отсюда и её главное эксплуатационное ограничение: без conntrack она не работает вовсе, а если инстанс подключился к уже установленному соединению, он сразу отключается.

[!note] Проще говоря Общий скелет — одинаковый порядок действий у всех функций дурения; oob выполняет их не все и не так, как остальные. Вместо фильтра направления она смотрит позицию пакета, вместо штатных проверок (гвардов) — позицию и непустой payload. desync.track — запись о текущем соединении в системе отслеживания (conntrack), в ней oob держит своё состояние (track.lua_state). Стадии подробнее: Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff, Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload, Zapret2/жизненный цикл desync-функции#Стадия 5. Три гварда, Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay), Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация; сводка отличий по всем функциям — Zapret2/жизненный цикл desync-функции#Карта отличий по всем функциям.


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

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

function oob(ctx, desync)
    -- 1. Требуется conntrack
    if not desync.track then return end

    -- 2. Только TCP (ICMP пропускается, остальное -- cutoff)
    if not desync.dis.tcp then
        if not desync.dis.icmp then instance_cutoff_shim() end
        return
    end

    -- 3. Проверка: начали с SYN?
    if not lua_state[key.."_syn"] then
        if th_flags ~= TH_SYN then
            -- Соединение началось без нас -- невозможно вставить OOB
            DLOG("must be applied since the very beginning - SYN packet")
            instance_cutoff_shim()
            return
        end
        lua_state[key.."_syn"] = true  -- запомнить, что видели SYN
    end

    if desync.outgoing then
        -- === ИСХОДЯЩИЕ ПАКЕТЫ ===
        local pos = pos_get(desync, 's', false)  -- relative seq position

        if pos <= 1 then
            -- SYN (pos=0) или первый ACK (pos=1): уменьшить seq на 1
            desync.dis.tcp.th_seq = th_seq - 1
        end

        if pos == 0 then
            -- SYN: просто модифицировать seq и пропустить
            return VERDICT_MODIFY

        elseif pos == 1 then
            -- Первый пакет с данными
            local data = desync.reasm_data or desync.dis.payload

            if #data == 0 then
                -- Пустой ACK: модифицировать seq и пропустить
                return VERDICT_MODIFY
            else
                -- Определить OOB-байт (char > byte > 0x00)
                local oob_byte = desync.arg.char
                    or (desync.arg.byte and bu8(desync.arg.byte))
                    or "\x00"
                assert(#oob_byte == 1)

                -- Создать копию диссекта
                local dis_oob = deepcopy(desync.dis)

                -- Определить urp позицию
                if not urp or urp == 'b' then
                    -- Начало: вставить перед 1-м байтом
                    insert_pos = 1
                    dis_oob.tcp.th_urp = 0
                elseif urp == 'e' then
                    -- Конец: вставить после последнего байта
                    insert_pos = #data + 1
                    dis_oob.tcp.th_urp = insert_pos
                else
                    -- Маркер: разрешить позицию
                    insert_pos = resolve_pos(data, l7payload, urp)
                    if not insert_pos then
                        DLOG("cannot resolve urp marker")
                        instance_cutoff_shim()
                        return
                    end
                    dis_oob.tcp.th_urp = insert_pos  -- +1 по RFC
                end

                -- Вставить OOB-байт
                dis_oob.payload = data[1..urp-1] .. oob_byte .. data[urp..]
                -- Установить TH_URG
                dis_oob.tcp.th_flags |= TH_URG

                -- Отправить с автосегментацией по MSS
                rawsend_dissect_segmented(desync, dis_oob)

                -- Cutoff (если не replay)
                if not desync.replay then
                    instance_cutoff_shim()  -- оба направления
                end

                return VERDICT_DROP
            end

        else
            -- pos > 1: replay-часть -- дропнуть
            if desync.replay then
                if desync.replay_piece_last then
                    instance_cutoff_shim()
                end
                return VERDICT_DROP
            end
            instance_cutoff_shim()
        end

    else
        -- === ВХОДЯЩИЕ ПАКЕТЫ ===
        local pos = pos_get(desync, 's', true)  -- reverse pos
        if pos > 1 then
            -- Неожиданная позиция -- cutoff
            instance_cutoff_shim()
            return
        end
        -- Увеличить ack на 1 (компенсация сдвига seq)
        desync.dis.tcp.th_ack = th_ack + 1
        return VERDICT_MODIFY
    end
end

Читать код удобнее по трём веткам. Если соединение началось не с SYN, функция отключается. В исходящих пакетах у SYN и первого пакета после него из seq вычитается единица, а первый пакет с данными заменяется пакетом со срочным байтом. Во входящих к ack прибавляется единица. Для тех, кто не читает Lua: return VERDICT_MODIFY значит «пропусти пакет дальше с моими правками», VERDICT_DROP — «этот пакет не отправляй» (нужный пакет функция отправила сама), а instance_cutoff_shim() — «отключи функцию на этом соединении».


Поведение при reasm (многопакетный payload)

Если payload не помещается в один TCP-сегмент (например, большой TLS ClientHello с post-quantum ключами), nfqws2 собирает все части в буфер reasm_data — общая механика описана в жизненный цикл desync-функции (стадия 6). oob обходится с ней по-своему:

  1. Берёт весь reasm_data (а не отдельные части)
  2. Вставляет OOB-байт в позицию urp внутри полного reasm
  3. Отправляет через rawsend_dissect_segmented, которая автоматически разбивает на сегменты по MSS

[!note] Проще говоря Reasm («повторная сборка») — целый запрос, собранный движком из нескольких пакетов; replay — прогон придержанных частей через функции; MSS — наибольший объём данных в одном TCP-сегменте. Если запрос не влез в один пакет, oob вставляет байт в собранное целое и сама режет результат на сегменты. Общая механика описана в Zapret2/desync/multisplit#Поведение при replay / reasm и Zapret2/desync/multisplit#Автосегментация по MSS у multisplit.

При сегментации rawsend_dissect_segmented корректно обрабатывает OOB:

Сегментация по MSS с OOB:

Полный payload с OOB (3000 байт, MSS=1460):
  [данные_до_urp][OOB][данные_после_urp]

Сегмент 1 (1460 байт): th_urp нормализуется, TH_URG если urp попал сюда
Сегмент 2 (1460 байт): TH_URG снимается если urp не попал сюда
Сегмент 3 (80 байт):   аналогично

В каждом сегменте:

  • Если urp попадает в диапазон [pos, pos+len) -- устанавливается TH_URG, th_urp нормализуется по смещению сегмента (th_urp = urp - pos + 1)
  • Если urp не попадает в сегмент -- TH_URG снимается, th_urp = 0

Replay-части (2-я и далее) дропаются. Cutoff выполняется после последней replay-части.

[!note] Проще говоря Флаг TH_URG остаётся только на том сегменте, куда попал срочный байт; в остальных он снят, а указатель обнулён. В нужном сегменте указатель считается от начала этого сегмента, а не от начала всего запроса.


Сравнение с tpws --oob

tpws — прокси-вариант Zapret: он работает не с пакетами, а с потоком данных, а рукопожатие делает сама операционная система. Свою вставку срочного байта он делает через флаг MSG_OOB обычного вызова send(). Таблица ниже сравнивает два подхода построчно.

Аспект oob (nfqws2) --split-pos=.. --oob (tpws)
Уровень работы Пакетный (L3/L4, raw sockets) Потоковый (L7, прокси)
Способ вставки Модификация raw TCP-пакетов с TH_URG Использование MSG_OOB в send()
Позиция OOB Любая (маркер, b, e) Привязана к --split-pos
Сегментация + OOB Невозможна (несовместима с multisplit) Возможна (split + oob в одном вызове)
Модификация handshake Да (сдвиг seq на SYN) Нет (ОС делает handshake)
Контроль th_urp Полный (можно задать b, e, маркер) Нет (ОС решает)
Контроль OOB-байта Да (char, byte) Ограниченный
Работа с reasm Да (весь reasm, MSS-сегментация) Нет (потоковый режим)
NAT совместимость Да Да
Необходимость --in-range Да (--in-range=-s1) Нет (прокси)

Вывод: oob в nfqws2 даёт больше контроля над позицией и содержимым OOB-байта, но не может быть скомбинирован с TCP-сегментацией. tpws позволяет комбинировать split + OOB, но без тонкого контроля th_urp.

[!note] Проще говоря oob в nfqws2 сама собирает и отправляет пакеты, поэтому может выбрать любой байт и любое место, но не сочетается с разрезом на сегменты. tpws умеет разрезать поток и вставить срочный байт одним вызовом, но тонко управлять указателем и самим байтом не даёт.


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

1. Обязательно --in-range=-s1

Для HTTP и TLS (протоколов, где сервер ждёт запроса клиента) необходим --in-range=-s1. Это разрешает перехват входящего SYN-ACK от сервера, что нужно для корректной работы oob (коррекция ack). Без --in-range=-s1 oob не увидит SYN и откажется работать.

На Windows --wf-tcp-in для HTTP/TLS не нужен -- достаточно автоматически перехватываемых SYN-пакетов.

[!note] Проще говоря --in-range=-s1 пускает oob к самому началу соединения: без этого она не увидит SYN и откажется работать. Формат --in-range разобран в заметке Zapret2/out-range. --wf-tcp-in — параметр перехвата входящих TCP-портов на Windows (Zapret2/wf; сами режимы перехвата — в Zapret2/windivert).

2. Для server-first протоколов нужен --wf-tcp-in (Windows)

Для протоколов, где сервер отправляет данные до первого запроса клиента (например, SMTP, FTP, SSH), требуется разрешить все входящие пакеты до первого исходящего с данными. На Windows это означает обязательный --wf-tcp-in.

[!note] Проще говоря Server-first протоколы — те, где первым говорит сервер: баннер SMTP, FTP или SSH приходит раньше любого запроса клиента. oob должна видеть все входящие пакеты до первого исходящего с данными, поэтому на Windows входящие приходится разрешать целиком через --wf-tcp-in (Zapret2/wf).

3. urp=b работает только на Linux-серверах

Значение th_urp=0 невалидно по RFC 1122. Linux-серверы его обрабатывают корректно (удаляют OOB-байт), но Windows и BSD серверы ломаются -- соединение может разорваться или данные будут повреждены. Используйте urp=b только если уверены, что целевой сервер работает на Linux.

[!note] Проще говоря Если не уверены, что сервер работает на Linux, urp=b не используйте; для не-Linux серверов в примере 2 безопаснее назван urp=0. О самих стандартах — #Два стандарта th_urp.

4. Нет фильтрации по payload

oob не может быть отфильтрован по типу payload (payload=tls_client_hello и т.д.). Причина: модификация начинается на SYN (до появления любого payload). После сдвига sequence "соскочить" невозможно -- если oob не вставит байт, sequence будут сдвинуты, и TCP-соединение сломается.

Фильтрация возможна только на уровне профиля (--filter-tcp, --hostlist через --ipcache-hostname и т.д.).

[!note] Проще говоря Профиль — набор правил для определённого трафика (Zapret2/profile#Анатомия профиля); отбирают трафик фильтрами вроде --filter-tcp (Zapret2/filter) и списками доменов (hostlist). Тип payload (tls_client_hello, http_req и другие) — то, как движок распознаёт протокол; подробнее в заметке payload. Причина запрета — сдвиг seq на SYN: #Фаза 1: перехват SYN.

5. Нет фильтрации по направлению (dir)

Стандартный аргумент dir отсутствует. Функция автоматически обрабатывает оба направления: исходящие -- вставка OOB и сдвиг seq, входящие -- коррекция ack.

6. Несовместимость с multisplit, fakedsplit и другими функциями отправки payload

oob не может работать совместно с multisplit, multidisorder, fakedsplit, fakeddisorder и другими функциями, которые пересылают текущий payload. Причина: эти функции отправляют копии payload, но без OOB-модификации. В результате:

  • oob вставит OOB-байт и сдвинет sequence
  • multisplit отправит свои сегменты без OOB и без учёта сдвига
  • Сервер получит дублирующиеся данные с рассогласованными sequence

Получить эффект "разбить на сегменты и всунуть OOB" (как в tpws) через комбинацию oob + multisplit невозможно.

[!note] Проще говоря Соседние функции отправляют собственные копии данных: Zapret2/desync/multisplit#Зачем нужен multisplit и Zapret2/desync/multidisorder#Зачем нужен multidisorder режут запрос на сегменты, Zapret2/desync/fakedsplit#Зачем нужен fakedsplit и Zapret2/desync/fakeddisorder#Зачем нужен fakeddisorder подмешивают к разрезу фейки. Ни одна из них не знает о срочном байте и сдвиге seq, поэтому сервер получил бы дубли с рассогласованной нумерацией. С Zapret2/desync/fake#Зачем нужен fake в разных профилях oob ужиться может: #10. Комбинация: fake + oob (в разных профилях).

7. Длящаяся десинхронизация и переключение профилей

oob -- длящаяся десинхронизация: между SYN (сдвиг seq) и первым payload (вставка OOB) проходит несколько пакетов. Если за это время происходит переключение профиля (например, --ipcache-hostname разрешил домен и переключил на другой профиль), новый профиль должен также содержать oob. Иначе:

  • Старый профиль сдвинул seq на SYN
  • Новый профиль не знает об этом и не вставит OOB
  • TCP-соединение сломано (sequence рассогласованы)

Решение: дублировать oob во всех профилях, которые могут получить управление в процессе handshake.

[!note] Проще говоря Длящаяся десинхронизация — та, что растянута на несколько пакетов соединения: сначала SYN, потом данные. Профиль — набор правил для определённого трафика (Zapret2/profile#Анатомия профиля); движок может сменить профиль соединения по ходу, например когда кэш --ipcache-hostname выдал имя сайта (Zapret2/filter#Как выбирается профиль). Если новый профиль не содержит oob, сдвиг seq, сделанный старым, никто не компенсирует. Поэтому строку oob дублируют во всех профилях, которые могут получить управление на рукопожатии; пример — #14. Дублирование oob при переключении профилей.

8. Требуется conntrack (tracking)

oob использует desync.track для хранения состояния (видели ли SYN, текущая позиция). Без tracking функция немедленно возвращает nil.

[!note] Проще говоря Tracking (conntrack) — система, которая запоминает состояние каждого соединения между пакетами. oob хранит там, видела ли она SYN и на каком пакете соединения сейчас находится. Без такой памяти функция сразу выходит и ничего не делает. Устройство записи track описано в Zapret2/структура desync и диссекта.

9. Соединение должно быть перехвачено с SYN

Если oob получает управление после SYN (например, после перескока профиля или при подключении к уже установленному соединению), она немедленно выполняет cutoff. Специальный флаг lua_state[key.."_syn"] гарантирует, что oob видела SYN самого первого пакета.

10. Фильтрация по хостлистам

Фильтрация через --hostlist возможна только при использовании --ipcache-hostname. Если в кэше хоста ещё нет, и профиль получает управление не с начала соединения -- срабатывает cutoff (см. п.9).

[!note] Проще говоря Hostlist — файл со списком доменов (hostlist): профиль срабатывает только для сайтов из списка. Имя сайта из SNI или Host: появляется лишь в первом пакете с данными, то есть позже SYN, а oob должна начать раньше; поэтому список доменов с oob работает только вместе с кэшем --ipcache-hostname (в пресетах он включается строкой --ipcache-hostname=1, см. Zapret2/preset).

11. OOB-байт ровно 1

OOB-байт обязательно 1 байт. Попытка задать char длиной != 1 вызовет Lua error.

12. Пустой ACK не ломает поток

Если первый исходящий пакет с данными оказался пустым ACK (пустой payload), oob просто уменьшает seq и возвращает VERDICT_MODIFY. Вставка OOB произойдёт при появлении следующего непустого пакета.


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

Пятнадцать примеров для Zapret2/profile#Анатомия профиля. Во всех строках с oob есть --in-range=-s1: без него функция не включится (нюанс 1). Если urp не указан, действует b. Порядок — от простого к комбинациям с другими опциями; самый популярный вариант — пример 4.

1. Минимальный: urp=b (по умолчанию), OOB=0x00

--in-range=-s1 --lua-desync=oob

Вставляет 0x00 перед первым байтом payload. Работает только на Linux-серверах.

2. Позиция urp=0 (нулевая, 0-based = первый байт)

--in-range=-s1 --lua-desync=oob:urp=0

Маркер 0 разрешается в абсолютную позицию. th_urp = 0 + 1 = 1 (RFC 1122). Безопаснее urp=b для не-Linux серверов.

Проще говоря: маркер 0 даёт указатель th_urp=1, а не 0, как режим b; для не-Linux серверов статья называет его более безопасным, чем urp=b. Про два стандарта — #Два стандарта th_urp.

3. Позиция urp=2 (после первого байта)

--in-range=-s1 --lua-desync=oob:urp=2

OOB-байт вставляется после первого байта payload. th_urp = 3. Для HTTP: G[OOB]ET /... -- DPI не распознает метод.

Проще говоря: сначала идёт буква G, затем лишний байт, затем ET; DPI, не вынимающий срочный байт, читает метод как G\x00ET, а не как GET.

4. Середина SLD (самый популярный вариант)

--in-range=-s1 --lua-desync=oob:urp=midsld

Для example.com: OOB вставляется посередине example. DPI видит exa[OOB]mple.com.

Маркер midsld разобран в Zapret2/desync/multisplit#Типы маркеров; схема этого варианта — в анимации в начале статьи (#Коротко и по-простому).

5. Произвольный OOB-символ

--in-range=-s1 --lua-desync=oob:char=Z:urp=midsld

OOB-байт = ASCII 'Z' вместо 0x00.

Аргумент char — один символ; описание — в #A) Собственные аргументы oob.

6. OOB-байт по числовому значению

--in-range=-s1 --lua-desync=oob:byte=255:urp=midsld

OOB-байт = 0xFF.

7. Для HTTPS (TLS) с фильтром по порту

--filter-tcp=443 --in-range=-s1 --lua-desync=oob:urp=midsld

Применяется только к HTTPS-соединениям.

--filter-tcp=443 — фильтр профиля по порту: 443 — порт HTTPS (Zapret2/filter).

8. Для HTTP и HTTPS одновременно

--filter-tcp=80,443 --in-range=-s1 --lua-desync=oob:urp=midsld

Порты 80 и 443 — HTTP и HTTPS: oob вставляет байт и в HTTP-запрос, и в TLS ClientHello.

9. С хостлистом и ipcache

--filter-tcp=443 --hostlist=blocked.txt --ipcache-hostname \
  --in-range=-s1 --lua-desync=oob:urp=midsld

Фильтрация по хостлисту. --ipcache-hostname обязателен для работы хостлиста с oob.

Проще говоря: hostlist — файл со списком доменов; кэш --ipcache-hostname нужен, чтобы хостлист вообще работал с oob (нюанс #10. Фильтрация по хостлистам).

10. Комбинация: fake + oob (в разных профилях)

# Профиль 1: фейк для TLS
--filter-tcp=443 --payload=tls_client_hello \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5

# Профиль 2: oob
--filter-tcp=443 --in-range=-s1 \
  --lua-desync=oob:urp=midsld

Важно: fake и oob могут быть в разных профилях, но oob несовместим с multisplit/fakedsplit в рамках одного потока.

Проще говоря: в первом профиле fake посылает отдельный фейковый ClientHello (blob=fake_default_tls) с добавленной опцией tcp_md5 — это fooling, порча, чтобы сервер фейк отбросил (Zapret2/desync/fake#blob — источник фейковых данных, Zapret2/ts-and-fooling#Что такое fooling); во втором профиле oob работает с настоящим запросом.

11. Перебор позиций urp (как в blockcheck2)

# blockcheck2 перебирает: b, 0, 2, midsld
--in-range=-s1 --lua-desync=oob:urp=b
--in-range=-s1 --lua-desync=oob:urp=0
--in-range=-s1 --lua-desync=oob:urp=2
--in-range=-s1 --lua-desync=oob:urp=midsld

Это паттерн из blockcheck2.d/standard/17-oob.sh -- попробовать разные позиции urp для нахождения рабочей.

Проще говоря: blockcheck2 — проверочный скрипт из репозитория zapret2, который перебирает варианты стратегий; для oob он пробует позиции b, 0, 2 и midsld. Как честно проверять результат — Zapret2/verify-strategy#Короткий чек-лист честной проверки.

12. С IP-фрагментацией

--in-range=-s1 --lua-desync=oob:urp=midsld:ipfrag:ipfrag_pos_tcp=32

OOB-пакет дополнительно фрагментируется на IP-уровне.

13. С IPv6 extension headers

--in-range=-s1 --lua-desync=oob:urp=midsld:ip6_hopbyhop

Добавляет hop-by-hop extension header к OOB-пакету.

IPv6 extension header — дополнительный заголовок IPv6 между основным заголовком и TCP; hop-by-hop — один из таких заголовков (таблица в #B) Standard fooling).

14. Дублирование oob при переключении профилей

# Профиль "по умолчанию" (до разрешения hostname)
--filter-tcp=443 --in-range=-s1 \
  --lua-desync=oob:urp=midsld

# Профиль "для заблокированных" (после ipcache-hostname)
--filter-tcp=443 --hostlist=blocked.txt --ipcache-hostname --in-range=-s1 \
  --lua-desync=oob:urp=midsld

oob дублируется в оба профиля, чтобы переключение не сломало TCP.

Проще говоря: первый профиль работает, пока имя сайта ещё неизвестно, второй — когда --ipcache-hostname его выдал; oob есть в обоих, иначе сдвиг seq останется без пары (#7. Длящаяся десинхронизация и переключение профилей).

15. OOB в позиции host (начало hostname)

--in-range=-s1 --lua-desync=oob:urp=host:byte=0

Вставляет нулевой байт прямо перед hostname. DPI видит \x00example.com вместо example.com.

Маркер host — первый байт имени хоста; byte=0 — то же, что значение по умолчанию (0x00).


📚 См. также

  • Zapret2/desync — обзор всех функций --lua-desync и общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты
  • Zapret2/жизненный цикл desync-функции — восемь стадий, общих для всех техник дурения; отдельно разобрано, чем от них отличается oob
  • Zapret2/структура desync и диссекта — поля th_flags, th_urp и работа с sequence numbers
  • Zapret2/out-range — формат --in-range и --out-range, в том числе -s1
  • multisplit · multidisorder · fakedsplit · fakeddisorder — функции отправки payload, с которыми oob в одном потоке несовместима
  • syndata — данные в SYN, другая техника, работающая с рукопожатием
  • tcpseg — отправка диапазона данных между двумя маркерами
  • fake — отдельные фейковые пакеты; их можно держать в соседнем профиле
  • payload — как распознаются типы данных и почему у oob нет фильтра payload
  • Zapret2/filter · hostlist — как отбирать соединения на уровне профиля
  • Zapret2/wf — --wf-tcp-in для протоколов, где сервер говорит первым
  • Zapret2/ts-and-fooling — что такое fooling и почему в oob он почти не нужен
  • Zapret2/verify-strategy — как честно проверить подобранную позицию urp
  • Zapret2/profile · Zapret2/preset — где --lua-desync живёт среди остальных настроек
  • DPI/DPI — как устроена инспекция трафика, против которой работает oob

Источники: lua/zapret-antidpi.lua:1084-1176, lua/zapret-lib.lua:1148-1192, docs/manual.md:4276-4309, docs/manual.en.md:4095-4127, blockcheck2.d/standard/17-oob.sh из репозитория zapret2.


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