Как я починил себе YouTube и Discord на Windows, Linux и macOS
TL;DR: взял идеи из открытого DPI-инструментария (zapret, GoodbyeDPI, byedpi), выкинул C, переписал на Rust как один кросс-платформенный workspace под Windows + Linux + macOS, и в процессе обнаружил, что мой собственный «дефолтный» профиль не пробивает мой собственный провайдер. Пришлось писать sweep-харнес который перебирает 89 комбинаций (decoy × fooling × repeats × strategy) и тыкается curl-ом в discord.com и youtube.com. Один-единственный конфиг сработал. И это был не тот, который я закладывал по памяти из мануалов.
Предыстория
В январе у меня был UnblockPro — гибрид GoodbyeDPI и собственного шаперона, под Windows. Работало. Потом я портировал его на macOS — отдельный проект, отдельный код, отдельный набор багов. Когда захотел Linux, понял, что повторять третий раз пайплайн «парс TLS — modify — emit» это уже самонаказание.
Решил собрать всё заново. Один Rust-workspace, четыре крейта:
crates/
├── sonicdpi-engine/ ← платформенно-независимый "мозг"
├── sonicdpi-platform/ ← перехват пакетов на каждой ОС
├── sonicdpi-cli/ ← бинарь sonicdpi
└── sonicdpi-tray/ ← бинарь с системным треем
Правило: engine ничего не знает про WinDivert / NFQUEUE / pf, platform ничего не знает про стратегии. Если в engine появляется cfg(target_os = ...) — я не туда что-то засунул.
Перехват:
ОС | Backend | Особенности |
|---|---|---|
Windows | WinDivert 2.2.2 (vendored, EV-signed) | Требует админ; WinDivert.dll рядом с .exe |
Linux | NFQUEUE через чистый Rust |
|
macOS | pf | Только TCP в v0.2 — |
Лицензионная заметка к слову: WinDivert — LGPL-3, остальное у меня MIT/Apache-2. Динамическая линковка через DLL держит остальной код permissive. cargo-deny на это пушит палец, но этот договорняк описан в THIRD_PARTY_LICENSES.md.
Стратегии — что в итоге
Открытый DPI-инструментарий описывает примерно десять разных техник десинка пакетов. Я прошёл по их каталогу и реализовал пять:
tls-multisplit— режу ClientHello на куски с seqovl-перекрытием и пихаю в overlay-регион байты «безопасного» SNI вроде4pda.to. DPI пересобирает first-write-wins, видит безопасный байт-шаблон и не палит реальный SNI.fake-multidisorder— N штук фейковых ClientHello уходят впереди настоящего, с TCP-fooling (TS=1 для PAWS, MD5SIG, или badseq). DPI коммитится на фейковый SNI, реальный CH идёт следом и проходит.hostfakesplit— переименовываю SNI в первом сегменте наwww.google.com, реальный пакет улетает следом.quic-fake-initial— то же самое для YouTube QUIC: бёрст фейковых Initial с SNI=google перед настоящим.discord-voice-prime— UDP-фейки в форме STUN / Discord-RTP перед настоящим голосом, чтобы классификатор не успел навесить ярлык.
В коде каждая стратегия — это impl Strategy { fn apply(...) -> Option<Action> }, и они складываются в StrategyPipeline. Профиль (TOML) описывает, какие из них подключать и с какими параметрами.
Дефолтный профиль я взял по памяти — «zapret ALT9 же, decoy ozon.ru, fooling ts, repeats 4, всё работает». Закоммитил, собрал, поставил, запустил.
И тут Discord перестал открываться
Точнее — TCP-коннект до gateway.discord.gg:443 идёт, а TLS handshake умирает в таймаут. Curl даёт честный code=000 time=5.0s. YouTube — работает. Discord — нет.
Ладно. Открыл лог. Стратегии fire-ятся. Декои улетают. Пакетов до Cloudflare-узла Discord приходит «достаточно». А handshake всё равно дохнет.
Полез в проблему по своему же diag-скрипту:
.\diag.ps1
(он у меня лежит в репе — diag.ps1, делает DNS-чек, tracert, запускает энжин в фоне, curl-ит несколько хостов, парсит лог. Когда что-то ломается — первая остановка.)
Результат:
PHASE 0 — engine OFF baseline
discord.com: 000 (4.0s)
gateway.discord.gg: 000 (4.0s)
www.youtube.com: 000 (4.0s)
HTTPS PROBE WHILE ENGINE RUNNING
https://discord.com/api/v9/gateway code=000 time=5.0s ← все ещё мертво
https://gateway.discord.gg/ code=000 time=5.0s
https://dl.discordapp.net/... (висит, тянется до timeout)
То есть ни мой default, ни три ALT-профиля, ни даже встроенный sonicdpi probe (у меня есть subcommand, прогоняет все билтины) — ничего не отрабатывает. Всё LOSS read.
probing gateway.discord.gg:443 with 5 profile(s)
youtube-discord LOSS read
youtube-discord-aggressive LOSS read
youtube-discord-multidisorder LOSS read
youtube-discord-seqovl LOSS read
youtube-discord-hostfakesplit LOSS read
Красивая такая колонка из пяти неудач.
А вот сейчас интересно
Посыпался когнитивный диссонанс. У меня в C:\Users\Sonic\Desktop\2\ лежит фолдер с конфигами zapret/Flowseal — general (ALT9).bat, его я конкретно использовал и в этот момент он у меня работает. Но мой код, который по памяти реализует тот же ALT9, не работает.
Начал ставить точки по логу.
Что я закоммитил в дефолтный профиль:
host_fake_split: vec![
HostFakeSplitCfg {
fake_host: "www.google.com",
targets: [YouTube, DiscordGateway],
repeats: 4,
fooling: "ts",
},
HostFakeSplitCfg {
fake_host: "ozon.ru",
targets: [DiscordCloudflare],
repeats: 4,
fooling: "ts,md5sig",
},
],
Лог говорит — flow classified by SNI/QUIC/RTP target=DiscordCloudflare. Класификатор работает. А вот fire-ов hostfakesplit нет. Совсем. Стратегия не срабатывает.
Открываю apply() в HostFakeSplit:
fn apply(&self, pkt: &mut Packet, flow: &mut Flow) -> Option<Action> {
if flow.hello_seen || pkt.proto != L4Proto::Tcp { return None; }
if !is_client_hello(pkt.payload()) { return None; }
// ...
let renamed = rewrite_sni_same_length(payload, &self.fake_host)?;
// ^^^^^^^^^^^^^^^^^^^^^^^
И вот оно. rewrite_sni_same_length — функция Option<Vec<u8>>, требует чтобы новый SNI был той же длины, что и оригинальный. discord.com это 11 байт. ozon.ru это 7 байт. Mismatch → None → стратегия молча возвращает None, flow.hello_seen остаётся false, никаких декоев не уходит, реальный CH идёт DPI-узлу провайдера без фольги.
Пять минут разглядывания собственного коммита, и я понял, что моя реализация hostfakesplit вообще не делает то, что я думал. Канонический подход эмитит N штук самодостаточных фейковых ClientHello (с разной TLS-структурой, со своим SNI, своей длиной), а у меня попытка переписать оригинал поверх существующего payload-а с теми же длинами.
Перепишу. Сделал чтобы стратегия принимала готовый fake_payload: Vec<u8> (через embedded_or_synth_ch(fake_host)), а не пыталась мутировать оригинал. Длины разные? Похуй, мы вообще не зависим от длины оригинала.
После фикса — снова всё ломается
Соник, что ты сделал? Запускаю probe повторно — те же LOSS read на всех пяти профилях. Что-то более глубокое.
Тут я понял, что догадки кончились. Память про «ALT9 = ts fooling, ozon decoy, repeats 4» — это снимок состояния на октябрь. На календаре май. DPI-логика провайдеров за это время патчится много раз. И эмпирика — единственный способ узнать, какая комбинация сейчас работает на моём провайдере.
Написал sweep.ps1. Логика прямая:
Дампим базовый профиль для секции
[targets].Генерим N TOML-файлов, каждый — отдельная комбинация.
Для каждого: глушим старый engine, стартуем новый с этим TOML, спим 2 секунды (WinDivert поднимается), curl-им четыре хоста, ловим http_code и time, глушим engine.
Финал — таблица «config X / N out of M успешных curl-ов / средний RTT».
Матрица:
host_fake_split × {ozon.ru, vk.me, max.ru, www.google.com, 4pda.to, dzen.ru}
× {ts, md5sig, ts+md5sig, badseq, ttl=4}
× {repeats=4, repeats=6}
fake_multisplit × 4 декоя × 3 fooling-а × repeats=4
fake_multidisorder× 4 декоя × 3 fooling-а × repeats=4
tls_multisplit × 4 варианта seqovl/decoy
default-current × 1
─────────────────────────────────────
= 89 конфигов
89 × (engine spawn 2s + 4 curl-а × ~1s + остановка) ≈ 27 минут.
Запустил. Пошёл сделать чай.
Вывод
Когда вернулся:
[ 75/89] fmd_ozon.ru_ts_md5sig_r4 0/4 ╳
[ 76/89] fmd_ozon.ru_badseq_r4 4/4 ✓✓✓✓
[ 77/89] fmd_vk.me_ts_r4 0/4 ╳
[ 78/89] fmd_vk.me_ts_md5sig_r4 0/4 ╳
[ 79/89] fmd_vk.me_badseq_r4 4/4 ✓✓✓✓
[ 80/89] fmd_max.ru_ts_r4 0/4 ╳
[ 81/89] fmd_max.ru_ts_md5sig_r4 0/4 ╳
[ 82/89] fmd_max.ru_badseq_r4 4/4 ✓✓✓✓
[ 83/89] fmd_4pda.to_ts_r4 0/4 ╳
[ 84/89] fmd_4pda.to_ts_md5sig_r4 0/4 ╳
[ 85/89] fmd_4pda.to_badseq_r4 4/4 ✓✓✓✓
Из 89 конфигов четыре дали 4/4. И все четыре — это fake_multidisorder с fooling = "badseq". Декой не важен (любой подходил), repeats=4, всё.
Ни один host_fake_split не зашёл. Ни один tls_multisplit. Ни один fake_multisplit. И — внимание — fake_multidisorder с fooling ts или ts,md5sig (то, что считалось каноном пол-года назад) тоже не работает. Только badseq.
Победный конфиг:
[strategies.fake_multidisorder]
fooling = "badseq"
repeats = 4
decoy_host = "ozon.ru"
[strategies.quic_fake_initial]
repeats = 6
decoy_host = "www.google.com"
=== TOP 15 ===
fmd_ozon.ru_badseq_r4 4/4 discord=200/0.08s | gateway=404/0.22s | dl=301/0.06s | www=200/0.28s
fmd_4pda.to_badseq_r4 4/4 ...
fmd_max.ru_badseq_r4 4/4 ...
fmd_vk.me_badseq_r4 4/4 ...
hfs_dzen.ru_badseq_r4 0/4
fms_max.ru_ttl_4_r4 0/4
... (84 нуля)
Почему именно badseq
Объяснение, которое я в результате склеил: DPI-узел на моём провайдере (за последний апдейт?) перестал дропать декои, у которых TCP-options содержат TSval=1 (это PAWS-trigger — нормальный TCP-стек обязан их отбрасывать как «вне-таймстамповые»). Раньше PAWS делал работу за тебя: декой долетал до сервера, сервер его молча выкидывал на L4, всё чисто. Сейчас декой долетает, проходит TCP, попадает в TLS-стек сервера, сервер шлёт TLS Alert / RST на разрыв, и этот RST убивает реальный коннект — потому что для DPI и для нашего ядра это пакеты от того же 5-tuple.
badseq — это TCP seq «вне окна». Для сервера такой пакет невидим (он его дропает на L4 как retransmit или garbage), но для DPI — это всё ещё TLS-byteshape с правильным SNI. То есть DPI его коммитит как «начало флоу к ozon.ru», а сервер его не видит и не отвечает на него ничем. Реальный CH летит за ним нетронутый.
Проще говоря: PAWS-фольга больше не fire-and-forget. badseq — fire-and-forget.
Я это записал себе в заметки (рецепт сегодня работает, а через две недели может развалиться). Если кто-то читает это в августе, и badseq уже не вывозит — sweep.ps1 в репе, прогон 25 минут, найдёт следующее.
Что в итоге получилось
Готовые сборки на каждую ОС в Releases:
SonicDPI-windows-x64.zip— внутриsonicdpi-tray.exe(двойной клик → UAC → значок в трее)SonicDPI-linux-x64.tar.gz/arm64SonicDPI-macos-x64.tar.gz/arm64(Apple Silicon)
CI собирает всё через GitHub Actions, fmt + clippy + test зелёные на всех трёх ОС. Для тех, кто хочет поковырять — там же sweep.ps1 (мой харнес) и diag.ps1 (быстрая диагностика «у меня сломалось»). И, что мне приятно — sonicdpi probe -H gateway.discord.gg -t 5 прогоняет все профили против хоста и печатает Wilson-ranking, можно дёргать без полного запуска.
Ещё один тулинг-кусок: на Windows sonicdpi.exe install --profile youtube-discord ставит как службу (через winsvc), Linux — генерит systemd unit, macOS — launchd plist. Установил один раз, забыл.
Что дальше
v0.3 — macOS NetworkExtension System Extension для голоса Discord (TCP уже работает, голос — нет).
v0.4 — adaptive probing harness внутри самого engine: ротация профилей при просадке.
v0.5 — нормальный Tauri-GUI. Сейчас UI — это только тей. Полноценное окно для не-CLI-пользователей.
Если кто-то хочет помочь — issues и PR-ы открыты. Особенно интересны полевые данные: какой провайдер, какой регион, какой профиль зашёл. У меня нет инфраструктуры чтобы тестить со ста разных ISP, и ваш sweep.ps1-вывод — это золото.
И последнее: если у вас «у меня sonicdpi не работает» — diag.ps1 в корне репо, прогон 30 секунд, кидайте вывод в issue. Это сильно сокращает мне дорогу до фикса.
Ссылки:
релизы: /releases/latest
мои предыдущие посты: habr.com/ru/users/by-sonic