todo/Zapret2/desync/hostfakesplit.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

98 KiB
Raw Permalink Blame History

date tags aliases description
2026-02-17
zapret
zapret2
nfqws2
lua
lua-desync
antidpi
tcp
hostfakesplit
fake
hostname
hostfakesplit
Функция hostfakesplit в zapret2/nfqws2: разрез TCP-сегментов по границам hostname с фейковыми SNI, аргументы host, midhost, disorder_after, примеры.

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

hostfakesplit — TCP-сегментация с фейковым hostname (zapret2 / nfqws2)

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

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

Имя сайта занимает в запросе небольшой участок: перед ним идут служебные данные запроса, после него — остальное. В HTTP-запросе имя стоит после Host:, в ClientHello лежит внутри поля SNI. Границы имени hostfakesplit находит сама, по названиям-маркерам. Маркер — это название места в данных, которое движок вычисляет по устройству запроса: host — первый байт имени, endhost — байт сразу за его последним байтом. Так это выглядит в HTTP (метки под строкой показывают границы имени):

GET /watch HTTP/1.1\r\nHost: www.youtube.com\r\n...
                             ^host          ^endhost

Эти маркеры работают, только если движок распознал запрос как HTTP или как TLS ClientHello; общий список маркеров — в разделе Zapret2/desync/multisplit#Относительные маркеры статьи про multisplit, а как hostfakesplit их использует — в разделе #Основные точки разреза: host и endhost.

HTTP-запросы и TLS-соединения передаются по протоколу TCP, а TCP отправляет данные кусками — сегментами. У каждого байта в соединении есть порядковый номер (sequence number, коротко seq), и сервер раскладывает принятые куски в свой буфер строго по этим номерам. Поэтому ему безразлично, пришли данные одним пакетом или несколькими и в каком порядке куски доехали: исходный поток он всё равно соберёт. Почему это гарантирует именно TCP, разобрано в нюансе Zapret2/desync/multisplit#1. Работает только с TCP — и почему статьи про multisplit.

Ещё одно понятие: фейк. Это сегмент с ложными данными, здесь — с подложным именем сайта. Чтобы сервер не принял фейк за настоящие данные и не сломал соединение, фейк «портят» так, чтобы сервер его отбросил, а DPI мог прочитать: такая порча называется fooling (Zapret2/ts-and-fooling#Что такое fooling). В команде из быстрого старта это tcp_md5: к сегменту добавляется TCP-опция с подписью MD5 (16 байт, по умолчанию случайные), и Linux-сервер обычно такой сегмент отбрасывает. Способ не универсальный, другие собраны в Zapret2/ts-and-fooling#Как заставить fake умереть: каталог способов. Без fooling сервер примет фейки и получит два противоречивых имени на одном месте потока, поэтому для hostfakesplit он обязателен.

Теперь сама техника, по шагам. hostfakesplit перехватывает исходящий ClientHello или HTTP-запрос и находит в нём границы имени. Затем генерирует фейковое имя той же длины, что и настоящее: функция genhost собирает случайный домен вроде u9a7bk2.org, а если подсказать шаблон (host=vk.com), получится поддомен вида e8nzn.vk.com. После этого функция отправляет пять сегментов (это базовый вариант, без midhost и disorder_after): данные до имени, фейковое имя, настоящее имя, ещё раз то же фейковое имя и данные после имени. Фейки уходят с fooling и с тем же seq, что и настоящее имя; настоящим сегментам fooling не достаётся (исключение — опция tcp_ts_up). В конце функция выбрасывает перехваченный оригинал (если не задан nodrop), чтобы те же данные не ушли второй раз. Если границы имени найти нельзя (например, это не HTTP и не ClientHello), она ничего не делает.

Если fooling сработал, сервер отбрасывает оба фейка, принимает настоящие сегменты и собирает ровно тот запрос, который отправил браузер. DPI на месте имени видит три кандидата с одним и тем же seq и одной и той же длиной: фейк, настоящее имя, фейк. Длина у всех трёх одинаковая, поэтому по размеру настоящее имя не отличить, и DPI приходится выбирать, какое из имён «правильное». Он может принять за настоящее одно из фейковых имён; если он выберет фейк, обход удался. Что видит каждая сторона, показано в разделе #Порядок отправки сегментов.

Команда из быстрого старта hostfakesplit:tcp_md5 находит в ClientHello границы имени сайта (маркеры host и endhost) и отправляет пять сегментов: данные до имени, фейковое имя, настоящее имя youtube.com, ещё раз фейковое имя и данные после имени. Фейковое имя (на схеме u9a7bk2.org) той же длины и ложится на тот же seq, поэтому на месте имени ТСПУ видит три кандидата; сервер отбрасывает фейки по неверной подписи TCP MD5. Как читать схему: строки — пакеты в порядке отправки, подписанные в тех же обозначениях, что и таблицы в этой статье; по горизонтали — место данных в TCP-потоке (номер последовательности, seq); в колонке справа — что сделал с пакетом сервер; внизу — буфер сервера, в который принятые куски встают по своим seq.

!desync-anim-hostfakesplit.svg

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5

Строка --payload=tls_client_hello значит «срабатывать только на TLS ClientHello», а --lua-desync=hostfakesplit:tcp_md5 включает функцию с fooling tcp_md5. Обе строки пишут в Zapret2/profile#Анатомия профиля — набор правил Zapret 2 для определённого трафика. Остальные готовые команды собраны в разделах #Быстрый старт и #Практические примеры. К этой схеме можно добавить несколько опций. Аргумент midhost= режет настоящее имя ещё на две части, например посередине домена второго уровня (midhost=midsld; у www.youtube.com домен второго уровня — youtube): вместо одного настоящего сегмента с именем уйдут два (#midhost — разрез внутри hostname). Аргумент disorder_after режет данные после имени ещё на две части и отправляет их в обратном порядке (#disorder_after — обратный порядок хвоста). Флаги nofake1 и nofake2 отключают первый или второй фейк, а repeats=N отправляет каждый фейк N раз (настоящие сегменты — по одному разу). Все аргументы разобраны в разделе #Полный список аргументов.

Чем hostfakesplit отличается от соседей. Zapret2/desync/multisplit#Зачем нужен multisplit только режет настоящие данные и фейков не шлёт; места разреза там задаёт аргумент pos, а у hostfakesplit pos нет: границы всегда стоят по имени. Zapret2/desync/fakedsplit#Зачем нужен fakedsplit режет по одной произвольной позиции и заливает фейки паттерном (нулями и т.п.), а hostfakesplit знает, где стоит имя, и подмешивает осмысленное фейковое имя той же длины. Zapret2/desync/fakeddisorder#Зачем нужен fakeddisorder и Zapret2/desync/multidisorder#Зачем нужен multidisorder отправляют куски в обратном порядке; у hostfakesplit обратный порядок необязательный и касается только данных после имени. Все отличия сведены в таблицу в разделе #Отличия от других функций сегментации.

Когда техника не сработает. Функция понимает только HTTP-запрос и TLS ClientHello: для QUIC (это не TCP) и других протоколов границ имени не найти, и она тихо ничего не делает. Приёма seqovl (скрытого фейка внутри сегмента, Zapret2/desync/multisplit#seqovl — скрытый фейк внутри сегмента) у неё нет. Обход удаётся, только если DPI выберет из трёх кандидатов фейк, а не настоящее имя, поэтому рабочую комбинацию для своего провайдера подбирают опытным путём и проверяют по Zapret2/verify-strategy#Короткий чек-лист честной проверки. В боевых примерах hostfakesplit ставят после Zapret2/desync/fake#Зачем нужен fake — отдельного пакета-обманки с ложным ClientHello; см. примеры #12. Комбинация: fake → hostfakesplit (два инстанса) и #13. Боевой пример для YouTube (TLS).


Оглавление


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

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

Файл: lua/zapret-antidpi.lua:707 nfqws1 эквивалент: --dpi-desync=hostfakesplit Сигнатура: function hostfakesplit(ctx, desync)

hostfakesplit — специализированная функция TCP-сегментации с замешиванием фейковых hostname-сегментов. Она предназначена исключительно для payload, содержащих имя хоста (http_req и tls_client_hello). Функция автоматически определяет границы hostname в payload, генерирует фейковый hostname той же длины (через genhost), и отправляет последовательность из реальных и фейковых TCP-сегментов. После успешной отправки выносит VERDICT_DROP, чтобы оригинальный пакет не ушёл.

[!note] Проще говоря Payload — полезные данные пакета без сетевых заголовков: сам HTTP-запрос или сам ClientHello. http_req и tls_client_hello — Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока), то есть названия распознанных протоколов. Hostname — имя сайта, то самое, которое ищет DPI. genhost — функция, которая генерирует фейковое имя нужной длины (#Генерация фейкового hostname (genhost)). VERDICT_DROP — решение «перехваченный пакет не отправлять»; как движок складывает такие решения от разных функций, описано в Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация.

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

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


Зачем нужен hostfakesplit

DPI ищет hostname в TCP-потоке: заголовок Host: в HTTP, поле SNI в TLS ClientHello. Если DPI работает попакетно (не реассемблирует TCP), достаточно разрезать payload по границам hostname. Но продвинутые DPI умеют реассемблировать поток и собрать hostname из нескольких сегментов.

hostfakesplit решает обе проблемы одновременно:

  1. Разрез по границам hostname: payload разрезается точно по маркерам host и endhost, так что ни один сегмент не содержит полный hostname в "чистом" виде
  2. Замешивание фейков: между реальными сегментами вставляются фейковые TCP-сегменты с тем же TCP sequence, но с другим hostname (сгенерированным genhost). DPI, пытающийся реассемблировать поток, может принять фейковый hostname за настоящий
  3. Fooling на фейках: фейковые сегменты отправляются с fooling-опциями (TTL, md5sig, badseq и т.д.), поэтому сервер их отбрасывает, а DPI — нет

Сервер корректно собирает поток: фейки отбрасываются благодаря fooling, реальные сегменты доставляются через стандартный TCP-механизм.

[!note] Проще говоря DPI ищет имя сайта в пакетах. Самый простой DPI разбирает каждый пакет отдельно и не склеивает их обратно в поток (склейка называется реассемблированием, от английского reassembly), поэтому его сбивает обычный разрез. Продвинутый DPI поток склеивает и может собрать имя из нескольких сегментов. Против него hostfakesplit подмешивает к настоящим сегментам фейковые с ложным именем: на одном месте потока лежат разные имена, и DPI приходится выбирать. Fooling здесь — способ испортить фейки так, чтобы сервер их выбросил; подробно — Zapret2/ts-and-fooling#Что такое fooling, короткие описания способов — Zapret2/ts-and-fooling#Как заставить fake умереть: каталог способов.

Ключевое отличие от fakedsplit: hostfakesplit автоматически знает где hostname, генерирует осмысленный фейковый hostname той же длины и имеет опцию midhost для дополнительного разреза внутри hostname. fakedsplit работает по произвольной позиции и заливает фейк паттерном (0x00 и т.п.).

Как отправляет пакеты fakedsplit и что при этом видит DPI, показано в разделе Zapret2/desync/fakedsplit#Последовательность отправки (6 пакетов). Про обычный разрез без фейков — Zapret2/desync/multisplit#Зачем нужен multisplit. Сводная таблица отличий — в разделе #Отличия от других функций сегментации.


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

Ниже четыре готовые строки для Zapret2/profile#Анатомия профиля Zapret 2, от самой короткой к более сложным. Флаг --payload=… работает как фильтр для всех вызовов --lua-desync, которые идут после него: они срабатывают только на данных указанного типа. Поэтому --payload пишут перед --lua-desync (подробно — в последовательность аргументов). Опция tcp_md5 в каждой строке — fooling для фейков (Zapret2/ts-and-fooling#Что такое fooling); без него функция вредна, см. нюанс #2. Fooling обязателен.

Минимально (fooling обязателен для фейков!):

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5

Функция срабатывает только на TLS ClientHello, режет его по границам имени сайта и вставляет два фейка с fooling tcp_md5.

С шаблоном для фейкового hostname:

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:host=google.com

Шаблон host=google.com подсказывает, на что должно быть похоже фейковое имя: например, k7z2a.google.com. Длина фейка по-прежнему равна длине настоящего имени (#Генерация фейкового hostname (genhost)).

С дополнительным разрезом посередине hostname:

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:midhost=midsld

Настоящее имя режется посередине домена второго уровня и уходит двумя сегментами (#midhost — разрез внутри hostname).

С disorder хвостовой части:

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:disorder_after

Данные после имени режутся ещё на две части и уходят в обратном порядке: сначала последний байт, потом остальное (#disorder_after — обратный порядок хвоста).


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

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

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

Следствие: маркеры host, endhost, midsld и прочие работают по тем данным, которые реально выбраны. Если задан blob=myblob, маркеры разрешатся только если myblob содержит валидный TLS ClientHello или HTTP-запрос, который zapret может распознать.

[!note] Проще говоря Функция сначала спрашивает: «мне явно подсунули blob (заготовленные данные вместо настоящих, см. blob)? — беру его». Если нет: «движок собрал запрос из нескольких пакетов (reasm)? — беру собранное». Если и этого нет: «беру тело текущего пакета». Маркеры host и endhost ищут имя в тех данных, которые выиграли этот выбор. Та же цепочка общая для большинства функций дурения: Zapret2/жизненный цикл desync-функции#Стадия 4. Выбор данных: blob → reasm → payload; на примере multisplit она разобрана в разделе Zapret2/desync/multisplit#Откуда берутся данные для нарезки.


Основные точки разреза: host и endhost

hostfakesplit не принимает произвольный pos=. Вместо этого две основные точки разреза определяются автоматически через вызов:

local pos = resolve_range(data, desync.l7payload, "host,endhost-1", true)

Это разрешает два маркера с strict=true:

  • pos[1] = host — первый байт имени хоста (начало Host: значения в HTTP, начало SNI в TLS)
  • pos[2] = endhost-1 — последний байт имени хоста (байт перед endhost)

Если хотя бы один маркер не разрешается (например, payload = unknown или quic_initial), функция логирует "host range cannot be resolved" и ничего не делает.

Таким образом: hostfakesplit работает только с http_req и tls_client_hello.

Разрешить маркер — значит превратить его название в конкретный номер байта в выбранных данных. Вот как выглядят разрешённые позиции на HTTP-запросе:

GET /watch HTTP/1.1\r\nHost: www.youtube.com\r\n...
                             ^host          ^endhost

pos[1] и pos[2] — первая и вторая позиции из результата вызова: начало имени и его последний байт. Запись endhost-1 — это Zapret2/desync/multisplit#Арифметика маркеров: «на один байт левее endhost». Какие типы бывают у payload и почему unknown или quic_initial не подходят, разобрано в заметке про Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока); все маркеры — в разделе «Zapret2/desync/multisplit#Относительные маркеры».

[!note] Проще говоря Функция сама находит начало и конец имени сайта в запросе. Если запрос не похож ни на HTTP, ни на TLS ClientHello, хотя бы один из двух маркеров не найдётся, и она ничего не сделает.


Генерация фейкового hostname (genhost)

Фейковый hostname генерируется вызовом:

fakehost = genhost(pos[2] - pos[1] + 1, desync.arg.host)
  • Длина фейка всегда равна длине реального hostname (endhost - host). Это критически важно: DPI видит hostname той же длины на том же TCP sequence, и не может отличить по размеру
  • С шаблоном (host=vk.com): генерируется случайный поддомен, например e8nzn.vk.com. Если реальный hostname короче шаблона — шаблон обрезается слева
  • Без шаблона: генерируется случайный домен с одним из стандартных TLD (com, org, net, edu, gov, biz). Если длина < 7 — случайная строка без точек

[!note] Проще говоря Фейк должен занимать ровно столько же байт, сколько настоящее имя, поэтому его собирает genhost: ей передают нужную длину и, если есть, шаблон. Шаблон — необязательная подсказка (host=vk.com): фейк становится случайным поддоменом этого домена. TLD (top-level domain, домен верхнего уровня) — последняя часть имени, вроде com или org; без шаблона функция выбирает один из стандартных. Не путайте аргумент host= (шаблон фейка) с маркером host (начало настоящего имени в запросе): названия совпадают, но это разные вещи.

Примеры генерации (шаблон google.com, реальный hostname длиной 16):

h82aj.google.com    (len=16, template="google.com")

Примеры без шаблона:

k3x.net             (len=7)
b8c54a              (len=6, без TLD)
u9a7bk2.org         (len=11)

Дополнительные точки разреза

midhost — разрез внутри hostname

midhost задает позицию для дополнительного разреза внутри реального hostname. Реальный hostname отправляется не одним сегментом, а двумя.

midhost=<posmarker>

Маркер разрешается через resolve_pos. Типичные значения: midsld, sld, endsld, host+5, и т.д.

Ограничение: разрешенная позиция midhost должна быть строго внутри hostname:

host + 1  <=  midhost  <=  endhost - 1

Если позиция выходит за эти границы (т.е. midhost <= pos[1] или midhost > pos[2]), разрез внутри hostname не происходит, и hostname отправляется одним сегментом. Функция логирует "midhost is not inside the host range".

Без midhost:

  Один сегмент: [весь реальный hostname]

С midhost=midsld:

  • сегмент 3a: от host до midhost-1 — первая часть hostname;
  • сегмент 3b: от midhost до endhost-1 — вторая часть hostname.

[!note] Проще говоря Обычно настоящее имя уходит одним сегментом. С midhost оно уходит двумя, и в каждом лежит только часть имени. Значения midsld, sld, endsld и host+5 — те же маркеры, что описаны в разделе «Zapret2/desync/multisplit#Относительные маркеры», а +5 — Zapret2/desync/multisplit#Арифметика маркеров. resolve_pos — вызов, который превращает маркер в номер байта. Если позиция выходит за пределы имени, разреза не будет, см. нюанс #5. midhost за пределами hostname молча игнорируется.

disorder_after — обратный порядок хвоста

disorder_after задает позицию для дополнительного разреза хвостовой части (всё что после hostname) и отправки двух результирующих частей в обратном порядке.

disorder_after=<posmarker>

Особый случай: если маркер — пустая строка (disorder_after без =значения, т.е. просто :disorder_after), используется маркер "-1" (последний байт payload).

disorder_after_pos = resolve_pos(data, desync.l7payload,
    desync.arg.disorder_after == "" and "-1" or desync.arg.disorder_after)

Ограничение: разрешенная позиция должна быть строго больше pos[2] + 1 (т.е. после endhost):

disorder_after_pos  >  pos[2] + 1

Если это условие не выполняется, disorder не происходит, и хвост отправляется одним сегментом в прямом порядке.

Без disorder_after:

  Один сегмент: [endhost → конец данных]

С disorder_after (порядок отправки перевернут!):

  • сначала уходит последняя часть: от disorder_after до конца данных;
  • затем средняя: от endhost до disorder_after-1.

[!note] Проще говоря «Хвост» — всё, что стоит в запросе после имени. disorder_after режет его ещё на две части и отправляет вторую раньше первой. Такой обратный порядок (Zapret2/desync/multidisorder#Обратный порядок отправки — суть disorder) может помешать DPI, который ждёт данные по порядку. Отрицательные маркеры вроде -1 и -20 отсчитываются от конца данных (Zapret2/desync/multisplit#Типы маркеров). Про пустое значение см. нюанс 6.

DPI, ожидающий данные по порядку, может не собрать хвост корректно.


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

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

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

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

[!note] Проще говоря Аргументы делятся на собственные (раздел A) и стандартные (разделы B–G): стандартные понимают и другие функции дурения, общий обзор есть в desync. Каждый аргумент отделяется от имени функции двоеточием, а флаг пишется без =значения.

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

host

  • Формат: host=<str>
  • Тип: строка (шаблон hostname)
  • По умолчанию: не задан (случайный домен со стандартным TLD)
  • Описание: Шаблон для генерации фейкового hostname через genhost. Фейк будет выглядеть как random.template, например для host=vk.com и реального hostname длиной 12 символов: e8nzn.vk.com
  • Рекомендация: задавайте домен, похожий на реальный (тот же TLD, популярный сервис), чтобы фейк выглядел правдоподобно для DPI-эвристик
  • Не путать: аргумент host= — шаблон для фейка, а маркер host — начало настоящего имени в запросе; это разные вещи с одинаковым названием
  • Примеры:
    • host=google.com — фейк вида k7z2a.google.com
    • host=vk.com — фейк вида e8nzn.vk.com
    • без host= — фейк вида r4k2m.net или b8c54a (случайный)

midhost

  • Формат: midhost=<posmarker>
  • Тип: строка (маркер позиции)
  • По умолчанию: не задан (реальный hostname отправляется одним сегментом)
  • Описание: Дополнительный разрез реального hostname на два сегмента. Маркер разрешается через resolve_pos. Должен быть строго внутри host+1..endhost-1, иначе игнорируется
  • Примеры:
    • midhost=midsld — разрез посередине SLD (самый популярный вариант)
    • midhost=sld — разрез по началу SLD
    • midhost=endsld — разрез по концу SLD
    • midhost=host+5 — 5 байт от начала hostname

nofake1

  • Формат: nofake1 (флаг, без значения)
  • Описание: Не отправлять первый фейковый сегмент (fake1, перед реальным hostname). Полезно для экспериментов: некоторые DPI реагируют только на один из фейков

nofake2

disorder_after

  • Формат: disorder_after[=<posmarker>]
  • Тип: строка (маркер позиции) или пустая строка
  • По умолчанию: не задан (хвост отправляется одним сегментом в прямом порядке)
  • Описание: Дополнительный разрез хвостовой части (после hostname) и отправка в обратном порядке. Если значение = пустая строка (:disorder_after без =...), используется маркер "-1" (последний байт payload). Позиция должна быть > endhost, иначе игнорируется
  • Примеры:
    • disorder_after — разрез по -1 (последний байт), disorder всего хвоста
    • disorder_after=-10 — разрез за 10 байт до конца payload
    • disorder_after=sniext+50 — разрез через 50 байт после начала SNI extension data

blob

  • Формат: blob=<blobName>
  • Тип: имя blob-переменной
  • По умолчанию: не задан
  • Описание: Заменить текущий payload/reasm на указанный blob. Blob должен содержать валидный HTTP-запрос или TLS ClientHello, иначе маркеры host/endhost не разрешатся
  • Подробнее: что такое blob и как его объявлять — в заметке blob; что внутри стандартных blob-ов — в разделе Zapret2/desync/fake#Стандартные blob-ы статьи про fake
  • Примеры:
    • blob=fake_default_tls — стандартный TLS-фейк
    • blob=my_custom_ch — предзагруженный blob с модифицированным ClientHello

optional

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

nodrop


B) Standard direction

Параметр Значения По умолчанию
dir in, out, any out

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

Направление говорит, в какую сторону идёт пакет: от вашего компьютера к серверу (исходящий) или обратно (входящий). При первом вызове функция отсекает себя от противоположного направления: Zapret2/жизненный цикл desync-функции#Стадия 2. Отсечение противоположного направления.

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

C) Standard payload

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

Фильтр по типу payload на уровне Lua. Это дополнительный фильтр к --payload=... на уровне профиля.

Проще говоря: тип payload — название распознанного протокола (Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока)). Основной отбор лучше делать флагом --payload=… на уровне профиля: он работает в быстром ядре программы, а аргумент payload= проверяется уже в Lua (Zapret2/жизненный цикл desync-функции#Стадия 5. Три гварда).

  • payload=known — только распознанные протоколы
  • payload=tls_client_hello,http_req — конкретные типы
  • payload=all — любой payload, включая unknown

Важно: hostfakesplit требует payload с hostname (http_req, tls_client_hello). Даже при payload=all, если payload = unknown, маркеры host/endhost не разрешатся и функция ничего не сделает. Поэтому фильтр payload здесь влияет только на то, дойдет ли код до попытки resolve_range.


D) Standard fooling

Модификации L3/L4 заголовков. В hostfakesplit fooling и repeats применяются только к фейковым сегментам. К оригиналам — только tcp_ts_up.

Fooling — намеренная порча заголовков пакета, чтобы сервер отбросил фейк, а DPI его прочитал (Zapret2/ts-and-fooling#Что такое fooling). L3/L4 — это заголовки IP (сетевой уровень) и TCP (транспортный). Таблица ниже — полный набор опций; как убивают фейк и где ломаются основные из них, описано в разделе «Zapret2/ts-and-fooling#Как заставить fake умереть: каталог способов».

Параметр Описание Пример
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 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 обязателен для hostfakesplit. Без fooling фейковые hostname будут приняты сервером, что вызовет ошибку соединения. Минимально рекомендуемый fooling: tcp_md5 или ip_ttl=1.

[!note] Проще говоря Фейки — это ложные данные. Если сервер их не отбросит, он получит два противоречивых имени на одном месте потока. Fooling делает так, что сервер выбрасывает именно фейки, а настоящие сегменты принимает: настоящим fooling не достаётся, кроме tcp_ts_up.

Заметка про tcp_ts_up: это единственная fooling-опция, которая применяется и к оригинальным сегментам. Она перемещает TCP timestamp option в начало заголовка, что улучшает совместимость с badseq-fooling на серверах с Linux TCP-стеком.

Подробнее про эту опцию — в заметке Zapret2/ts-and-fooling.


E) Standard ipid

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

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

ip_id применяется и к фейкам, и к оригиналам (через ipid = desync.arg в обоих opts).


F) Standard reconstruct

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

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

Важно: reconstruct (включая badsum) применяется только к фейкам. У оригиналов reconstruct = {} (пустая таблица). Это логично: если испортить checksum на оригинале, сервер его отбросит.


G) Standard rawsend

Rawsend — отправка готовых сегментов в сеть самой функцией. Эти параметры управляют отправкой.

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

Важно: repeats применяется только к фейкам. Оригиналы используют rawsend_opts_base, в которой repeats отсутствует. Фейки используют rawsend_opts, включающую repeats. Таким образом repeats=3 означает, что каждый фейковый сегмент отправится 3 раза, а каждый реальный — 1 раз.

ifout и fwmark применяются и к фейкам, и к оригиналам.


Порядок отправки сегментов

В этом разделе показано, в каком порядке уходят сегменты и с каким seq. Реальный сегмент несёт настоящие данные запроса и доходит до сервера, фейк несёт ложное имя и должен быть отброшен сервером. Пометки opts_orig и opts_fake — два набора опций: для реальных сегментов и для фейков (#Разделение opts: оригиналы vs фейки). Seq сегмента — место его данных в потоке: seq=host значит «с того байта, где начинается имя». Настоящие части идут слева направо (кроме случая disorder_after), а второй фейк возвращается на то же место, где стоит имя.

Базовый вариант (без midhost, без disorder_after)

!wiki-anim-z2-hostfake-base.svg

С midhost (hostname делится на две части)

!wiki-anim-z2-hostfake-midhost.svg

С disorder_after (хвост в обратном порядке)

!wiki-anim-z2-hostfake-disorder.svg

Полный вариант (midhost + disorder_after + оба фейка)

Payload:
  [before_host][host_p1][host_p2][after_1][after_2]
                ^host    ^mid     ^endhost ^disord

Отправка (7 сегментов):
  1.  [before_host]      seq=0              opts_orig
  2.  [FAKE hostname]    seq=host           opts_fake (fake1)
  3a. [host_p1]          seq=host           opts_orig
  3b. [host_p2]          seq=midhost        opts_orig
  4.  [FAKE hostname]    seq=host           opts_fake (fake2)
  5a. [after_2]          seq=disorder_after opts_orig (ПЕРВЫМ)
  5b. [after_1]          seq=endhost        opts_orig (ВТОРЫМ)

Обратите внимание на seq: у обоих фейков и у первой части настоящего имени он один и тот же (seq=host); при midhost вторая часть имени идёт со своим seq (seq=midhost).

ASCII-диаграмма: вид на уровне TCP sequence

TCP sequence (байты):
  0          host     midhost  endhost  disord   end
  |           |         |        |        |       |
  v           v         v        v        v       v
  [before_host][=hostname=area=][=after_host_area=]

Порядок отправки пакетов по времени (сверху вниз):

  t=1  |-before-|                                        seq=0          REAL
  t=2            |====FAKE_HOST====|                     seq=host       FAKE
  t=3            |--p1--|                                seq=host       REAL
  t=4                    |---p2---|                      seq=mid        REAL
  t=5            |====FAKE_HOST====|                     seq=host       FAKE
  t=6                                       |--after2--| seq=disord    REAL
  t=7                              |after1--|            seq=endhost   REAL

На схеме время идёт сверху вниз, а по горизонтали отложено место данных в потоке. Полоса FAKE_HOST покрывает то же место, что обе части имени (p1 и p2), поэтому на месте имени оказываются два фейка и настоящее имя, разрезанное на p1 и p2.

Что видит DPI: на одном и том же TCP sequence (host) приходят разные данные — реальный hostname и фейковый. DPI должен выбрать, какой из них "правильный". Если DPI выбирает фейк — обход успешен.

Что видит сервер: фейковые пакеты отброшены благодаря fooling (инвалидный TTL, md5sig, badseq и т.д.). Реальные сегменты корректно реассемблируются TCP-стеком.

[!note] Проще говоря DPI видит на месте имени три кандидата и должен угадать настоящий; сервер фейки выбрасывает и собирает запрос из настоящих сегментов. Те же две картинки на примере fakedsplit — в разделах Zapret2/desync/fakedsplit#Что видит DPI и Zapret2/desync/fakedsplit#Что видит сервер.


Разделение opts: оригиналы vs фейки

Перед отправкой функция собирает два набора настроек: opts_orig для настоящих сегментов («оригиналов») и opts_fake для фейков. Именно это различие позволяет испортить только фейки. Тот же приём использует Zapret2/desync/fakedsplit#Два набора опций: opts_orig vs opts_fake; у multisplit такого разделения нет, там fooling достаётся всем сегментам (Zapret2/desync/multisplit#6. Fooling применяется ко ВСЕМ сегментам).

Это ключевая особенность hostfakesplit (и аналогичных fake-функций):

opts_orig = {
    rawsend    = rawsend_opts_base(desync),   -- ifout, fwmark. БЕЗ repeats
    reconstruct = {},                          -- пустой (без badsum)
    ipfrag     = {},                           -- пустой (ipfrag не используется)
    ipid       = desync.arg,                   -- ip_id применяется
    fooling    = {tcp_ts_up = desync.arg.tcp_ts_up}  -- ТОЛЬКО tcp_ts_up
}

opts_fake = {
    rawsend    = rawsend_opts(desync),         -- ifout, fwmark, repeats
    reconstruct = reconstruct_opts(desync),    -- badsum
    ipfrag     = {},                           -- пустой (ipfrag не используется)
    ipid       = desync.arg,                   -- ip_id применяется
    fooling    = desync.arg                    -- ВСЕ fooling-опции
}
Что Оригиналы Фейки
fooling Только tcp_ts_up Все (ip_ttl, tcp_md5, tcp_seq, ...)
reconstruct (badsum) Нет Да
repeats Нет (всегда 1 раз) Да
ipfrag Нет Нет
ip_id Да Да
ifout, fwmark Да Да

[!note] Проще говоря Настоящие сегменты уходят почти нетронутыми: из fooling-опций им достаётся только tcp_ts_up (а ip_id, ifout и fwmark действуют на оба набора). Фейки получают весь fooling, порчу контрольной суммы (badsum) и повторы (repeats). Поле ipfrag (IP-фрагментация, см. Zapret2/desync/multisplit#F) Standard ipfrag) в обоих наборах пустое: ipfrag = {}.


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

!desync-anim-hub-reasm.svg

Многопакетный запрос nfqws2 придерживает, собирает в буфер reasm_data и перепроигрывает через desync-функции. hostfakesplit использует штатную развилку из общего скелета desync-функций (жизненный цикл desync-функции, стадия 6) без изменений: на первой части определяет по собранному буферу границы имени хоста, генерирует фейковый hostname той же длины и отправляет всю нарезку; остальные части дропает. Если отправка сорвалась, флаг «реасм уже отправлен» не ставится и оставшиеся части проходят как есть.

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

[!note] Проще говоря Если запрос не влез в один пакет, движок придерживает пакеты, собирает запрос целиком (reasm — повторная сборка) и затем проигрывает придержанные части заново (replay). hostfakesplit ищет имя во всём собранном запросе, отправляет всю нарезку на первой части, а остальные части выбрасывает. Как это устроено, описано в Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay) и в разделе Zapret2/структура desync и диссекта#Приём многопакетных пейлоадов (reasm / decrypt / replay); на примере multisplit — в разделе Zapret2/desync/multisplit#Поведение при replay / reasm.


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

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

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

Две особенности стоит держать в голове. Первая: диапазон имени хоста разрешается в строгом режиме — если маркеры host/endhost не разрешились (payload не распознан как HTTP или TLS), функция не делает ничего. Вторая: как и у fakedsplit, опции собираются в два набора — полный fooling и repeats уходят только на фейковые hostname, реальным частям достаётся лишь tcp_ts_up и ip_id. Про ipfrag комментарий в коде (перед отправкой реальных частей имени) обещает выборочность: если IP-фрагментация задана, фрагментировать только реальные части имени хоста, а фейки и всё вне имени оставлять видимыми для DPI. Но в текущей версии кода оба набора опций передают ipfrag = {}, поэтому фактически IP-фрагментации нет ни у одной части — подробно в #3. ipfrag не задействуется.

[!note] Проще говоря Стадии скелета в таблице выше разобраны в заметке про жизненный цикл: instance_cutoff — навсегда отключиться от потока, который не TCP (Zapret2/жизненный цикл desync-функции#Стадия 1. Отсев чужого транспорта и instance cutoff); гварды — три проверки перед работой (Zapret2/жизненный цикл desync-функции#Стадия 5. Три гварда); replay — работа на первой части собранного запроса (Zapret2/жизненный цикл desync-функции#Стадия 6. Развилка перепроигрывания (replay)); вердикт — решение, что делать с оригиналом (Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация). ipfrag — IP-фрагментация, деление пакета на уровне IP (Zapret2/desync/multisplit#F) Standard ipfrag).


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

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

function hostfakesplit(ctx, desync)
    -- 1. Проверка: только TCP
    if not desync.dis.tcp then
        if not desync.dis.icmp then instance_cutoff_shim() end
        return
    end

    -- 2. Cutoff противоположного направления
    direction_cutoff_opposite(ctx, desync)

    -- 3. Проверка optional blob
    if optional and blob specified and blob not exists then
        DLOG("blob not found. skipped")
        return
    end

    -- 4. Выбор данных
    data = blob_or_def(blob) or reasm_data or dis.payload

    -- 5. Проверки: данные не пусты, направление OK, payload OK
    if #data > 0 and direction_check() and payload_check() then
        if replay_first() then

            -- 6. Разрешение host и endhost (strict=true)
            pos = resolve_range(data, l7payload, "host,endhost-1", true)
            -- pos[1] = host, pos[2] = endhost-1 (последний байт hostname)

            if pos then
                -- 7. Подготовка opts
                opts_orig = { fooling = {tcp_ts_up only}, no repeats, no badsum }
                opts_fake = { fooling = all, repeats, badsum }

                -- 8. Отправка before_host (реальный)
                rawsend(data[1..host-1], seq=0, opts_orig)

                -- 9. Генерация фейкового hostname
                fakehost = genhost(endhost - host, arg.host)

                -- 10. Fake1 (если не nofake1)
                if not nofake1 then
                    rawsend(fakehost, seq=host-1, opts_fake)
                end

                -- 11. Реальный hostname (с midhost или без)
                if midhost and midhost внутри host+1..endhost-1 then
                    rawsend(data[host..midhost-1], seq=host-1, opts_orig)
                    rawsend(data[midhost..endhost-1], seq=midhost-1, opts_orig)
                else
                    rawsend(data[host..endhost-1], seq=host-1, opts_orig)
                end

                -- 12. Fake2 (если не nofake2)
                if not nofake2 then
                    rawsend(fakehost, seq=host-1, opts_fake)
                end

                -- 13. After_host (с disorder или без)
                if disorder_after and disorder_after > endhost then
                    -- ОБРАТНЫЙ порядок:
                    rawsend(data[disorder_after..end], seq=disorder_after-1, opts_orig)
                    rawsend(data[endhost..disorder_after-1], seq=endhost-1, opts_orig)
                else
                    rawsend(data[endhost..end], seq=endhost-1, opts_orig)
                end

                -- 14. Пометить как отправленное
                replay_drop_set()
                return nodrop and VERDICT_PASS or VERDICT_DROP
            else
                DLOG("host range cannot be resolved")
            end
        else
            DLOG("not acting on further replay pieces")
        end
        -- 15. Drop replayed packets если ранее успешно отправлено
        if replay_drop() then
            return nodrop and VERDICT_PASS or VERDICT_DROP
        end
    end
end

[!note] Проще говоря Псевдокод читается по порядку. Если это не TCP, функция выходит. Иначе она выбирает данные, находит границы имени и отправляет: часть до имени, первый фейк, настоящее имя (одним куском или двумя, если задан midhost), второй фейк и часть после имени (в обратном порядке, если задан disorder_after). В конце она помечает запрос отправленным и выбрасывает оригинал (при nodrop — пропускает).


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

1. Работает только с http_req и tls_client_hello

hostfakesplit требует payload с hostname. Для unknown, quic_initial и любых других типов маркеры host/endhost не разрешатся, и функция ничего не сделает. Это не ошибка — просто тихий пропуск.

[!note] Проще говоря unknown — данные, тип которых не распознан; quic_initial — первый пакет QUIC; это не TCP, а на не-TCP функция отключается (см. #Место в общем скелете). Как распознаются типы, описано в заметке про Zapret2/payload#Что такое тип payload (и чем он отличается от протокола потока).

2. Fooling обязателен

Без fooling фейковые hostname будут приняты сервером. Сервер получит два противоречивых hostname на одном TCP sequence. Результат непредсказуем: от ошибки TLS handshake до 400 Bad Request. Всегда задавайте хотя бы одну fooling-опцию (tcp_md5, ip_ttl=1, badsum, tcp_seq=-10000 и т.д.).

Каталог способов испортить фейк — в разделе «Zapret2/ts-and-fooling#Как заставить fake умереть: каталог способов».

3. ipfrag не задействуется

В отличие от multisplit и multidisorder, hostfakesplit не поддерживает IP-фрагментацию. Поля ipfrag = {} пусты и для оригиналов, и для фейков. Если вам нужна IP-фрагментация поверх hostname-разреза — придётся комбинировать с другими инструментами.

Проще говоря: IP-фрагментация — деление пакета на части уже на уровне IP, а не TCP; что это такое и какие у неё параметры, описано в разделе Zapret2/desync/multisplit#F) Standard ipfrag.

4. repeats идут только на фейки

repeats=5 означает, что каждый фейковый сегмент отправится 5 раз, а реальные — по 1 разу. Это полезно: повторение фейков увеличивает шанс, что DPI примет именно фейк, а не реальный hostname.

То же самое для fakedsplit — в нюансе Zapret2/desync/fakedsplit#6. repeats применяются ТОЛЬКО к фейкам.

5. midhost за пределами hostname молча игнорируется

Если midhost разрешается в позицию за пределами hostname (например, midhost=method для HTTP, который указывает на начало GET), разрез внутри hostname не произойдет. Функция логирует "midhost is not inside the host range" и отправляет hostname одним сегментом.

Проще говоря: маркер method указывает на начало HTTP-метода (GET), то есть далеко от имени, поэтому такой midhost разреза не даёт. Описание маркеров — в разделе «Zapret2/desync/multisplit#Относительные маркеры»; работа midhost — в разделе #midhost — разрез внутри hostname.

6. disorder_after с пустым значением = "-1"

Запись :disorder_after (без =значения) эквивалентна :disorder_after=-1. Маркер -1 разрешается в #data - 1 (предпоследний байт payload в 0-based, последний байт в 1-based). Это делает disorder хвостовой части максимально выраженным: последний байт отправляется первым, а основная часть хвоста — вторым.

Проще говоря: :disorder_after без значения означает «последний байт данных отправить первым, а всё остальное после имени — вторым».

7. Оба фейка используют один и тот же fakehost

genhost вызывается один раз, и результат (fakehost) используется и для fake1, и для fake2. Оба фейковых сегмента содержат идентичный фейковый hostname и отправляются с одинаковым TCP sequence (pos[1]-1).

Проще говоря: оба фейка одинаковые, а не два разных случайных имени. Как строится фейк, описано в разделе #Генерация фейкового hostname (genhost).

8. TCP sequence фейков = TCP sequence реального hostname

Фейки отправляются с тем же seq = pos[1] - 1, что и реальный hostname. Это означает, что DPI видит перекрывающиеся данные: на одном и том же диапазоне sequence — фейковый hostname, затем реальный, затем снова фейковый. TCP-стек сервера отбросит фейки (благодаря fooling) и примет реальный.

Проще говоря: три сегмента претендуют на одно и то же место потока — фейк, настоящее имя, фейк. Сервер оставляет настоящее. Картинка — в разделе #ASCII-диаграмма: вид на уровне TCP sequence, про seq — в разделе #Коротко и по-простому.

9. nodrop создает дублирование

С nodrop оригинальный пакет тоже будет отправлен. Сервер получит hostname дважды (из нарезанных сегментов и из оригинала). Используйте nodrop только для отладки.

Про решение «выбросить или пропустить» — Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация; то же для multisplit — Zapret2/desync/multisplit#4. nodrop создаёт дублирование.

10. Порядок инстансов важен

Если перед hostfakesplit стоит pktmod с fooling — fooling применится к диссекту, и hostfakesplit порежет модифицированный пакет. Если после hostfakesplit стоит другой инстанс — он увидит VERDICT_DROP и не получит оригинальный payload.

Уточнение из разбора вердиктов: VERDICT_DROP не обрывает цепочку. Движок проходит все инстансы профиля и складывает их вердикты (VERDICT_DROP перебивает VERDICT_PASS и VERDICT_MODIFY), так что инстансы после hostfakesplit вызываются как обычно, а дроп означает лишь то, что оригинальный пакет в итоге не уйдёт (Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация; то же — в Zapret2/desync/multisplit#7. Порядок инстансов важен и в Zapret2/desync/fakedsplit#10. Порядок инстансов важен).

Проще говоря: инстанс — одна запись --lua-desync=… в профиле, а диссект — пакет, заранее разобранный движком на поля (структура desync и диссекта). pktmod — функция, которая модифицирует диссект. Порядок инстансов задаёт порядок флагов в командной строке (последовательность аргументов); как складываются вердикты нескольких инстансов — Zapret2/жизненный цикл desync-функции#Стадия 8. Вердикт и его агрегация.


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

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

Аспект hostfakesplit fakedsplit fakeddisorder multisplit multidisorder
Точки разреза Автоматические (host/endhost) + midhost, disorder_after Одна произвольная Одна произвольная Список произвольных Список произвольных
Привязка к hostname Да (только http_req, tls_client_hello) Нет Нет Нет Нет
Фейковые сегменты 2 (фейковый hostname) До 4 (паттерн) До 4 (паттерн) Нет Нет
Содержимое фейков Осмысленный hostname (genhost) Паттерн (0x00 и т.п.) Паттерн (0x00 и т.п.) — —
disorder хвоста Опционально (disorder_after) Нет Весь порядок обратный Нет Весь порядок обратный
seqovl Нет Да (число) Да (маркер) Да (число) Да (маркер)
ipfrag Нет Нет Нет Да Да
Fooling к Только фейкам Только фейкам Только фейкам Всем сегментам Всем сегментам
repeats к Только фейкам Только фейкам Только фейкам Всем сегментам Всем сегментам

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


Миграция с nfqws1

Этот раздел для тех, кто переносит настройки из первой версии zapret (программа nfqws) во вторую (nfqws2). В первой версии техника выбиралась ключом --dpi-desync=…, а её параметры — отдельными ключами --dpi-desync-…; во второй всё это записывается аргументами одного вызова --lua-desync=hostfakesplit:….

Соответствие параметров

nfqws1 nfqws2
--dpi-desync=hostfakesplit --lua-desync=hostfakesplit
--dpi-desync-fooling=md5sig :tcp_md5
--dpi-desync-fooling=badseq :tcp_seq=-10000
--dpi-desync-fooling=ttl + --dpi-desync-ttl=N :ip_ttl=N
--dpi-desync-fake-unknown=hex :host=<template> (не прямой аналог; genhost вместо hex)
нет аналога :midhost=midsld (новая возможность nfqws2)
нет аналога :disorder_after (новая возможность nfqws2)
нет аналога :nofake1, :nofake2 (новая возможность nfqws2)

Проще говоря: режимы fooling из nfqws1 становятся аргументами функции (md5sig превращается в :tcp_md5), а midhost, disorder_after, nofake1 и nofake2 в первой версии просто не существовали.

Пример полной миграции

# nfqws1:
nfqws --dpi-desync=hostfakesplit \
  --dpi-desync-fooling=md5sig

# nfqws2 (эквивалент):
nfqws2 \
  --payload=tls_client_hello,http_req \
    --lua-desync=hostfakesplit:tcp_md5
# nfqws1: hostfakesplit + fake
nfqws --dpi-desync=fake,hostfakesplit \
  --dpi-desync-fooling=md5sig \
  --dpi-desync-fake-tls-mod=rnd,rndsni,dupsid

# nfqws2 (два инстанса):
nfqws2 \
  --payload=tls_client_hello \
    --lua-desync=fake:blob=fake_default_tls:tcp_md5:tls_mod=rnd,rndsni,dupsid \
  --payload=http_req \
    --lua-desync=fake:blob=fake_default_http:tcp_md5 \
  --payload=tls_client_hello,http_req \
    --lua-desync=hostfakesplit:tcp_md5

В nfqws1 фейк и разрез задавались одним ключом --dpi-desync=fake,hostfakesplit, в nfqws2 каждая техника становится отдельным инстансом --lua-desync.

# nfqws1: hostfakesplit + syndata
nfqws --dpi-desync=syndata,hostfakesplit \
  --dpi-desync-fooling=md5sig --wssize 1:6

# nfqws2:
nfqws2 \
  --lua-desync=wssize:wsize=1:scale=6 \
  --lua-desync=syndata \
  --payload=tls_client_hello,http_req \
    --lua-desync=hostfakesplit:tcp_md5

Третий пример переносит связку с Zapret2/desync/syndata#Зачем нужен syndata и ключом --wssize; каждый ключ nfqws1 тоже становится своим инстансом (о wssize см. desync).


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

Ниже шестнадцать готовых сочетаний, от минимального до боевых. В большинстве сначала стоит фильтр --payload=… («на каких данных срабатывать»), потом --lua-desync=hostfakesplit:…; fooling на фейках обязателен (Zapret2/ts-and-fooling#Что такое fooling).

1. Минимальный (TLS + md5sig fooling)

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5

Разрезает TLS ClientHello по границам SNI, вставляет 2 фейка с md5sig fooling.

2. HTTP + TTL fooling

--payload=http_req --lua-desync=hostfakesplit:ip_ttl=4

Разрезает HTTP-запрос по границам Host:, фейки с TTL=4 (не доживут до сервера).

TTL — ограничение числа промежуточных узлов, которые может пройти пакет; фейк с малым TTL «умирает» по дороге и до сервера не доходит (Zapret2/ts-and-fooling#Как заставить fake умереть: каталог способов).

3. С шаблоном фейкового hostname

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:host=google.com

Фейковый SNI будет выглядеть как r7k2q.google.com (длина = длина реального hostname).

4. С midhost (разрез hostname пополам)

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:midhost=midsld

Реальный hostname разрезается посередине SLD. Вместо 3 реальных сегментов — 4. DPI получает ещё более фрагментированную картину.

5. С disorder хвоста (пустое значение = "-1")

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:disorder_after

Хвостовая часть (после hostname) отправляется в обратном порядке: последний байт первым, основная часть — вторым.

6. С disorder_after по конкретной позиции

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:disorder_after=-20

Хвост разрезается за 20 байт до конца payload. Последние 20 байт идут первыми, остальная часть хвоста — вторым.

7. Полный набор: midhost + disorder_after + шаблон

--payload=tls_client_hello \
  --lua-desync=hostfakesplit:tcp_md5:host=vk.com:midhost=midsld:disorder_after

Максимальная фрагментация: hostname разрезан пополам, хвост перевернут, фейки с правдоподобным *.vk.com.

8. Только один фейк (nofake2)

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:nofake2

Отправляется только fake1 (перед реальным hostname). Полезно если DPI реагирует именно на первый hostname в потоке.

9. Только второй фейк (nofake1)

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:nofake1

Отправляется только fake2 (после реального hostname). Для DPI, которые берут последний hostname.

10. С badsum на фейках

--payload=tls_client_hello --lua-desync=hostfakesplit:badsum

Фейковые сегменты получают невалидный TCP checksum. Сервер отбросит их, DPI (который часто не проверяет checksum) примет.

11. Многократные фейки (repeats)

--payload=tls_client_hello --lua-desync=hostfakesplit:tcp_md5:repeats=5

Каждый фейковый сегмент отправляется 5 раз. Реальные — по 1 разу. Итого: 1 before_host + 5 fake1 + 1 hostname + 5 fake2 + 1 after_host = 13 пакетов.

12. Комбинация: fake → hostfakesplit (два инстанса)

--payload=tls_client_hello \
  --lua-desync=fake:blob=fake_default_tls:tcp_md5:repeats=3:tls_mod=rnd,rndsni,dupsid \
  --lua-desync=hostfakesplit:tcp_md5:midhost=midsld:host=google.com

Сначала 3 фейковых TLS ClientHello (целиком), затем реальный — нарезанный по hostname с дополнительным разрезом по midsld и фейковым hostname *.google.com.

Проще говоря: инстанс Zapret2/desync/fake#Зачем нужен fake отправляет отдельный фейковый ClientHello целиком (заготовленный blob fake_default_tls, см. Zapret2/desync/fake#Стандартные blob-ы), а tls_mod=rnd,rndsni,dupsid меняет в нём случайные поля, SNI и Session ID (Zapret2/desync/fake#Опции tls_mod).

13. Боевой пример для YouTube (TLS)

--filter-tcp=443 --hostlist=youtube.txt \
  --lua-desync=fake:blob=fake_default_tls:repeats=5:tcp_md5 \
  --lua-desync=hostfakesplit:tcp_md5:midhost=midsld:host=google.com

5 фейковых TLS ClientHello + реальный с hostname-фейками и разрезом по midsld.

Здесь --filter-tcp=443 и --hostlist=youtube.txt — фильтры профиля (порт и список доменов, filter и hostlist). Похожий боевой пример с multisplit — Zapret2/desync/fake#17. Боевая связка: fake + multisplit для YouTube.

14. HTTP + TLS в одном конфиге

--filter-tcp=80 --hostlist=blocked.txt \
  --lua-desync=hostfakesplit:ip_ttl=3 \
--filter-tcp=443 --hostlist=blocked.txt \
  --lua-desync=hostfakesplit:tcp_md5:midhost=midsld

HTTP с TTL-fooling, TLS с md5sig и разрезом hostname.

--filter-tcp=80 и --filter-tcp=443 — фильтры по порту TCP: 80 обычно занят HTTP, 443 — HTTPS (filter). Между двумя блоками в примере нет --new, которым в Zapret 2 разделяют профили (filter); как движок разберёт два --filter-tcp подряд без разделителя, статья не уточняет. Если нужны два отдельных профиля, между блоками ставят --new.

15. С blob (отправка произвольного payload)

--blob=my_ch:@custom_clienthello.bin \
--payload=tls_client_hello \
  --lua-desync=hostfakesplit:tcp_md5:blob=my_ch

Вместо реального ClientHello отправляется кастомный blob (при условии, что в нём есть валидный SNI).

--blob=my_ch:@custom_clienthello.bin объявляет blob из файла; подробности — в заметке blob.

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

--payload=tls_client_hello \
  --lua-desync=hostfakesplit:tcp_md5:blob=maybe_missing:optional

Если blob не существует — тихий пропуск, без ошибок.


📚 См. также

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

Источники: lua/zapret-antidpi.lua:695-800, lua/zapret-lib.lua:396-401 (rawsend_opts/rawsend_opts_base), lua/zapret-lib.lua:1287-1302 (genhost), docs/manual.md:4208-4246 из репозитория zapret2.


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