К контенту
habrforever
ЖИВА #dpi 2026 · ~11 мин чтения

L×Box: диагностика per-app трафика, посмотрим кто куда ходит — актуализация 2026

оригинал на Хабре habr.com/…/1033178
снимок Wayback не сохранялся
статус на Хабре (ноябрь 2026) доступна
права © автор, переиздание в архивных целях с указанием источника

L×Box: диагностика per-app трафика, посмотрим кто куда ходит

Архивное переиздание с актуализацией. Оригинал опубликован на Habr (2026), автор — неизвестен. оригинал доступен на Habr по состоянию на ноябрь 2026. Ниже — редакция ноября 2026: что работает сейчас, что изменилось и как правильно повторить рецепт сегодня. Полный текст оригинала — в конце, без купюр.

TL;DR — версия 2026

В 2026 ByeDPI-семейство (GoodbyeDPI/ByeDPI/Zapret/SpoofDPI) — способ номер один для «открыть сайт» и «вернуть скорость YouTube»: правка пакетов вместо VPN, ноль серверов, ноль подписок.

С момента выхода оригинала DPI-обход из нишевого трюка стал народным массовым решением: замедление YouTube и волны блокировок 2024–2026 привели к тому, что ByeDPI ставят даже те, кто никогда не слышал слово «VPN». ТСПУ (DPI-боксы) стоят у всех крупных российских провайдеров и умеют резать VPN-хендшейки WireGuard, OpenVPN и «голый» Shadowsocks. Изменились и сами приёмы: появились фрагментация TLS-ClientHello в обе стороны, fake-Seq, десинхронизация и обфускация Host-заголовка.

Статус оригинала

Ограничения, о которых молчат

DPI-обход не помогает, если: заблокирован IP целиком (нужен прокси/VPN), провайдер режет по «подозрительной» статистике сессии, или сайт за Cloudflare, который сам не пусит «странный» клиент. Ещё один класс проблем 2026 — ECH/TLS1.3 анти-фрагментация на стороне сервера: часть трюков умирает. Поэтому ByeDPI — инструмент «открыть страницу», а не «жить в интернете».

Семейство инструментов 2026

  • GoodbyeDPI (Windows, ValdikSS) — классика, режимы 1–9 + стратегии под конкретных провайдеров;
  • ByeDPI (кросс-платформа, форки) — lighter, есть Android-сборки, работающие как локальный VPN-сервис без root;
  • Zapret (Linux) — богатый набор DPI-трюков (nfqws, tpws), конфиги под ТСПУ конкретных операторов;
  • SpoofDPI (macOS/Linux) — «поставил и забыл»;
  • Android: ByeDPI-Mod и вариации, плюс системный «приватный DNS» DoH не забыть;
  • iOS: остаётся personal-VPN-профиль с SOCKS/DNS-трюками — слабее, чем на других платформах.

Как выбрать режим (методика из оригинала жива)

Алгоритм не изменился с оригинала: tracert до сайта → посмотреть, где теряется (пер-хоп) → определить тип блокировки (RST-инъекция, дроп ClientHello, дроп по SNI, IP-блок) → подобрать режим фрагментации. Только теперь всё это умеет автоматизировать blockcheck (скрипт ValdikSS): он сам прогоняет комбинации и печатает таблицу «что сработало».

Иллюстрации из оригинала

Иллюстрация из оригинала статьи

Что изменилось с 2026 года

  • у ТСПУ появились «мягкие» и «жёсткие» профили по провайдерам — настройки индивидуальны
  • появились Android-сборки ByeDPI, работающие без root
  • обфускация Host и фрагментация в обе стороны вместо старого one-sided
  • GitHub-репозитории периодически «прибирают» — держите локальные копии бинарников

Чек-лист 2026

  • сначала blockcheck — потом настройка
  • DoH/DoT отдельно от DPI-обхода
  • проверить и TCP, и QUIC (UDP/443) пути
  • локальная копия инструмента + конфига

Вердикт

Оригинальный лаунчер-гайд был верен: «окошко с галочками» — то, что нужно людям. В 2026 окно то же, галочек больше, а под капотом — та же идея из 2017-го: подсунуть ТСПУ пакет, который она не умеет классифицировать.


ОРИГИНАЛ СТАТЬИ (без изменений)

Недавно я дебажил проблему «Tinkoff Investments не открывается через VPN» — стандартная, split-routing проблема. Симптом: app запускается, картинки грузятся, но авторизационная сессия не поднимается, login висит. Через 30 минут возни со снапшотами /state, /connections, /logs и ручного matching’а conn_id’ов между ними картина прояснилась.

Часть доменов вида *.t-bank-app.ru корректно матчится в мой ru-domains rule_set и идёт напрямую. Но другая часть резолвится через CNAME на *.trbcdn.net (TLD .net!) — этот target в ru-domains уже не попадает, и sing-box честно отправляет трафик через bypass-VPN в Польшу. Получается split: часть запросов уходит с моего домашнего IP, часть — с польского. Bank-backend, который привязывает session к source-IP / fingerprint’у, видит непоследовательного клиента и просто отказывается поднимать auth state.

Симптом — «login завис», корень — domain-level split routing внутри одного приложения.

Стало ясно: такая диагностика не должна занимать 30 минут. Поэтому в L×Box (мой Android-клиент на sing-box, open source) появилась фича — Per-app traffic profiler.

Иллюстрация из оригинала статьи

Pick app → ▶ Record → 30 секунд работы приложения → читаешь Domains tab. Видно по строкам, что часть *.t-bank-app.ru уходит через direct-out, а другая — через vpn-1. Cовершенно очевидно где провал в правилах. Никакого packet capture, root’а, внешних tools.

Зачем это вообще

Сейчас индивидуальная и корпоративная безопасность более востребована чем ранее, пользователи хотят знать куда смотрит какое их приложение — это простая точка диагностики.

Три класса задач, которые регулярно возникают у любого пользователя split-routing VPN’а:

Дебаг роутинга. «X не работает через VPN» — flagship-кейс. Корень обычно один: домен выглядит RU, но CNAME уводит на CDN с иностранным TLD, и rule_set не отрабатывает. Без видимости CNAME-chain’а — это всегда детектив.

Privacy audit. «Куда стучит это приложение?» — список доменов отсортированный по объёму трафика. Замечаешь незнакомое — копаешься в outbound’е, при необходимости блокируешь.

Дебаг производительности. «Почему этот сервис тормозит?» — смотришь в каком outbound’е лежит трафик, есть ли DNS retry’и, не висит ли всё на одном медленном IP.

На рынке Android-VPN-клиентов никто не показывает routing chain per-app в реальном времени. PCAPDroid делает packet capture (но не видит роутинг). NetGuard блокирует (но не диагностирует). AdGuard видит DNS (но не outbound). Wireshark — это десктоп.

Преимущество в том, что у меня под капотом sing-box — единственный VPN-engine, который пишет в логи структурированные DNS-резолвы с CNAME chain’ом, package_name по conn_id и outbound chain. Эту информацию надо просто вытащить и разложить по полкам.

Как это устроено

Singleton-сервис TrafficProfiler (Dart, ChangeNotifier) держит одну active session + ring-buffer на 5 завершённых. Всё in-memory — на kill приложения сессии стираются. Persist принципиально не делается: фича diagnostic-only, не аналитическая, цена schema/migration’ов превышает value.

Два независимых источника данных:

              ┌────────────────────────────────────────┐
              │  TrafficProfiler (singleton)            │
              │   _active: Session?  _completed: Q[5]   │
              └────────────────────────────────────────┘
                     ▲                    ▲
        ┌────────────┴────────┐  ┌────────┴──────────────────┐
        │ A: log stream       │  │ B: /connections poll       │
        │  AppLog (core src)  │  │  Clash API every 2s        │
        │  ts-diff drain      │  │  diff vs prev snapshot     │
        └─────────────────────┘  └────────────────────────────┘

Источник A — sing-box log stream через native EventChannel lxbox/coreLog → AppLog (мой per-source ring-buffer для логов) → TrafficProfiler listener. Парсятся три типа строк:

INFO[N] [<conn_id> <Nms>] router: found package name: <pkg>
INFO[N] [<conn_id> <Nms>] dns: exchanged|cached <type> <name>. <ttl> IN <type> <data>
INFO[N] [<conn_id> <Nms>] dns: exchange failed for <host>: <reason>

Per-conn-id accumulator (_DnsAccumulator) собирает CNAME chain и эмитит финальное dnsResolve event’ы с исходным запрошенным domain’ом, списком CNAME hops и финальным IP.

Источник B — Clash API polling /connections каждые 2 секунды (только пока session active — battery-friendly). Diff с предыдущим snapshot’ом даёт tcpOpen / tcpClose события с bytes, chains, host, IP, port. Фильтрация по metadata.process или metadata.processPath после strip’а UID-суффикса (там приходит "ru.tinkoff.investing (10364)", picker отдаёт чистый package).

Aggregates byDomain / byIp рассчитываются on-demand при чтении (sub-tab views). Connection-issue классификатор отмечает ⚠ на event’ах — два locale-агностичных типа:

Issue

Условие

DNS timeout

sing-box лог dns: exchange failed ... — прямой engine-сигнал, не heuristic

TCP RST early

conn closed в течение 1с, ↑0 ↓0 байт — heuristic для блокировки/RST/unreachable

| Issue | Условие | | DNS timeout | sing-box лог dns: exchange failed … — прямой engine-сигнал, не heuristic |

TCP RST early conn closed в течение 1с, ↑0 ↓0 байт — heuristic для блокировки/RST/unreachable

Memory bounds — 50000 events на session или 3-часовое sliding window (что раньше). _completed ring-buffer FIFO-evict’ится при добавлении нового. Conn-id maps GC’атся по 30-секундному TTL когда map > 256 entries.

UI — 4 sub-tab’а:

Live — newest-first стрим events с цветными kind-метками (DNS/TCP/UDP) и inline-показом CNAME chain + outbound chain под каждым event’ом

Domains — aggregated по domain’у, sorted by bytes, с search-фильтром по domain || ip || cname-target. Раскрытие row’а показывает CNAME chain, все IPs, outbound, issues (⚠)

IPs — аналогично по IP. Полезен для hostless конн’ов (без SNI sniffing) и подозрительных IP из threat-feed’а

Connections — per-conn timeline с inline-expand: CNAME, all IPs, issues (⚠), кнопка [View in Domains →] с автопереходом + autofill search’а

Recording продолжается даже когда я вышел из этого экрана (singleton). На HomeScreen в traffic bar появляется красный ⚡-chip с короткой версией package name’а — индикатор; tap → возврат в Per-app tab. Останавливается только manual STOP, force-stop приложения или start новой сессии.

Грабли, на которые наступил

Из всего, что было запланировано в spec’е, больше всего времени ушло на штуки, которые не были предсказуемы из дизайна. Перечислю короткой сводкой — кто захочет деталей, найдёт в [lib/services/traffic_profiler.dart](https://github.com/Leadaxe/LxBox/blob/main/app/lib/services/traffic_profiler.dart) и §044 spec:

**UID-суффикс в ****metadata.process**. Sing-box возвращает не "ru.tinkoff.investing", а "ru.tinkoff.investing (10364)" — pid в скобках. Без strip’а ни одна conn’ция не атрибутируется к target session’у. Заодно сюрприз — metadata.process чаще null, актуальный package живёт в metadata.processPath.

Ring-buffer overflow ломает length-diff scan. Изначальный drain логов сравнивал entries.length с прошлым snapshot’ом. Мой AppLog имеет cap=500 для core source — когда busy traffic заполняет буфер, новые entries вытесняют старые, длина стабилизируется на 500, length-diff навсегда нулевой. DNS-events переставали приходить через 1-2 минуты после старта recording’а. Фикс — timestamp-diff (хранить ts последнего обработанного entry, а не его индекс).

**dns: cached**** нужно ловить наравне с ****dns: exchanged**. Sing-box логирует обе формы; у занятых apps большинство резолвов — cache-hit’ы. Изначальная regex-маска ловила только exchanged — DNS-уровень был разорванный (TCP-conn’ы есть, DNS-events нет).

Domain attribution на исходный domain, не на CNAME-target. Sing-box логирует CNAME chain последовательно: CNAME a → b, потом A b → ip. Если эмитить event с domain = именем из A-записи — в Domains tab появится b (CDN-хост), а изначально-запрошенный a исчезнет. Фикс — per-conn-id accumulator хранит первое имя, CNAME-targets копятся отдельно как chain.

Connections click target — только header. Inline-expand на conn row делал весь tile (включая раскрытую секцию) кликабельным. Тапаешь на кнопку «View in Domains» или хочешь скопировать CNAME — row схлопывается. Завернул InkWell только в header’ную часть, expanded section живёт отдельно.

Debug API — для глаз и для агентов

Все controls UI доступны через localhost HTTP API (Bearer-token authenticated, bind на 127.0.0.1, anti-rebinding host-check). Эндпоинты — /profiler/start, /profiler/stop, /profiler/active, /profiler/session/<id>?include=domains,ips,events, /profiler/sessions, плюс SSE-стрим /profiler/stream для real-time event feed’а. Полная reference — в [docs/api/debug-api-reference.md](https://github.com/Leadaxe/LxBox/blob/main/docs/api/debug-api-reference.md).

Это про себя круто, но настоящая ценность — другая. Это API не для меня, это API для агента.

Сценарий который реально работает у меня сейчас:

Втыкаю телефон в кабель, поднимаю adb forward tcp:9270 tcp:9269 (один раз — потом скрипт scripts/ensure-wifi-adb.sh восстанавливает соединение через wifi автоматически)

Открываю Cursor / Claude Code / любой coding agent в проекте L×Box

Пишу: «Tinkoff Investments login виснет, разберись» — и кидаю токен Debug API из app’а

Иду пить чай

Дальше агент сам:

Дёргает /profiler/start с package=ru.tinkoff.investing

Просит меня сделать действие в app’е (тапнуть Login)

Пуллит /profiler/session/<id>?include=domains,events

Видит split: 80% доменов через direct-out, 3 — через vpn-1 (несимметричный routing внутри одного app’а — это ровно то, что ломает auth)

Раскапывает CNAME chain в самой session JSON’е

Пишет: «Корень — *.trbcdn.net уходит на .net TLD, в ru-domains rule_set его нет, добавь suffix»

Если попросить — сам делает PR с правкой template’а через Edit-tool, прогоняет тесты

Возвращаюсь — на экране summary проблемы, ссылка на git diff, иногда уже зелёный CI. Времени потрачено — на чай.

Это особенно работает потому что вся диагностика structured: agent читает не grep по логам, а typed JSON с domain / ip / cname_chain / outbound_chain / issues полями. SSE-стрим (fire-and-forget, без Last-Event-ID reconnect — overkill для in-app single-user use case’а) даёт live-feed для агентов которые хотят не пуллить, а реактивно ловить события.

Собственно фичу Per-app traffic profiler я во многом и ради этого сделал — раньше тот же flow был «дай мне 10 файлов logcat / state / connections, я разберу», теперь — «дай адрес API и подожди».

Что не делает (пока)

✅ DNS-уровень с CNAME chain’ом — для каждого резолва записываю не только финальный IP, но и всю цепочку: cdn.t-bank-app.ru → cl-ead2c819.edgecdn.ru → 193.17.93.194. Это главное чего нет ни в одном другом Android роутере клиенте, и без чего сложные инциденты не отлавливаются.

✅ Connections с outbound chain’ом — для каждого TCP/UDP conn’а видно через что роутится: direct-out или vless-server → 🇫🇮Финляндия (vpn-1), с детуром если он есть. Split-routing-диагностика без видимости outbound’а невозможна — это и есть та симметричная половина к domain’у, ради которой я фичу делал.

✅ Connection-issue detection (DNS timeout + TCP RST early) — пассивная ⚠ подсветка на event’ах, чтобы не сканировать самому список из 50 доменов искать «что плохо». DNS timeout — прямой engine-сигнал из лога sing-box, TCP RST early — heuristic (closed <1с с 0 bytes).

✅ Process inference fallback — когда sing-box find_process не атрибутировал conn к package’у (характерно для WebView и system services), пробую attribute по prior DNS resolved IPs в окне 10 секунд, помечается 〽. Без этого для WebView-based apps вроде того же Tinkoff’а часть трассы была бы дырявая.

✅ JSON export через Share — один тап на иконку share — native Android Share intent с полным session JSON’ом (events + aggregates + meta). Закрывает workflow «записал → отправил себе/агенту/в Telegram → продолжил». Дешёвая ниточка, но без неё фича была бы dead-end.

✅ Debug API + SSE — все UI-controls доступны через localhost HTTP API + Server-Sent Events стрим. Это foundation для agent-driven диагностики (про что я выше написал) — без него фича была бы просто-ещё-один-UI-tab. Самое ценное для меня лично.

❌ HTTP-уровневые URL’ы и headers — сейчас видно только domain:port, а не GET /api/v3/auth/login, User-Agent, Authorization. Полезно было бы для full API-аудита (какой именно endpoint app дёргает, какие токены утекают). Но sing-box работает на уровне TUN/SOCKS — к нему трафик приходит уже зашифрованный TLS’ом, расшифровать без MITM-сертификата нельзя. Так что это принципиальная limit’ация архитектуры, не моя.

❌ Per-domain latency / RTT — сейчас собираю только bytes и conn count. Добавить TCP handshake timings и DNS round-trip — было бы понятно, какой CDN-нод тормозит, а не «весь сервис тупит». Sing-box частично эту информацию пишет в dns: exchanged (в Nms префиксе), TCP-handshake придётся probing’ом мерить отдельно, пока как идея чисто.

❌ Inline создание routing rules — [Block this domain] / [Add to ru-direct] кнопками прямо в Domains tab. Закрывает feedback loop: увидел проблему в trace → одним тапом сделал rule → проверил → не надо переключаться на Custom Rules screen и копировать имя домена руками. Это самое near-term — UI почти готов, осталось wire’нуть в существующий CustomRule API, но не ясно нужно ли, кстати агенты через API эту работу могут сделать без UI.

❌ TLS fingerprinting (JA3/JA4) — хеш TLS ClientHello для каждой connection. Очень полезно для дебага DPI-проблем соединений: многие провайдеры в РФ блокируют не по домену, а по JA3 (распознают что вас sing-box а не Chrome). Sing-box внутри умеет fake’ить fingerprint через outbound/uTLS (мимикрия под Chrome/Firefox/Safari), но сам fingerprint наружу через libbox API пока не expose’ится. Жду в апстриме.

Где это работает

Релиз L×Box v1.7.0 (тема цикла — «Observability»). Android 8.0+ (minSdk 26), arm64-v8a / x86_64. Sing-box find_process: true нужен в config’е (по дефолту включён в моём wizard-template’е).

Open-source: github.com/Leadaxe/LxBox. Apk’и собираются исключительно через CI/CD на GitHub, телеметрии и сетевых обращений «налево» нет. Spec — [docs/spec/features/044](https://github.com/Leadaxe/LxBox/tree/main/docs/spec/features). User guide на per-app trace — [docs/features/per-app-trace.md](https://github.com/Leadaxe/LxBox/blob/main/docs/features/per-app-trace.md).

Если у вас есть кейсы где per-app трассировка реально пригодилась бы (или наоборот — где её всё ещё не хватает), идеи по issue rule’ам / locale-агностичной geo-проверке через geoip-lookup, или просто желание помочь с UI — буду рад issue’м и pull request’ам.

Sing-box внутри уже знает кто куда ходит и как роутится. L×Box — просто интерфейс, который наконец показывает это людям, не заставляя вручную matching’ать conn_id’ы между log-снапшотами.

по теме #dpi