Find a file
Kirill Minovsky 9339a6b378 refactor: one home per shared helper, split the 1700-line main
A review pass (Gemini) claimed a list of duplications, a keyboard-layout bug in
the burst-settings screen and a diverged timeout classification. Verified each
against the source; this is the part that held up.

Bugs:
* Burst settings answered only Latin w/s/a/d/q. The menus answer the physical
  key on six layouts by listing each layout's characters, and this screen
  simply had no such list -- so a user in a Cyrillic, Persian, Bopomofo or
  Hebrew layout could not move the cursor there. Key handling now resolves once
  (`nav_key`: IME width, then the Latin key at the same physical position) and
  every screen uses it; the per-layout character lists are gone. The domain box
  is exempt on purpose: inside it a letter is text.
* `classify_connect_error_full` classified timeouts twice. The message-text
  branch knew the send/read stages, the OS-code branch did not, so a
  WSAETIMEDOUT (10060) while reading was reported as a bare `Timeout` instead of
  `ReadTimeout` -- on Windows, whose error text carries no "timeout". Both
  branches now return `timeout_at_stage(stage)`.

Duplication, all verified present:
* DNS A/AAAA extraction: four copies -> `DnsResponse::collect_ips`.
* hyper io::Error unwrapping: `hyper_err_info` (domains) and `hyper_info`
  (tcp16) -> one `probe::http::hyper_err_info`.
* ASN display: `asn_display_of` (core) and a copy in the run loop ->
  `Tcp16Target::display_asn`.
* config list parsing: file and embedded variants of both the domain and the
  SNI lists -> `parse_domains` / `parse_whitelist_sni`.
* box glyphs: three copies -> `render::box_chars`.
* winreg REG_SZ/REG_MULTI_SZ reads: four copies -> `netinfo::reg_strings`.

Cost: `create_verifying_tls_config` and the DoH variant rebuilt the WebPKI root
store and its verifier index on every call -- once per DoT connection and per
DoH request, ~70 times in a DNS run. Both are immutable and `Arc`-shared, so
they are built once.

Structure: main.rs 1778 -> 640 lines. `runner.rs` takes the run orchestration
(`run_test_suite` and the burst plan), `menu.rs` takes the key handling and the
post-run/legend screens, `terminal.rs` takes the Win32 console probe -- the only
`unsafe` in the crate, now alone in its file. The single-threaded runtime keeps
its `current_thread` flavour, with the reasoning (docs/OPTIMIZATIONS.md §2.5,
~50-120 KB, 1-2 core routers, auto_gate) written down at the attribute, since
the review read it as an oversight.

Verified: 107 + 34 tests, clippy --all-targets 0 warnings, a live `-t 3 --json`
run, and a pty run of the TUI -- Cyrillic keys move the menu and the burst
screen (they did not before), a full 35-domain x 4-profile test 6 run printed its
table, and the domain box still inserts Latin text without transliterating.
2026-09-13 23:47:40 +03:00
.cargo build: vendor patched rustls, h2/compression features, release-local profile 2026-09-13 19:41:43 +03:00
.github/workflows chore: release v5.0.0-alpha.8 (UPX for routers, low-space auto-selection) 2026-09-07 20:21:12 +03:00
crates refactor: one home per shared helper, split the 1700-line main 2026-09-13 23:47:40 +03:00
docs feat(i18n): localized CLI help and probe details, config and README docs 2026-09-13 19:41:52 +03:00
vendor/rustls chore: untrack the rustls build directory a cargo check left behind 2026-09-13 22:13:59 +03:00
.gitignore chore: keep run exports out of git 2026-09-13 22:32:39 +03:00
Cargo.lock perf(tls): decode compressed certificates without brotli's encoder 2026-09-13 23:21:42 +03:00
Cargo.toml perf(tls): decode compressed certificates without brotli's encoder 2026-09-13 23:21:42 +03:00
config.yml feat(fingerprint): pinned curl-impersonate versions, and probes that speak h2 2026-09-13 20:49:16 +03:00
Cross.toml fix(ci): use edge container for aarch64-linux-android and clean job names in release workflow 2026-09-05 01:38:47 +03:00
domains.txt chore: ignore the crash log, drop a dead domain from the list 2026-09-13 19:43:48 +03:00
install.ps1 release: 5.0.0-alpha.11 2026-09-13 21:50:34 +03:00
install.sh release: 5.0.0-alpha.11 2026-09-13 21:50:34 +03:00
README.md fix(test6): the domain box now shows what it will probe, and a pasted URL survives 2026-09-13 22:32:21 +03:00
tcp16.json feat: Native Rust engine (v4.2.0) 2026-09-04 00:15:58 +03:00
whitelist_sni.txt feat: Native Rust engine (v4.2.0) 2026-09-04 00:15:58 +03:00


"Маяк у гаснущего горизонта свободного интернета"
Сквозь цифровые сумерки. Смотритель маяка, Runni

🦀 DPI Detector (Native Rust Engine)

Language: Rust License: MIT Release Telegram

Полностью переписанный нативный инструмент для анализа цензуры и блокировок трафика на чистом Rust:

  • 0 зависимостей: Никакого Python, C/C++ библиотек или системного OpenSSL. Собрано на rustls, ring и RustCrypto.
  • Легковесный: Размер бинарника всего ~3.2 МБ, потребление оперативной памяти в работе — 3–6 МБ RAM.
  • Максимальная кросс-платформенность: Нативная поддержка роутеров (MIPS, ARM), Windows (от Windows 7 до 11), Linux musl и macOS.
  • Интерактивный 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:

  1. Автоматически находит директорию /opt/bin (Entware) и устанавливает бинарник туда (сохраняется после перезагрузки).
  2. Контролирует свободное место: если на накопителе осталось < 7 МБ, скрипт автоматически скачивает компактную версию со сжатием UPX (~1.4 МБ вместо ~3.9 МБ), защищая роутер от переполнения Flash или tmpfs.
  3. Заменяет файл атомарно (.tmp.$$ -> dpi-detector) с проверкой архитектуры через --version (при сбое UPX автоматически переключается на стандартный бинарник).
  4. Поддерживает принудительный выбор версии: DPI_UPX=1 (компактная) или DPI_UPX=0 (стандартная).
  5. Поддерживает запуск напрямую через 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' или "1,2,3")
      --json                   Вывод в машиночитаемом JSON
  -v, --verbose                Подробное / отладочное логирование
  -l, --lang <LANG>            Язык интерфейса (ru, en, zh, fa, auto). По умолчанию auto
      --profile <PROFILE>      Региональный профиль цензуры (ru, ir, cn, global)
      --legend                 Показать легенду статусов и выйти
  -p, --proxy <URL>            URL SOCKS5-прокси (socks5://127.0.0.1:1080)
  -c, --concurrency <N>        Лимит параллельных запросов
  -d, --domain <DOMAIN>        Тестируемый домен (флаг можно повторять)
  -o, --output <PATH>          Путь к файлу отчёта
      --domains <PATH>         Путь к файлу со списком доменов
      --tcp16 <PATH>           Путь к файлу целей TCP16
      --ascii                  Только ASCII для старых консолей (без Unicode-глифов и рамок)
      --fingerprint <PROFILE>  Профиль отпечатка TLS ClientHello (rustls|custom|chrome|safari). custom — форма Firefox 133, chrome — Chrome 107 / Edge 99-101, safari — Safari 15.5-18.4 из curl-impersonate; все предлагают h2
      --burst <N>              Fingerprint/Сибирская блокировка (тест 6): одновременных запросов за раунд [по умолчанию: 4]
      --burst-timeout <SECONDS>  Тест 6: таймаут одного рукопожатия, секунды [по умолчанию: 8]
      --burst-profiles <LIST>  Fingerprint для теста 6: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [по умолчанию: all]
      --burst-tls <VERSION>    Версия TLS для теста 6: 1.2|1.3 [по умолчанию: 1.3]
      --burst-alpn <PROTOCOL>  ALPN для теста 6: h2 (предлагает h2 с откатом на http/1.1)|http/1.1 (только http/1.1) [по умолчанию: h2]
  -h, --help                   Показать справку
  -V, --version                Показать версию

Справка и все сообщения локализованы: dpi-detector --lang en --help, --lang zh --help, --lang fa --help. Любой другой текст интерфейса - таблицы, легенда, предупреждения - тоже следует за --lang; --json не зависит от языка (ключи и значения detail остаются каноническими).

Локализация (crates/dpi-core/src/i18n)

  • Messages - по одному полю на строку, четыре блока (En, Ru, Zh, Fa); шаблоны используют {}.
  • legend_sections*() - таблицы --legend; details.rs - перевод канонических деталей DET_* (русский - исходный язык, выводится как есть).
  • Язык Farsi рендерится Finglish (латиница, без персидской графики и диакритики).
  • Статусные бейджи и протоколы (OK, BLOCKED, TLS RST, SNI, ClientHello) не переводятся никогда.

Номера доступных тестов:

  • 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-рукопожатий на один домен выбранными отпечатками: сколько ответили и что именно перестало отвечать)
  • 7 — Легенда статусов (справка по вердиктам)

Тест 6: Fingerprint/Сибирская блокировка

При выборе теста 6 открывается свой экран настроек: число одновременных рукопожатий, таймаут одного рукопожатия в секундах, версия TLS (1.2/1.3), протокол HTTP в ALPN (h2 — как у браузеров, с откатом на http/1.1, или http/1.1 — только он), домен (в поле показан хост, который проверял прошлый прогон; первый набранный символ заменяет его, а пустое поле — весь список, как в тесте 2; вставленный URL приводится к имени хоста тем же очистителем, что и список из конфига) и Fingerprint (по умолчанию все). Поле домена подсвечивается: серый фон — строка выбрана, синий — в поле идёт ввод. Q/Esc отменяет тест.

На каждый домен и отпечаток выполняется N одновременных рукопожатий выбранной версии TLS — отдельные TCP-соединения, свежие сессии без resumption. Две оси дают 4 комбинации, и они измеряют разные вещи: 1.3 против 1.2 — реакцию на версию протокола, h2 против http/1.1 — реакцию на ALPN (некоторые триггеры срабатывают именно на h2-хелло; JA4 при этом меняет поле ALPN, h2 → h1). В таблице M/N по каждому отпечатку (зелёный — ответили все, жёлтый — часть, красный — никто), в колонке «Детали» — самый частый вердикт отказа (TLS RST, TLS DROP, SYN DROP, …). Тест идёт последним: серия рукопожатий может «сжечь» цель на время, и измерения после неё недействительны.

Во время прогона печатается одна живая строка Тестируем: CHROME 107 3/4 12/35 · 00:04 — какая форма сейчас в эфире, который это раунд из скольких, сколько хостов раунда готово и сколько идёт время. Отдельной шапки с настройками и блок «Итог» тест не выводит: отпечаток с версией и так виден в колонках таблицы, а пустой итог не рисуется.

Из неинтерактивного запуска настройки задаются флагами --burst, --burst-timeout, --burst-profiles, --burst-tls, --burst-alpn; в --json результат лежит в аддитивном ключе results.fingerprint_burst (вместе с tls и alpn этого прогона).


🏗️ Сборка из исходников

Для самостоятельной сборки потребуется установленный 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