| .cargo | ||
| .github/workflows | ||
| crates | ||
| docs | ||
| vendor | ||
| .gitignore | ||
| burst-domains.txt | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.yml | ||
| Cross.toml | ||
| domains.txt | ||
| install.ps1 | ||
| install.sh | ||
| README.md | ||
| tcp16.json | ||
| whitelist_sni.txt | ||
"Маяк у гаснущего горизонта свободного интернета"
Сквозь цифровые сумерки. Смотритель маяка, Runni
🦀 DPI Detector (Native Rust Engine)
Нативный инструмент для анализа цензуры и блокировок трафика на чистом Rust:
- Никаких внешних зависимостей: без сторонних рантаймов, без C/C++ библиотек и системного OpenSSL — только
rustls,ringиRustCrypto. - Легковесный: статический бинарник ≈3.9 МБ (4–6 МБ на роутерных сборках, UPX — ≈1.4 МБ), потребление оперативной памяти в работе — 3–6 МБ RAM.
- Максимальная кросс-платформенность: нативная поддержка роутеров (MIPS, ARM), Windows (от Windows 7 до 11), Linux musl, macOS и Android (Termux).
- Интерактивный TUI + Batch: полноценное стрелочное меню в терминале и быстрый тихий запуск при передаче аргументов командной строки.
🚀 Быстрый запуск в 1 строку (Без установки)
🐧 Linux, macOS, роутеры Keenetic, OpenWrt & Entware
curl -fsSL https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install.sh | sh
📱 Android (Termux)
curl -fsSL https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install.sh | sh
Скрипт автоматически определит Termux, установит нативный Android-бинарник в $PREFIX/bin/dpi-detector и запустит его. Для повторного запуска достаточно ввести dpi-detector.
🪟 Windows (PowerShell)
irm https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install.ps1 | iex
📦 Готовые бинарные сборки
Все сборки полностью статичны и не требуют внешних библиотек:
| Платформа / Устройство | Архитектура | Бинарник в релизах |
|---|---|---|
| Windows 10, 11, Server | x86_64 | dpi-detector-windows-x86_64.exe |
| Windows 7, 8, Server 2008/2012 | x86_64 | dpi-detector-windows-7-x86_64.exe |
| Linux (PC, Серверы, VPS) | x86_64 musl | dpi-detector-linux-x86_64 |
| Роутеры ARM64 (Keenetic Hopper/Titan 2, RPi, OpenWrt) | aarch64 musl | dpi-detector-linux-arm64 |
| Роутеры ARMv7 (Keenetic Titan 1/Hero, Asus RT-AX58U) | armv7hf musl | dpi-detector-linux-armv7 (и -upx) |
| Роутеры MIPS LE (Keenetic Viva/Giga, MT7621, OpenWrt) | mipsel musl | dpi-detector-linux-mipsel (и -upx) |
| Роутеры MIPS BE (Atheros, Qualcomm, OpenWrt) | mips musl | dpi-detector-linux-mips (и -upx) |
| macOS Apple Silicon (M1, M2, M3, M4) | aarch64 | dpi-detector-macos-arm64 |
| macOS Intel | x86_64 | dpi-detector-macos-intel |
| Android ARM64 (Смартфоны, планшеты, Termux, ADB) | aarch64 bionic | dpi-detector-android-arm64 |
| Android ARM32 (Старые устройства, Android TV) | armv7 bionic | dpi-detector-android-armv7 |
🛠️ Установка на роутеры (Keenetic / OpenWrt / Entware)
При запуске на роутере скрипт install.sh:
- Автоматически находит директорию
/opt/bin(Entware) и устанавливает бинарник туда (сохраняется после перезагрузки). - Контролирует свободное место: если на накопителе осталось
< 7 МБ, скрипт автоматически скачивает компактную версию со сжатием UPX (~1.4 МБ вместо ~3.9 МБ), защищая роутер от переполнения Flash илиtmpfs. - Заменяет файл атомарно (
.tmp.$$->dpi-detector) с проверкой архитектуры через--version(при сбое UPX автоматически переключается на стандартный бинарник). - Поддерживает принудительный выбор версии:
DPI_UPX=1(компактная) илиDPI_UPX=0(стандартная). - Поддерживает запуск напрямую через SSH:
# Установка и запуск меню
curl -fsSL https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install.sh | sh
# Повторный запуск после установки
/opt/bin/dpi-detector
⚙️ Параметры командной строки (CLI)
Использование: dpi-detector [ОПЦИИ]
Опции:
-t, --tests <TESTS> Строка выбора тестов (например, '012', '1', '2')
--json Вывод в машиночитаемом JSON
-v, --verbose Подробное / отладочное логирование
-l, --lang <LANG> Язык интерфейса (ru, en, zh, fa, auto). По умолчанию auto
--profile <PROFILE> Региональный профиль цензуры (ru, ir, cn, global) [по умолчанию: ru]
--legend Показать легенду статусов и выйти
-p, --proxy <URL> URL SOCKS5-прокси (socks5://127.0.0.1:1080)
-c, --concurrency <N> Лимит параллельных запросов
-d, --domain <DOMAIN> Конкретные домены для проверки (флаг можно повторять: -d vk.com -d ya.ru)
-o, --output <PATH> Путь к файлу отчёта
--domains <PATH> Путь к файлу со списком доменов
--tcp16 <PATH> Путь к файлу целей TCP16
--ascii Только ASCII для старых консолей (без Unicode-глифов и рамок)
--fingerprint <PROFILE> Профиль отпечатка (rustls|firefox|chrome|safari): ClientHello, User-Agent и заголовки, преамбула HTTP/2. firefox — форма Firefox 133, chrome — Chrome 107 / Edge 99-101, safari — Safari 15.5-18.4 из curl-impersonate; все предлагают h2
--burst <N> Fingerprint/Сибирская блокировка (тест 6): соединений на домен, с задержкой 20 мс между стартами [по умолчанию: 5]
--burst-timeout <SECONDS> Тест 6: таймаут одного рукопожатия, секунды [по умолчанию: 8]
--burst-profiles <LIST> Fingerprint для теста 6: all|rustls,firefox(firefox133),chrome(chrome107),safari(safari155) [по умолчанию: all]
--burst-tls <VERSION> TLS для теста 6: 1.3+1.2 (предложение браузера, ответ должен быть 1.3)|1.3 (только 1.3)|1.2 (только 1.2) [по умолчанию: 1.3+1.2]
--burst-alpn <PROTOCOL> ALPN для теста 6: h2 (предлагает h2 с откатом на http/1.1)|http/1.1 (только http/1.1) [по умолчанию: h2]
--trace [<PATH>] Тест 6: трасса по строке на попытку (раунд, отпечаток, домен, статус, деталь, мс); без пути — в stderr, с путём — в указанный файл [по умолчанию: выключено]
-h, --help Показать справку
-V, --version Показать версию
Справка и все сообщения локализованы: dpi-detector --lang en --help, --lang zh --help, --lang fa --help. Язык интерфейса задаётся --lang, таблицы, легенда и предупреждения следуют за ним; Farsi выводится латиницей (Finglish). Статусные бейджи и имена протоколов (OK, BLOCKED, TLS RST, SNI, ClientHello) остаются латиницей в любом языке. Машинный вывод --json от языка не зависит.
Номера доступных тестов:
0— Информация о сети и системе (внешний IP, провайдер, AS, страна, тип NAT)1— Доступность DNS-серверов (проверка UDP 53, DoH Cloudflare/Google/Quad9/Yandex/AdGuard)2— Доступность сайтов (HTTP, TLS 1.2, TLS 1.3, классификация блокировок DPI)3— Доступность CDN и хостингов (тест 16 KB) (детект сброса соединений после 14–34 КБ данных)4— Поиск белых SNI (перебор рабочих SNI для заблокированных AS: цель отбирается по 16 KB-обрыву, TLS RST и TLS DROP)5— Доступность Telegram (проверка доступности и скорости датацентров DC1–DC5)6— Fingerprint/Сибирская блокировка (N накладывающихся TLS-рукопожатий на один домен выбранными отпечатками, старты через 20 мс: сколько ответили и на какой попытке включился блок)7— Легенда статусов (справка по вердиктам)
Блок о перехвате: печатается перед тестами
Когда тесты запускаются на роутере Keenetic/Netcraze с пакетом nfqws2-keenetic, отчёт может измерять блокировку через обход или мимо него — и от этого зависит, что вообще означают цифры. Перед тестами, сразу после шапки, детектор проверяет, видит ли пакет его собственный трафик, и печатает блок из двух абзацев с пустыми строками вокруг:
Обнаружен включённый nfqws2, но трафик детектора он не видит:
соединение исключено политикой доступа «nfqws».
Чтобы обход применялся и к самому роутеру, временно поставьте POLICY_EXCLUDE=1
или укажите свой POLICY_NAME, которого нет среди политик роутера, затем:
/opt/etc/init.d/S51nfqws2 restart
| когда печатается | что сообщает | что делать |
|---|---|---|
| соединение исключено политикой | политика доступа вернула нас выше очереди; так работает режим «обрабатывать только устройства политики», а роутер в неё не входит | временно POLICY_EXCLUDE=1 или своё POLICY_NAME, которого нет среди политик |
| правила на другом интерфейсе | правила висят на ISP_INTERFACE, а наш трафик уходит другим путём (туннель, второй аплинк) — в тексте видны оба интерфейса |
добавить нужный интерфейс в ISP_INTERFACE |
| порт не обрабатывается | нашего порта нет в TCP_PORTS пакета |
добавить порт в TCP_PORTS и в --filter-tcp стратегии |
| IPv6 не покрыт | наш трафик идёт по IPv6, а правила для IPv6 не ставятся: в конфиге IPV6_ENABLED=0 |
IPV6_ENABLED=1 в конфиге пакета |
| очередь не отвечает | пакет запущен, но очередь не привязана: служба не успела подняться | перезапустить службу (команда печатается в блоке) |
Когда трафик перехватывается, и когда пакета на устройстве нет вообще — не печатается ничего: nfqws2 проверяется на роутере, а не на домашнем ПК, и на Windows/macOS блоков не бывает. Список прочих обходчиков (sing-box, zapret и т.д.) остался в тесте 0, в строке «Локальный обход DPI на устройстве».
Факты берутся из состояния самого пакета: /opt/etc/nfqws2/nfqws2.conf.run (политика, интерфейсы, порты, IPV6_ENABLED), pid-файл, /proc/net/netfilter/nfnetlink_queue (очередь привязана), /proc/net/route и /proc/net/ipv6_route (куда уходит наш трафик) и /proc/net/nf_conntrack (метка собственного соединения). Счётчик пакетов очереди для вердикта не используется: на Keenetic его фоновый ход около 97 пакетов/с против наших единиц, и вердикт по нему прыгал между прогонами. Ничего не меняется, отправляются только SYN-пакеты одной-двух пробных попыток на порт 443 — по обеим семьям сразу, а в блок попадает та, которой пойдут тесты (ip_version).
🧬 Тест 6: Fingerprint / Сибирская блокировка
Тест отвечает на вопрос «блокируют ли именно мою форму TLS-рукопожатия»: на каждый домен отправляются N попыток выбранными отпечатками, старты разнесены на 20 мс, и видно, какие формы ответили, а какие нет.
Триггер, который тест воспроизводит, следит за частотой: несколько попыток соединения внутри окна в пару сотен миллисекунд. Отсюда и 20 мс между стартами. Строго по одной попытке этот порог на медленном канале не достигается вообще: интервал между стартами там равен длительности предыдущей попытки (у меня на восьми хостах это 90–400 мс, то есть «три старта подряд» занимают от 220 до 750 мс — половина уже вне окна 250 мс). Залпом всего сразу — противоположная ошибка: все N успевают установиться до того, как блок ляжет, весь раунд проходит целиком, и блок видно только в следующем прогоне. 20 мс — компромисс: пять попыток укладываются в 80 мс (внутри любого разумного окна), но блок успевает лечь между двумя стартами, поэтому в отчёте видно, на какой именно попытке он включился. Домены при этом по-прежнему идут параллельно (их ограничивает -c).
Домены у теста свои: burst-domains.txt рядом с бинарником, в сборке — realmeteo.ru, ezgame.su, scpslgame.ru, kyles.ru, amserg.com, smartape.ru, reg.ru, info.paymaster.ru. Это обычные сайты, которые на первом соединении отвечают нормально: заблокированный сайт провалит все попытки по причине, которую тест не измеряет. Список теста 2 (domains.txt) остаётся списком заблокированных сайтов и к тесту 6 отношения не имеет; -d и --domains перебивают оба.
При выборе теста 6 открывается свой экран настроек: число соединений на домен, таймаут одного рукопожатия в секундах, версия TLS (1.3+1.2 — предложение браузера, ответ должен быть 1.3; 1.3 — только 1.3; 1.2 — только 1.2), протокол HTTP в ALPN (h2 — как у браузеров, с откатом на http/1.1, или http/1.1 — только он), домен (пустое поле — весь список теста 6; вставленный URL приводится к имени хоста) и Fingerprint (по умолчанию все). Поле домена подсвечивается: серый фон — строка выбрана, синий — в поле идёт ввод. Q/Esc отменяет тест.
Попытка не заканчивается рукопожатием: сразу после него тем же профилем (и тем же User-Agent/заголовками) уходит GET / по тому протоколу, который выбрал ALPN. Попытка считается удачной, только если вернулся ответ — обрыв на запросе, редирект на чужой хост, HTTP BLOCK или таймаут чтения видны как своя строка, а не как «ответили». Редирект внутри своего сайта вердикта не меняет: основной домен, его поддомены и два поддомена одного домена — одна и та же площадка, поэтому holod.media → www.holod.media, m.holod.media → holod.media и m.youtube.com → www.youtube.com читаются как OK, а на чужой домен (www.instagram.com → www.facebook.com) — как REDIR с именем хоста в детали.
В таблице M/N по каждому отпечатку: зелёный — все попытки прошли целиком, жёлтый — часть, красный — никто. В колонке «Детали» — все вердикты строки, сгруппированные по статусу, самые частые первыми (TLS ALERT ×8 · TLS RST ×4), так что двенадцать потерянных рукопожатий не схлопываются в одну «победившую» ошибку. Номер попытки в трассе — это номер старта, а не завершения: попытки идут внахлёст, поэтому в отчёте видно, на какой именно из них включился блок (2/5 — первые два соединения ответили, дальше перестало).
Раунд одной формы на одном домене — это N накладывающихся попыток, поэтому по времени он стоит примерно как одна: последний старт отстоит от первого на (N−1) × 20 мс, и раунд заканчивается, когда завершится самая долгая попытка. При --burst 5 и таймауте 8 это ~8.1 секунды на домен, а не 40, как было бы при строгой очереди. Домены идут параллельно, поэтому общее время прогона ≈ самое медленное домен-раунд, а не сумма.
Во время прогона печатается одна живая строка Тестируем: CHROME 107 3/4 12/35 · 00:04 — какая форма сейчас в эфире, который это раунд из скольких, сколько хостов раунда готово и сколько идёт время.
Из неинтерактивного запуска настройки задаются флагами --burst, --burst-timeout, --burst-profiles, --burst-tls, --burst-alpn; в --json результат лежит в ключе results.fingerprint_burst.
Трасса попыток (--trace)
--trace [<PATH>] пишет по строке на каждую попытку — время UTC, раунд, форма, домен, номер попытки, статус, деталь и миллисекунды:
13:19:30Z r1/1 chrome www.google.com #1 tls_alert alert_unexpected_message 719 ms
13:19:30Z r1/1 chrome standby-rezka.tv #1 tls_alert alert_illegal_parameter 1031 ms
Без пути трасса идёт в stderr, с путём — в указанный файл: --trace C:\temp\trace.log и Get-Content -Wait C:\temp\trace.log во втором окне дают разбор прямо во время работы интерактивного меню. Видно, какая именно из N попыток, с какой задержкой и по какой причине не прошла — тишина до нашего таймаута, сброс за десятки миллисекунд или алерт от сервера.
Отдельно стоит знать про разбор провалившейся колонки: если попытки умирают с задержкой ровно в таймаут (8000 мс при --burst-timeout 8) у всех форм сразу, а быстрые хосты в том же раунде успевают ответить, — это провал пути, а не блокировка отпечатка. В замерах через VPN-тоннель целые раунды из 35 доменов умирали так двадцать-сорок секунд и так же сами восстанавливались, причём жертвой оказывалась то одна форма, то другая: при последовательных раундах такое окно целиком попадает в одну колонку и выглядит как её блокировка.
Профили отпечатка (--fingerprint)
Профиль задаёт три слоя сразу — форму TLS-рукопожатия, User-Agent с набором и порядком заголовков и преамбулу HTTP/2, — и все три взяты из одного набора curl-impersonate v2.2.2, поэтому клиент, выглядящий как curl_chrome107 в одном слое, выглядит так же и в остальных.
rustls(по умолчанию) — контрольная форма: никого не изображает.firefox— Firefox 133,chrome— Chrome 107 / Edge 99-101,safari— Safari 15.5–18.4.
Отличия от настоящих браузеров, о которых стоит знать при сравнении: ECH не отправляется (его синтез ломает рукопожатие с Google и Cloudflare), а accept-encoding у тестов 2–4 подменяется на identity — они считают байты до обрыва, и согласованное сжатие сделало бы эти числа зависящими от сжимаемости ответа. Тест 6 и всё, что сверяется с бандлом, шлёт accept-encoding профиля (gzip, deflate, br, у firefox ещё zstd) — ровно как curl-impersonate. Само тело ответа инструмент не распаковывает и не разбирает: считаются байты, статус и тайминги, поэтому отсутствие zstd-декодера ничего не ломает. Свой user_agent в config.yml перебивает профиль.
У профиля firefox есть ещё два, оба в телах расширений и потому невидимые для JA3/JA4 и для tls.peet.ws: список алгоритмов сжатия сертификата короче на zstd (в сборке нет его распаковщика), а долей ключа две вместо трёх — у бандла третья, P-256. Оба видны только при побайтовом сравнении с curl_firefox133.
🏗️ Сборка из исходников
Для самостоятельной сборки потребуется установленный Rust (версии 1.80+):
git clone https://github.com/Runnin4ik/dpi-detector.git -b rust
cd dpi-detector
cargo build --release -p dpi-detector
Скомпилированный бинарник будет находиться в target/release/dpi-detector (dpi-detector.exe на Windows).
Для статической сборки под Windows без внешних зависимостей CRT:
RUSTFLAGS="-C target-feature=+crt-static" cargo build --release -p dpi-detector
💬 Обратная связь
- Чат: t.me/DPI_detector
- Баги и предложения: Issues
- Лицензия: MIT