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

971 lines
91 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
date: 2026-02-17
tags:
- zapret
- zapret2
- nfqws2
- lua
- lua-desync
- antidpi
- tcp
- oob
- urgent
aliases:
- oob
description: "Техника oob в zapret2: вставка TCP Out-of-Band байта с флагом URG ломает сигнатуры DPI. Режимы urp, полный список аргументов, подводные камни, примеры."
---
> [!mirror] Резервное зеркало
> Актуальная версия этой страницы — на основной вики: [wiki2.zapret.moe/Zapret2/desync/oob](https://wiki2.zapret.moe/Zapret2/desync/oob)
# `oob` -- TCP Out-of-Band десинхронизация (zapret2 / nfqws2)
## Коротко и по-простому
Когда браузер открывает сайт по HTTPS, первым сообщением с данными он отправляет TLS ClientHello («привет от клиента»). В нём браузер перечисляет, какое шифрование умеет, и открытым текстом называет нужный сайт: имя лежит в поле SNI (Server Name Indication, «указание имени сервера»). У обычного HTTP без шифрования то же имя стоит в строке `Host:` запроса. ТСПУ (Технические средства противодействия угрозам — оборудование [[DPI/DPI|DPI]], то есть глубокой инспекции трафика, которое стоит у провайдера) ищет это имя в первом пакете соединения и по нему решает, что делать с соединением дальше.
HTTP-запросы и TLS-соединения идут по протоколу TCP. Соединение начинается с рукопожатия: первым пакетом клиент отправляет SYN («хочу соединиться») и называет в нём стартовый номер — sequence number, коротко seq. Дальше у каждого байта соединения есть порядковый номер, а сервер раскладывает принятые данные в свой буфер строго по этим номерам. Стартовый номер из SYN он запоминает и ждёт первый байт данных сразу за ним: если клиент начал с номера 999, то данные ожидаются с 1000 (999 плюс единица за сам SYN). Как с такими номерами считать в Lua (они 32-битные беззнаковые и заворачиваются по кругу) — в [[Zapret2/структура desync и диссекта#Работа с sequence numbers|Работа с sequence numbers]].
Кроме обычных данных, в TCP есть редко используемый механизм срочных данных, по-английски Out-of-Band, сокращённо OOB («вне основного потока»). Отправитель ставит в заголовке пакета флаг URG («urgent», «срочно») и записывает в поле `th_urp` (urgent pointer, «указатель срочности») положение срочного байта. TCP-стек операционной системы сервера — та её часть, что принимает пакеты и собирает из них поток для программ, — этот байт из потока выбрасывает, и программа получает чистые данные без вставки. [[DPI/DPI|DPI]] устроен иначе: он может не понимать механизм срочных данных и разбирать поток вместе с лишним байтом. Как именно два стандарта указывают на срочный байт, разобрано в разделе [[#Два стандарта th_urp|Два стандарта th_urp]].
Функция `oob` в [[Zapret2/Zapret2|Zapret 2]] (движок `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|«Соединение должно быть перехвачено с SYN»]] и [[#7. Длящаяся десинхронизация и переключение профилей|«Длящаяся десинхронизация…»]].
Сервер получает поток, из которого срочный байт вынут, и отвечает как обычно: приложение видит ровно тот запрос, который отправил браузер. DPI, который не вынимает срочные байты, читает поток с лишним байтом посередине (например, `exa\x00mple.com` вместо `example.com`) и не находит имя сайта. Всё держится на слове «может»: приём рассчитан на DPI, который срочный байт не вынимает; на другом DPI он может не сработать. Чем `oob` отличается от простого разреза на куски, разобрано в разделе [[#Зачем нужен 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: байт срочных данных с флагом URG посреди имени, сервер вынимает его и имя смыкается]]
Самая короткая команда включает `oob` со значениями по умолчанию: лишний байт `0x00` ставится перед самым первым байтом данных (`urp=b`). Строку пишут в [[Zapret2/profile#Анатомия профиля|профиль]] — набор правил Zapret 2 для определённого трафика. Для HTTP и TLS флаг `--in-range=-s1` обязателен: он задаёт, какие входящие пакеты вообще доходят до функции (формат разобран в заметке [[Zapret2/out-range|про --out-range и --in-range]]), и без него `oob` не увидит рукопожатие и не включится; подробнее — в нюансе [[#1. Обязательно --in-range=-s1|«Обязательно --in-range=-s1»]].
```bash
--in-range=-s1 --lua-desync=oob
```
Режим `urp=b` работает только на Linux-серверах: Windows и BSD с нулевым `th_urp` ломаются ([[#3. urp=b работает только на Linux-серверах|нюанс 3]]). Самый популярный вариант — `urp=midsld`, как на схеме: байт ставится посередине домена второго уровня (SLD, главной части имени сайта: у `www.youtube.com` это `youtube`); все режимы и маркеры разобраны в разделе [[#Режимы urp|Режимы urp]], готовые строки — в [[#Быстрый старт|Быстром старте]] и [[#Практические примеры|Практических примерах]].
`oob` не режет данные на куски, как [[Zapret2/desync/multisplit#Зачем нужен multisplit|multisplit]], и не посылает отдельных фейковых пакетов, как [[Zapret2/desync/fake#Зачем нужен fake|fake]] (фейк — пакет-обманка с ложными данными, который нарочно портят приёмом fooling, чтобы сервер его отбросил, а DPI прочитал; см. [[Zapret2/ts-and-fooling#Что такое fooling|Что такое fooling]]): лишний байт вставляется внутрь единого настоящего потока. Поэтому `oob` нельзя сочетать в одном потоке с `multisplit`, `fakedsplit` и другими функциями, которые пересылают данные, — они отправят копии без срочного байта и без учёта сдвига ([[#6. Несовместимость с multisplit, fakedsplit и другими функциями отправки payload|нюанс 6]]). Соседи по приёму: [[Zapret2/desync/syndata#Зачем нужен syndata|syndata]] кладёт данные прямо в SYN, а [[Zapret2/desync/tcpseg#Зачем нужен tcpseg|tcpseg]] отправляет диапазон данных между двумя маркерами. Есть и другие ограничения. У `oob` нет фильтров `dir` и `payload` ([[#4. Нет фильтрации по payload|нюанс 4]]), поэтому отбирать соединения приходится на уровне профиля. Ей нужен conntrack — отслеживание состояния соединений в системе ([[#8. Требуется conntrack (tracking)|нюанс 8]]). Режим `urp=e` как правило бесполезен для обхода DPI ([[#urp=e -- конец|почему]]). Сдвиг seq на SYN, отключение при подключении «на ходу» и переключение профилей — в нюансах 7 и 9. Подходящую позицию `urp` подбирают перебором и проверяют по [[Zapret2/verify-strategy#Короткий чек-лист честной проверки|чек-листу честной проверки]]; пример перебора — [[#11. Перебор позиций urp (как в blockcheck2)|как в blockcheck2]].
---
## Оглавление
- [Коротко и по-простому](#коротко-и-по-простому)
- [Справка: где функция в коде и родственники](#справка-где-функция-в-коде-и-родственники)
- [Зачем нужен oob](#зачем-нужен-oob)
- [Быстрый старт](#быстрый-старт)
- [Принцип работы](#принцип-работы)
- [Фаза 1: перехват SYN](#фаза-1-перехват-syn)
- [Фаза 2: вставка OOB-байта](#фаза-2-вставка-oob-байта)
- [Фаза 3: cutoff](#фаза-3-cutoff)
- [Обработка входящих пакетов](#обработка-входящих-пакетов)
- [Два стандарта th_urp](#два-стандарта-th_urp)
- [Режимы urp](#режимы-urp)
- [urp=b -- начало (по умолчанию)](#urpb----начало-по-умолчанию)
- [urp=e -- конец](#urpe----конец)
- [urp=маркер -- произвольная позиция](#urpмаркер----произвольная-позиция)
- [Полный список аргументов](#полный-список-аргументов)
- [A) Собственные аргументы oob](#a-собственные-аргументы-oob)
- [B) Standard fooling](#b-standard-fooling)
- [C) Standard ipid](#c-standard-ipid)
- [D) Standard ipfrag](#d-standard-ipfrag)
- [E) Standard reconstruct](#e-standard-reconstruct)
- [F) Standard rawsend](#f-standard-rawsend)
- [Место в общем скелете](#место-в-общем-скелете)
- [Псевдокод алгоритма](#псевдокод-алгоритма)
- [Поведение при reasm (многопакетный payload)](#поведение-при-reasm-многопакетный-payload)
- [Сравнение с tpws --oob](#сравнение-с-tpws---oob)
- [Нюансы и подводные камни](#нюансы-и-подводные-камни)
- [Практические примеры](#практические-примеры)
- [📚 См. также](#-см-также)
---
## Справка: где функция в коде и родственники
Паспорт функции для тех, кто сверяется с исходниками 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|стадии 1 жизненного цикла]]. Поля `th_flags` и `th_urp` — части диссекта, то есть пакета, разобранного на поля, — описаны в [[Zapret2/структура desync и диссекта|структуре desync и диссекта]]. Чем `oob` отличается от `tpws --oob`, разобрано в разделе [[#Сравнение с tpws --oob|Сравнение с tpws --oob]].
Родственные функции: [[multisplit]] (TCP-сегментация), [[fakedsplit]] (с фейками), [[fake]] (фейковые пакеты), [[syndata]] (данные в SYN), [[tcpseg]] (диапазон).
Общий для всех техник дурения порядок работы — восемь стадий от отсева чужого транспорта до вердикта — разобран в [[жизненный цикл desync-функции]]; здесь описано только то, чем `oob` от этого скелета отличается.
---
## Зачем нужен oob
[[DPI/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|Зачем нужен multisplit]] (разрез на сегменты), [[Zapret2/desync/fake#Зачем нужен fake|Зачем нужен fake]] и [[Zapret2/ts-and-fooling#Что такое fooling|Что такое fooling]] (фейковые пакеты и их порча). Что такое `--lua-desync` и как движок вызывает такие функции — в [[Zapret2/desync|desync]].
---
## Быстрый старт
Три готовые строки для [[Zapret2/profile#Анатомия профиля|профиля]] Zapret 2, от самой короткой к более гибким. Во всех есть `--in-range=-s1`; зачем он нужен, сказано сразу под примерами. Значения `urp`, `char` и `byte` разобраны в разделах [[#Режимы urp|Режимы urp]] и [[#A) Собственные аргументы oob|Собственные аргументы oob]].
Минимально (urp=b по умолчанию, OOB-байт = `0x00`):
```bash
--in-range=-s1 --lua-desync=oob
```
С маркером по середине SLD:
```bash
--in-range=-s1 --lua-desync=oob:urp=midsld
```
С конкретным OOB-символом:
```bash
--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)|2]].
---
## Принцип работы
Работа идёт в три фазы, а параллельно с ними — обработка входящих пакетов: [[#Фаза 1: перехват SYN|перехват SYN]] со сдвигом seq, [[#Фаза 2: вставка OOB-байта|вставка]] срочного байта в первый пакет с данными, [[#Фаза 3: cutoff|отключение]] и [[#Обработка входящих пакетов|коррекция ack]] в пакетах от сервера. Как эти шаги вписаны в общую схему всех функций, показано в разделе [[#Место в общем скелете|Место в общем скелете]].
### Фаза 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|4]]. Как с такими номерами считать в Lua (они 32-битные беззнаковые и заворачиваются по кругу) — [[Zapret2/структура desync и диссекта#Работа с sequence numbers|Работа с 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|стадия 4 жизненного цикла]]). `rawsend_dissect_segmented` отправляет пакет, разобранный на поля (диссект), и сама разбивает его на сегменты по MSS — наибольшему объёму данных в одном TCP-сегменте ([[#Поведение при reasm (многопакетный payload)|подробнее ниже]]); среди способов выпустить пакет она описана в разделе [[Zapret2/жизненный цикл desync-функции#Четыре способа выпустить пакет|Четыре способа выпустить пакет]]. `VERDICT_DROP` — решение «перехваченный пакет не отправлять» ([[Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация|стадия 8]]).
**Визуализация вставки (urp=b, th_urp=0):**
![[wiki-anim-z2-oob-bytes.svg|Анимация: OOB-байт вставлен в запрос, сервер его удаляет, DPI читает сдвинутую строку]]
Квадратные скобки — по одному байту; `OOB` — вставленный срочный байт, `^` указывает на него, а `th_urp=0` и `TH_URG=1` — значения полей заголовка. DPI, не вынимающий срочный байт, видит `0x00` перед `GET`: сигнатура сдвинута и сломана. Режим `b` подробнее — в разделе [[#urp=b -- начало (по умолчанию)|urp=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|Два стандарта 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|стадия 1 жизненного цикла]]). Replay («перепроигрывание») — это когда движок придержал части большого запроса, собрал их и прогоняет по очереди через функции: `oob` при этом дропает каждую придержанную часть, а отключается только на последней ([[Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)|стадия 6]]).
### Обработка входящих пакетов
Для входящих пакетов (от сервера) 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. urp=b работает только на Linux-серверах|нюанс 3]]). Для `e` и маркеров `oob` пишет позицию плюс единица.
---
## Режимы urp
Аргумент `urp` отвечает на вопрос «в какое место данных вставить лишний байт»: `b` — в самое начало, `e` — в самый конец, маркер — в произвольную позицию ([[Zapret2/desync/multisplit#Маркеры позиций (pos)|подробно про маркеры]]). От выбора зависит и значение `th_urp` ([[#Два стандарта 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. urp=b работает только на Linux-серверах|нюанс 3]]); для не-Linux серверов статья называет более безопасным `urp=0` ([[#2. Позиция urp=0 (нулевая, 0-based = первый байт)|пример 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|Важные нюансы pos]].
**Пример:** `urp=midsld` -- OOB-байт вставляется посередине SLD. DPI видит домен с лишним байтом посередине.
**Если маркер не разрешается** (например, `midsld` для unknown payload), функция логирует ошибку и выполняет `instance_cutoff`.
> [!note] Проще говоря
> «Не разрешается» значит, что в данных нет места, на которое указывает маркер: у пакета неизвестного протокола нет имени хоста, а значит нет и его середины. Тогда функция пишет ошибку в лог и отключается (cutoff — см. [[#Фаза 3: cutoff|Фаза 3]]).
---
## Полный список аргументов
Формат вызова:
```
--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|B) Standard direction]] и [[Zapret2/desync/multisplit#C) Standard payload|C) Standard payload]] у multisplit. У `oob` их нет, потому что после сдвига seq на SYN она обязана довести соединение до конца. Направление она определяет сама: исходящие — вставка байта, входящие — правка ack. Отбирать соединения приходится профилем ([[Zapret2/filter|фильтры профиля]]), см. [[#4. Нет фильтрации по payload|нюанс 4]].
### 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|Что такое fooling]]). L3 — сетевой уровень (заголовок IP), L4 — транспортный (заголовок TCP). Тот же набор параметров описан в [[Zapret2/desync/multisplit#D) Standard fooling|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|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|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|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|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|1]], [[Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload|4]], [[Zapret2/жизненный цикл desync-функции#Стадия 5. Три гварда|5]], [[Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)|6]], [[Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация|8]]; сводка отличий по всем функциям — [[Zapret2/жизненный цикл desync-функции#Карта отличий по всем функциям|Карта отличий по всем функциям]].
---
## Псевдокод алгоритма
Тело функции целиком, включая общие для всех техник стадии — они пронумерованы так же, как в [[жизненный цикл desync-функции]]:
```lua
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|Поведение при replay / reasm]] и [[Zapret2/desync/multisplit#Автосегментация по MSS|Автосегментация по 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|заметке про 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 (нулевая, 0-based = первый байт)|примере 2]] безопаснее назван `urp=0`. О самих стандартах — [[#Два стандарта th_urp|Два стандарта 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|Фаза 1]].
### 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|multisplit]] и [[Zapret2/desync/multidisorder#Зачем нужен multidisorder|multidisorder]] режут запрос на сегменты, [[Zapret2/desync/fakedsplit#Зачем нужен fakedsplit|fakedsplit]] и [[Zapret2/desync/fakeddisorder#Зачем нужен fakeddisorder|fakeddisorder]] подмешивают к разрезу фейки. Ни одна из них не знает о срочном байте и сдвиге seq, поэтому сервер получил бы дубли с рассогласованной нумерацией. С [[Zapret2/desync/fake#Зачем нужен fake|fake]] в разных профилях `oob` ужиться может: [[#10. Комбинация: fake + oob (в разных профилях)|пример 10]].
### 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 при переключении профилей|14]].
### 8. Требуется conntrack (tracking)
`oob` использует `desync.track` для хранения состояния (видели ли SYN, текущая позиция). Без tracking функция немедленно возвращает `nil`.
> [!note] Проще говоря
> Tracking (conntrack) — система, которая запоминает состояние каждого соединения между пакетами. `oob` хранит там, видела ли она SYN и на каком пакете соединения сейчас находится. Без такой памяти функция сразу выходит и ничего не делает. Устройство записи `track` описано в [[Zapret2/структура desync и диссекта|структуре 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. Обязательно --in-range=-s1|нюанс 1]]). Если `urp` не указан, действует `b`. Порядок — от простого к комбинациям с другими опциями; самый популярный вариант — пример 4.
### 1. Минимальный: urp=b (по умолчанию), OOB=0x00
```bash
--in-range=-s1 --lua-desync=oob
```
Вставляет `0x00` перед первым байтом payload. Работает только на Linux-серверах.
### 2. Позиция urp=0 (нулевая, 0-based = первый байт)
```bash
--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|Два стандарта th_urp]].
### 3. Позиция urp=2 (после первого байта)
```bash
--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 (самый популярный вариант)
```bash
--in-range=-s1 --lua-desync=oob:urp=midsld
```
Для `example.com`: OOB вставляется посередине `example`. DPI видит `exa[OOB]mple.com`.
Маркер `midsld` разобран в [[Zapret2/desync/multisplit#Типы маркеров|Типы маркеров]]; схема этого варианта — в анимации в начале статьи ([[#Коротко и по-простому|Коротко и по-простому]]).
### 5. Произвольный OOB-символ
```bash
--in-range=-s1 --lua-desync=oob:char=Z:urp=midsld
```
OOB-байт = ASCII 'Z' вместо `0x00`.
Аргумент `char` — один символ; описание — в [[#A) Собственные аргументы oob|Собственные аргументы oob]].
### 6. OOB-байт по числовому значению
```bash
--in-range=-s1 --lua-desync=oob:byte=255:urp=midsld
```
OOB-байт = `0xFF`.
### 7. Для HTTPS (TLS) с фильтром по порту
```bash
--filter-tcp=443 --in-range=-s1 --lua-desync=oob:urp=midsld
```
Применяется только к HTTPS-соединениям.
`--filter-tcp=443` — фильтр профиля по порту: 443 — порт HTTPS ([[Zapret2/filter|фильтры профиля]]).
### 8. Для HTTP и HTTPS одновременно
```bash
--filter-tcp=80,443 --in-range=-s1 --lua-desync=oob:urp=midsld
```
Порты 80 и 443 — HTTP и HTTPS: `oob` вставляет байт и в HTTP-запрос, и в TLS ClientHello.
### 9. С хостлистом и ipcache
```bash
--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]]).
### 10. Комбинация: fake + oob (в разных профилях)
```bash
# Профиль 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 — источник фейковых данных|blob]], [[Zapret2/ts-and-fooling#Что такое fooling|Что такое fooling]]); во втором профиле `oob` работает с настоящим запросом.
### 11. Перебор позиций urp (как в blockcheck2)
```bash
# 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-фрагментацией
```bash
--in-range=-s1 --lua-desync=oob:urp=midsld:ipfrag:ipfrag_pos_tcp=32
```
OOB-пакет дополнительно фрагментируется на IP-уровне.
### 13. С IPv6 extension headers
```bash
--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|B) Standard fooling]]).
### 14. Дублирование oob при переключении профилей
```bash
# Профиль "по умолчанию" (до разрешения 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. Длящаяся десинхронизация и переключение профилей|нюанс 7]]).
### 15. OOB в позиции host (начало hostname)
```bash
--in-range=-s1 --lua-desync=oob:urp=host:byte=0
```
Вставляет нулевой байт прямо перед hostname. DPI видит `\x00example.com` вместо `example.com`.
Маркер `host` — первый байт имени хоста; `byte=0` — то же, что значение по умолчанию (`0x00`).
---
## 📚 См. также
- [[Zapret2/desync|desync]] — обзор всех функций `--lua-desync` и общий контракт: кто вызывает функцию, что ей передаёт и как складываются вердикты
- [[Zapret2/жизненный цикл desync-функции|жизненный цикл desync-функции]] — восемь стадий, общих для всех техник дурения; отдельно разобрано, чем от них отличается `oob`
- [[Zapret2/структура desync и диссекта|структура desync и диссекта]] — поля `th_flags`, `th_urp` и работа с sequence numbers
- [[Zapret2/out-range|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|WinDivert-фильтры]] — `--wf-tcp-in` для протоколов, где сервер говорит первым
- [[Zapret2/ts-and-fooling|ts-and-fooling]] — что такое fooling и почему в `oob` он почти не нужен
- [[Zapret2/verify-strategy|verify-strategy]] — как честно проверить подобранную позицию `urp`
- [[Zapret2/profile|profile]] · [[Zapret2/preset|preset]] — где `--lua-desync` живёт среди остальных настроек
- [[DPI/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: [исходник этой заметки](https://git2.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/desync/oob.md) · [скачать весь репозиторий одним zip-архивом](https://git2.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).