mirror of
https://github.com/Runnin4ik/dpi-detector.git
synced 2026-10-19 01:20:05 +00:00
feat(ui): test 6 is Fingerprint/Сибирская блокировка, with a live line and no summary
* Tests 6 and 7 swap places: the fingerprint burst is 6, the status legend 7.
* The settings screen says "Таймаут в секундах", calls the profile row
"Fingerprint", and names profiles with the version they reproduce
("CHROME 107", not "CHROME") — the same labels the table headers and the CLI
warnings now use. `token()` stays version-less because it is the round-trip
name the parsers accept back; `display_label()` is the display form.
* The domain field paints its own state: a grey background while the row is
only selected, blue while it is being typed into, brackets and pad included.
The five colors are the 16-color set on purpose — the legacy console
translator maps exactly those to SetConsoleTextAttribute.
* The burst no longer prints a header block of settings, and it no longer
prints an Итог row: what it is doing is one live line
(`Тестируем: CHROME 107 3/4 12/35 · 00:04`) fed by a `BurstObserver` the
library calls once per profile round and once per finished host. An empty
summary panel was already suppressed by `render_summary`.
* `standby-rezka.tv` (domains.txt:19, embedded in the binary) turned out to be
the host that shows why the GREASE key share matters: see the vendor README.
This commit is contained in:
parent
e9894df7ad
commit
e6ae7db583
9 changed files with 317 additions and 173 deletions
19
README.md
19
README.md
|
|
@ -103,11 +103,11 @@ curl -fsSL https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install
|
|||
--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> Тест 7: одновременных рукопожатий за раунд [по умолчанию: 4]
|
||||
--burst-timeout <SECONDS> Тест 7: таймаут одного рукопожатия, секунды [по умолчанию: 8]
|
||||
--burst-profiles <LIST> Отпечатки для теста 7: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [по умолчанию: all]
|
||||
--burst-tls <VERSION> Версия TLS для теста 7: 1.2|1.3 [по умолчанию: 1.3]
|
||||
--burst-alpn <PROTOCOL> ALPN для теста 7: h2 (предлагает h2 с откатом на http/1.1)|http/1.1 (только http/1.1) [по умолчанию: 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 Показать версию
|
||||
```
|
||||
|
|
@ -128,14 +128,17 @@ curl -fsSL https://raw.githubusercontent.com/Runnin4ik/dpi-detector/rust/install
|
|||
* `3` — **Доступность CDN и хостингов (тест 16 KB)** (детект сброса соединений после 14–34 КБ данных)
|
||||
* `4` — **Поиск белых SNI** (перебор рабочих SNI для заблокированных AS: цель отбирается по 16 KB-обрыву, TLS RST и TLS DROP)
|
||||
* `5` — **Доступность Telegram** (проверка доступности и скорости датацентров DC1–DC5)
|
||||
* `7` — **Стресс отпечатка (burst)** (N одновременных TLS-рукопожатий на один домен выбранными отпечатками: сколько ответили и что именно перестало отвечать)
|
||||
* `6` — **Fingerprint/Сибирская блокировка** (N одновременных TLS-рукопожатий на один домен выбранными отпечатками: сколько ответили и что именно перестало отвечать)
|
||||
* `7` — **Легенда статусов** (справка по вердиктам)
|
||||
|
||||
### Тест 7: стресс отпечатка
|
||||
### Тест 6: Fingerprint/Сибирская блокировка
|
||||
|
||||
При выборе теста `7` открывается свой экран настроек: число одновременных рукопожатий, таймаут одного рукопожатия, **версия TLS** (`1.2`/`1.3`), **протокол HTTP в ALPN** (`h2` — как у браузеров, с откатом на `http/1.1`, или `http/1.1` — только он), домен (пусто — весь список, как в тесте `2`) и отпечатки (по умолчанию все). `Q`/`Esc` отменяет тест.
|
||||
При выборе теста `6` открывается свой экран настроек: число одновременных рукопожатий, таймаут одного рукопожатия **в секундах**, **версия TLS** (`1.2`/`1.3`), **протокол HTTP в ALPN** (`h2` — как у браузеров, с откатом на `http/1.1`, или `http/1.1` — только он), домен (пусто — весь список, как в тесте `2`) и **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` этого прогона).
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -166,10 +166,9 @@ pub struct Messages {
|
|||
pub burst_field_http: &'static str,
|
||||
pub burst_field_profiles: &'static str,
|
||||
pub burst_profiles_all: &'static str,
|
||||
pub burst_title: &'static str,
|
||||
pub burst_attempts_label: &'static str,
|
||||
pub burst_summary_label: &'static str,
|
||||
pub burst_summary_value: &'static str,
|
||||
/// Word in front of the profile the burst is firing right now
|
||||
/// (`Тестируем: CHROME 107 2/4`).
|
||||
pub burst_testing: &'static str,
|
||||
pub fingerprint_label: &'static str,
|
||||
pub fingerprint_note: &'static str,
|
||||
pub lang: Language,
|
||||
|
|
@ -398,7 +397,8 @@ impl Messages {
|
|||
'3' => self.menu_test_tcp,
|
||||
'4' => self.menu_test_sni,
|
||||
'5' => self.menu_test_telegram,
|
||||
'6' => self.menu_test_legend,
|
||||
'6' => self.menu_test_burst,
|
||||
'7' => self.menu_test_legend,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
|
@ -443,21 +443,18 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
menu_test_sni: "Whitelist SNI discovery",
|
||||
menu_test_telegram: "Telegram availability",
|
||||
menu_test_legend: "Status legend (help)",
|
||||
menu_test_burst: "Fingerprint stress (burst)",
|
||||
burst_settings_title: "Test 7 settings",
|
||||
menu_test_burst: "Fingerprint/Siberian blocking",
|
||||
burst_settings_title: "Test 6 settings",
|
||||
burst_field_attempts: "Simultaneous requests",
|
||||
burst_field_timeout: "Timeout, s",
|
||||
burst_field_timeout: "Timeout in seconds",
|
||||
burst_field_domain: "Domain to test",
|
||||
burst_domain_placeholder: "Press → to start typing",
|
||||
burst_domain_default_hint: "By default — all domains",
|
||||
burst_field_tls: "TLS version",
|
||||
burst_field_http: "HTTP protocol",
|
||||
burst_field_profiles: "Fingerprints",
|
||||
burst_field_profiles: "Fingerprint",
|
||||
burst_profiles_all: "all",
|
||||
burst_title: "Simultaneous handshakes (fingerprint stress)",
|
||||
burst_attempts_label: "Requests at once",
|
||||
burst_summary_label: "Stress",
|
||||
burst_summary_value: "{} of {} handshakes answered · hosts with losses: {}",
|
||||
burst_testing: "Testing",
|
||||
fingerprint_label: "Fingerprint",
|
||||
fingerprint_note: "Each profile reproduces one pinned curl-impersonate shape: FIREFOX = firefox133, CHROME = chrome107 (also edge 99-101), SAFARI = safari155. All offer h2 and http/1.1 as browsers do. None is byte-for-byte a real browser — deeper fingerprinting (HTTP/2 settings, record timing) can still distinguish them.",
|
||||
lang: Language::En,
|
||||
|
|
@ -633,11 +630,11 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
cli_concurrency: "Concurrency limit for parallel requests",
|
||||
cli_domain: "Specific domain(s) to test (repeat the flag: -d vk.com -d ya.ru)",
|
||||
cli_output: "Output file path to save report",
|
||||
cli_burst: "Fingerprint stress (test 7): simultaneous requests per round [default: 4]",
|
||||
cli_burst_timeout: "Test 7: timeout of one handshake, seconds [default: 8]",
|
||||
cli_burst_profiles: "Test 7 fingerprints: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [default: all]",
|
||||
cli_burst_tls: "Test 7 TLS version: 1.2|1.3 [default: 1.3]",
|
||||
cli_burst_alpn: "Test 7 ALPN: h2 (offers h2 with the http/1.1 fallback)|http/1.1 (offers http/1.1 only) [default: h2]",
|
||||
cli_burst: "Fingerprint stress (test 6): simultaneous requests per round [default: 4]",
|
||||
cli_burst_timeout: "Test 6: timeout of one handshake, seconds [default: 8]",
|
||||
cli_burst_profiles: "Test 6 fingerprints: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [default: all]",
|
||||
cli_burst_tls: "Test 6 TLS version: 1.2|1.3 [default: 1.3]",
|
||||
cli_burst_alpn: "Test 6 ALPN: h2 (offers h2 with the http/1.1 fallback)|http/1.1 (offers http/1.1 only) [default: h2]",
|
||||
cli_domains: "Path to custom domain list file",
|
||||
cli_tcp16: "Path to custom TCP16 target file",
|
||||
cli_ascii: "ASCII-only output for legacy consoles (no Unicode glyphs or borders)",
|
||||
|
|
@ -680,22 +677,19 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
menu_test_sni: "Поиск белых SNI",
|
||||
menu_test_telegram: "Доступность Telegram",
|
||||
menu_test_legend: "Легенда статусов (справка)",
|
||||
menu_test_burst: "Стресс отпечатка (burst)",
|
||||
burst_settings_title: "Настройки теста 7",
|
||||
menu_test_burst: "Fingerprint/Сибирская блокировка",
|
||||
burst_settings_title: "Настройки теста 6",
|
||||
burst_field_attempts: "Одновременных запросов",
|
||||
burst_field_timeout: "Таймаут, с",
|
||||
burst_field_timeout: "Таймаут в секундах",
|
||||
burst_field_domain: "Домен для тестирования",
|
||||
burst_domain_placeholder: "Переключитесь для ввода",
|
||||
burst_domain_default_hint: "По умолчанию — все домены",
|
||||
burst_field_tls: "Версия TLS",
|
||||
burst_field_http: "Протокол HTTP",
|
||||
burst_field_profiles: "Отпечатки",
|
||||
burst_field_profiles: "Fingerprint",
|
||||
burst_profiles_all: "все",
|
||||
burst_title: "Одновременные рукопожатия (стресс отпечатка)",
|
||||
burst_attempts_label: "Запросов сразу",
|
||||
burst_summary_label: "Стресс",
|
||||
burst_summary_value: "{} из {} рукопожатий ответили · доменов с потерями: {}",
|
||||
fingerprint_label: "Отпечаток TLS",
|
||||
burst_testing: "Тестируем",
|
||||
fingerprint_label: "Fingerprint",
|
||||
fingerprint_note: "Каждый профиль воспроизводит одну закреплённую форму curl-impersonate: FIREFOX = firefox133, CHROME = chrome107 (и edge 99-101), SAFARI = safari155. Все предлагают h2 и http/1.1, как браузеры. Ни один не является побайтовой копией настоящего браузера — более глубокий фингерпринтинг (настройки HTTP/2, тайминги записей) всё ещё может их отличить.",
|
||||
lang: Language::Ru,
|
||||
replies_label: "ответов",
|
||||
|
|
@ -870,11 +864,11 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
cli_concurrency: "Лимит параллельных запросов",
|
||||
cli_domain: "Конкретные домены для проверки (флаг можно повторять: -d vk.com -d ya.ru)",
|
||||
cli_output: "Путь к файлу отчёта",
|
||||
cli_burst: "Стресс отпечатка (тест 7): одновременных запросов за раунд [по умолчанию: 4]",
|
||||
cli_burst_timeout: "Тест 7: таймаут одного рукопожатия, секунды [по умолчанию: 8]",
|
||||
cli_burst_profiles: "Отпечатки для теста 7: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [по умолчанию: all]",
|
||||
cli_burst_tls: "Версия TLS для теста 7: 1.2|1.3 [по умолчанию: 1.3]",
|
||||
cli_burst_alpn: "ALPN для теста 7: h2 (предлагает h2 с откатом на http/1.1)|http/1.1 (только http/1.1) [по умолчанию: h2]",
|
||||
cli_burst: "Fingerprint/Сибирская блокировка (тест 6): одновременных запросов за раунд [по умолчанию: 4]",
|
||||
cli_burst_timeout: "Тест 6: таймаут одного рукопожатия, секунды [по умолчанию: 8]",
|
||||
cli_burst_profiles: "Fingerprint для теста 6: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [по умолчанию: all]",
|
||||
cli_burst_tls: "Версия TLS для теста 6: 1.2|1.3 [по умолчанию: 1.3]",
|
||||
cli_burst_alpn: "ALPN для теста 6: h2 (предлагает h2 с откатом на http/1.1)|http/1.1 (только http/1.1) [по умолчанию: h2]",
|
||||
cli_domains: "Путь к файлу со списком доменов",
|
||||
cli_tcp16: "Путь к файлу целей TCP16",
|
||||
cli_ascii: "Только ASCII для старых консолей (без Unicode-глифов и рамок)",
|
||||
|
|
@ -917,22 +911,19 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
menu_test_sni: "发现白名单 SNI",
|
||||
menu_test_telegram: "Telegram 可用性",
|
||||
menu_test_legend: "状态图例 (帮助)",
|
||||
menu_test_burst: "指纹压力测试 (burst)",
|
||||
burst_settings_title: "测试 7 设置",
|
||||
menu_test_burst: "Fingerprint/西伯利亚封锁",
|
||||
burst_settings_title: "测试 6 设置",
|
||||
burst_field_attempts: "同时请求数",
|
||||
burst_field_timeout: "超时, 秒",
|
||||
burst_field_timeout: "超时秒数",
|
||||
burst_field_domain: "要测试的域名",
|
||||
burst_domain_placeholder: "按 → 开始输入",
|
||||
burst_domain_default_hint: "默认 — 全部域名",
|
||||
burst_field_tls: "TLS 版本",
|
||||
burst_field_http: "HTTP 协议",
|
||||
burst_field_profiles: "指纹",
|
||||
burst_field_profiles: "Fingerprint",
|
||||
burst_profiles_all: "全部",
|
||||
burst_title: "同时握手 (指纹压力)",
|
||||
burst_attempts_label: "并发请求",
|
||||
burst_summary_label: "压力",
|
||||
burst_summary_value: "{} / {} 次握手得到响应 · 有丢包的域名: {}",
|
||||
fingerprint_label: "TLS 指纹",
|
||||
burst_testing: "正在测试",
|
||||
fingerprint_label: "Fingerprint",
|
||||
fingerprint_note: "每个配置复现一个固定的 curl-impersonate 形态:FIREFOX = firefox133,CHROME = chrome107(以及 edge 99-101),SAFARI = safari155。三者都像浏览器一样提供 h2 与 http/1.1。它们均非真实浏览器的逐字节复制,更深层的指纹识别(HTTP/2 设置、记录时序)仍可能将它们区分开。",
|
||||
lang: Language::Zh,
|
||||
replies_label: "响应",
|
||||
|
|
@ -1107,11 +1098,11 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
cli_concurrency: "并行请求的并发上限",
|
||||
cli_domain: "要测试的指定域名(可重复指定: -d vk.com -d ya.ru)",
|
||||
cli_output: "保存报告的输出文件路径",
|
||||
cli_burst: "指纹压力测试 (测试 7): 每轮同时请求数 [默认: 4]",
|
||||
cli_burst_timeout: "测试 7: 单次握手超时, 秒 [默认: 8]",
|
||||
cli_burst_profiles: "测试 7 的指纹: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [默认: all]",
|
||||
cli_burst_tls: "测试 7 的 TLS 版本: 1.2|1.3 [默认: 1.3]",
|
||||
cli_burst_alpn: "测试 7 的 ALPN: h2(提供 h2 并回退 http/1.1)|http/1.1(仅 http/1.1)[默认: h2]",
|
||||
cli_burst: "Fingerprint/西伯利亚封锁 (测试 6): 每轮同时请求数 [默认: 4]",
|
||||
cli_burst_timeout: "测试 6: 单次握手超时, 秒 [默认: 8]",
|
||||
cli_burst_profiles: "测试 6 的 Fingerprint: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [默认: all]",
|
||||
cli_burst_tls: "测试 6 的 TLS 版本: 1.2|1.3 [默认: 1.3]",
|
||||
cli_burst_alpn: "测试 6 的 ALPN: h2(提供 h2 并回退 http/1.1)|http/1.1(仅 http/1.1)[默认: h2]",
|
||||
cli_domains: "自定义域名列表文件路径",
|
||||
cli_tcp16: "自定义 TCP16 目标文件路径",
|
||||
cli_ascii: "面向旧终端的纯 ASCII 输出(无 Unicode 符号或边框)",
|
||||
|
|
@ -1154,21 +1145,18 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
menu_test_sni: "Jost o juye SNI haye whitelist",
|
||||
menu_test_telegram: "Dastrasi be Telegram",
|
||||
menu_test_legend: "Rahnemaye barname",
|
||||
menu_test_burst: "Stress-e fingerprint (burst)",
|
||||
burst_settings_title: "Tanzimat-e test 7",
|
||||
menu_test_burst: "Fingerprint/Sibir blocking",
|
||||
burst_settings_title: "Tanzimat-e test 6",
|
||||
burst_field_attempts: "Darkhast haye hamzaman",
|
||||
burst_field_timeout: "Timeout, sanie",
|
||||
burst_field_timeout: "Timeout be sanie",
|
||||
burst_field_domain: "Domain baraye test",
|
||||
burst_domain_placeholder: "Baraye neveshtan -> bezanid",
|
||||
burst_domain_default_hint: "Pishfarz - hame-ye domain ha",
|
||||
burst_field_tls: "Version-e TLS",
|
||||
burst_field_http: "Protocol-e HTTP",
|
||||
burst_field_profiles: "Fingerprint ha",
|
||||
burst_field_profiles: "Fingerprint",
|
||||
burst_profiles_all: "hame",
|
||||
burst_title: "Mosafehe haye hamzaman (stress-e fingerprint)",
|
||||
burst_attempts_label: "Darkhast dar yek bar",
|
||||
burst_summary_label: "Stress",
|
||||
burst_summary_value: "{} az {} mosafehe javab dad | domain ba ziyan: {}",
|
||||
burst_testing: "Dar hal-e test",
|
||||
fingerprint_label: "Fingerprint",
|
||||
fingerprint_note: "Har profile yek shape-e pin-shode-ye curl-impersonate ra bazsazi mikonad: FIREFOX = firefox133, CHROME = chrome107 (va edge 99-101), SAFARI = safari155. Hame h2 va http/1.1 ra mesl-e browser ha pishnahad mikonand. Hich yek copy-e byte-be-byte-e yek browser-e vaghe-i nist - fingerprinting-e amigh-tar (HTTP/2 settings, record timing) hanuz mitavanad anha ra tafzil konad.",
|
||||
lang: Language::Fa,
|
||||
|
|
@ -1337,11 +1325,11 @@ pub fn get_messages(lang: Language) -> Messages {
|
|||
cli_concurrency: "Hadde aksar-e darkhast haye hamzaman",
|
||||
cli_domain: "Domain haye khass baraye barresi (mitavanid tekrar konid: -d vk.com -d ya.ru)",
|
||||
cli_output: "Masir-e file baraye zakhire-ye report",
|
||||
cli_burst: "Stress-e fingerprint (test 7): darkhast haye hamzaman dar har round [pishfarz: 4]",
|
||||
cli_burst_timeout: "Test 7: timeout-e yek mosafehe, sanie [pishfarz: 8]",
|
||||
cli_burst_profiles: "Fingerprint haye test 7: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [pishfarz: all]",
|
||||
cli_burst_tls: "Version-e TLS baraye test 7: 1.2|1.3 [pishfarz: 1.3]",
|
||||
cli_burst_alpn: "ALPN baraye test 7: h2 (h2 ba bazgasht be http/1.1)|http/1.1 (faghat http/1.1) [pishfarz: h2]",
|
||||
cli_burst: "Fingerprint/Sibir blocking (test 6): darkhast haye hamzaman dar har round [pishfarz: 4]",
|
||||
cli_burst_timeout: "Test 6: timeout-e yek mosafehe, sanie [pishfarz: 8]",
|
||||
cli_burst_profiles: "Fingerprint haye test 6: all|rustls,custom(firefox133),chrome(chrome107),safari(safari155) [pishfarz: all]",
|
||||
cli_burst_tls: "Version-e TLS baraye test 6: 1.2|1.3 [pishfarz: 1.3]",
|
||||
cli_burst_alpn: "ALPN baraye test 6: h2 (h2 ba bazgasht be http/1.1)|http/1.1 (faghat http/1.1) [pishfarz: h2]",
|
||||
cli_domains: "Masir-e file-e list-e domain ha",
|
||||
cli_tcp16: "Masir-e file-e target haye TCP16",
|
||||
cli_ascii: "Khorooj-e faghat ASCII baraye console haye ghadimi (bedun-e glyph ya border-e Unicode)",
|
||||
|
|
@ -1708,12 +1696,13 @@ mod tests {
|
|||
('3', msg.menu_test_tcp),
|
||||
('4', msg.menu_test_sni),
|
||||
('5', msg.menu_test_telegram),
|
||||
('6', msg.menu_test_legend),
|
||||
('6', msg.menu_test_burst),
|
||||
('7', msg.menu_test_legend),
|
||||
] {
|
||||
assert!(!field.is_empty());
|
||||
assert_eq!(msg.menu_test_label(d), field);
|
||||
}
|
||||
assert_eq!(msg.menu_test_label('7'), "");
|
||||
assert_eq!(msg.menu_test_label('8'), "");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,11 @@ pub enum TlsFingerprint {
|
|||
struct Spec {
|
||||
id: TlsFingerprint,
|
||||
token: &'static str,
|
||||
/// Token plus the version the shape reproduces, for the places that have
|
||||
/// room for it (burst table headers, the settings screen, the live line).
|
||||
/// The bare `token` stays version-less because it is also the round-trip
|
||||
/// name the parsers accept back.
|
||||
label: &'static str,
|
||||
code: &'static str,
|
||||
}
|
||||
|
||||
|
|
@ -75,21 +80,25 @@ const SPECS: [Spec; 4] = [
|
|||
Spec {
|
||||
id: TlsFingerprint::Rustls,
|
||||
token: "RUSTLS",
|
||||
label: "RUSTLS",
|
||||
code: "rustls",
|
||||
},
|
||||
Spec {
|
||||
id: TlsFingerprint::Custom,
|
||||
token: "FIREFOX",
|
||||
label: "FIREFOX 133",
|
||||
code: "custom",
|
||||
},
|
||||
Spec {
|
||||
id: TlsFingerprint::Chrome,
|
||||
token: "CHROME",
|
||||
label: "CHROME 107",
|
||||
code: "chrome",
|
||||
},
|
||||
Spec {
|
||||
id: TlsFingerprint::Safari,
|
||||
token: "SAFARI",
|
||||
label: "SAFARI 155",
|
||||
code: "safari",
|
||||
},
|
||||
];
|
||||
|
|
@ -107,6 +116,13 @@ impl TlsFingerprint {
|
|||
self.spec().token
|
||||
}
|
||||
|
||||
/// Token plus the version it reproduces ("CHROME 107"). Latin like `token`
|
||||
/// and never translated (rule 4): a table header or a progress line that
|
||||
/// says only "CHROME" hides which shape was actually sent.
|
||||
pub fn display_label(self) -> &'static str {
|
||||
self.spec().label
|
||||
}
|
||||
|
||||
/// Stable value for machine JSON and the config file.
|
||||
pub fn code(self) -> &'static str {
|
||||
self.spec().code
|
||||
|
|
@ -601,6 +617,21 @@ mod tests {
|
|||
const SAFARI_155_JA4: &str = "t13d2014h2_a09f3c656075_14788d8d241b";
|
||||
const FIREFOX_133_JA4_LESS_ECH: &str = "t13d1715h2_5b57614c22b0_8fb63dbc839a";
|
||||
|
||||
/// The version-bearing label is display only: it must not collide with a
|
||||
/// parser name, and every profile whose shape is a pinned curl version has
|
||||
/// to say which one.
|
||||
#[test]
|
||||
fn display_labels_name_the_pinned_version() {
|
||||
assert_eq!(TlsFingerprint::Custom.display_label(), "FIREFOX 133");
|
||||
assert_eq!(TlsFingerprint::Chrome.display_label(), "CHROME 107");
|
||||
assert_eq!(TlsFingerprint::Safari.display_label(), "SAFARI 155");
|
||||
assert_eq!(TlsFingerprint::Rustls.display_label(), "RUSTLS");
|
||||
for fp in TlsFingerprint::ALL {
|
||||
assert!(fp.display_label().starts_with(fp.token()), "{fp:?}");
|
||||
assert!(fp.display_label().is_ascii(), "{fp:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fingerprint_tokens_are_stable() {
|
||||
assert_eq!(TlsFingerprint::Rustls.token(), "RUSTLS");
|
||||
|
|
|
|||
|
|
@ -297,6 +297,26 @@ impl BurstReport {
|
|||
}
|
||||
}
|
||||
|
||||
/// Follows a burst round by round. The runner draws its live line from this;
|
||||
/// the no-op implementation below lets tests and library callers ignore it.
|
||||
pub trait BurstObserver {
|
||||
/// A profile round starts: `index` (0-based) of `total` shapes is about to
|
||||
/// be fired at `hosts` resolved targets.
|
||||
fn round_started(
|
||||
&self,
|
||||
_fingerprint: TlsFingerprint,
|
||||
_index: usize,
|
||||
_total: usize,
|
||||
_hosts: usize,
|
||||
) {
|
||||
}
|
||||
|
||||
/// One host finished its round, answered or not.
|
||||
fn host_finished(&self) {}
|
||||
}
|
||||
|
||||
impl BurstObserver for () {}
|
||||
|
||||
/// Fires every profile at every target, one profile at a time.
|
||||
///
|
||||
/// The outer loop is the profile: all targets are probed with the first shape,
|
||||
|
|
@ -310,6 +330,7 @@ pub async fn burst_targets(
|
|||
targets: &[BurstTarget],
|
||||
settings: &BurstSettings,
|
||||
concurrency: usize,
|
||||
observer: &dyn BurstObserver,
|
||||
) -> Vec<BurstReport> {
|
||||
let gate = Arc::new(Semaphore::new(concurrency.max(1)));
|
||||
let addresses = resolve_targets(cfg, targets, &gate).await;
|
||||
|
|
@ -323,7 +344,11 @@ pub async fn burst_targets(
|
|||
})
|
||||
.collect();
|
||||
|
||||
for &fingerprint in &settings.profiles {
|
||||
// Unresolved hosts are not probed, so they must not count towards the
|
||||
// round's total either.
|
||||
let hosts = addresses.iter().filter(|address| address.is_some()).count();
|
||||
for (index, &fingerprint) in settings.profiles.iter().enumerate() {
|
||||
observer.round_started(fingerprint, index, settings.profiles.len(), hosts);
|
||||
let mut rounds = JoinSet::new();
|
||||
for (index, target) in targets.iter().enumerate() {
|
||||
let Some(address) = addresses[index] else {
|
||||
|
|
@ -339,6 +364,7 @@ pub async fn burst_targets(
|
|||
});
|
||||
}
|
||||
while let Some(joined) = rounds.join_next().await {
|
||||
observer.host_finished();
|
||||
if let Ok((index, report)) = joined {
|
||||
reports[index].profiles.push(report);
|
||||
}
|
||||
|
|
@ -734,7 +760,24 @@ mod tests {
|
|||
BurstTarget { domain: "fast.example".to_string(), address: Some(fast_addr) },
|
||||
];
|
||||
let plan = settings(2, 4000, vec![TlsFingerprint::Rustls, TlsFingerprint::Chrome]);
|
||||
let reports = burst_targets(&AppConfig::default(), &targets, &plan, 4).await;
|
||||
// The live line the runner draws comes from the observer: a round must
|
||||
// be announced once, in the order the profiles are fired, or the line
|
||||
// would name a shape that is not on the wire.
|
||||
#[derive(Default)]
|
||||
struct Rounds {
|
||||
started: std::sync::Mutex<Vec<(TlsFingerprint, usize, usize, usize)>>,
|
||||
finished: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
impl BurstObserver for Rounds {
|
||||
fn round_started(&self, fp: TlsFingerprint, index: usize, total: usize, hosts: usize) {
|
||||
self.started.lock().expect("lock").push((fp, index, total, hosts));
|
||||
}
|
||||
fn host_finished(&self) {
|
||||
self.finished.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
let rounds = Rounds::default();
|
||||
let reports = burst_targets(&AppConfig::default(), &targets, &plan, 4, &rounds).await;
|
||||
|
||||
slow.await.expect("slow stand");
|
||||
fast.await.expect("fast stand");
|
||||
|
|
@ -745,6 +788,14 @@ mod tests {
|
|||
witness.unexpected.lock()
|
||||
);
|
||||
assert!(!witness.overlapped.load(Ordering::SeqCst), "two shapes were in flight at the same time");
|
||||
assert_eq!(
|
||||
*rounds.started.lock().expect("lock"),
|
||||
vec![
|
||||
(TlsFingerprint::Rustls, 0, 2, 2),
|
||||
(TlsFingerprint::Chrome, 1, 2, 2),
|
||||
]
|
||||
);
|
||||
assert_eq!(rounds.finished.load(Ordering::SeqCst), 4, "one tick per host per round");
|
||||
assert_eq!(reports.len(), 2);
|
||||
for report in &reports {
|
||||
let shapes: Vec<TlsFingerprint> = report.profiles.iter().map(|p| p.fingerprint).collect();
|
||||
|
|
|
|||
|
|
@ -22,15 +22,15 @@ pub struct CliArgs {
|
|||
pub tcp16: Option<String>,
|
||||
pub ascii: bool,
|
||||
pub fingerprint: Option<String>,
|
||||
/// Test 7: simultaneous handshakes per round.
|
||||
/// Test 6: simultaneous handshakes per round.
|
||||
pub burst: Option<usize>,
|
||||
/// Test 7: per-handshake timeout in seconds.
|
||||
/// Test 6: per-handshake timeout in seconds.
|
||||
pub burst_timeout: Option<u64>,
|
||||
/// Test 7: `all` or a comma list of profile codes/names.
|
||||
/// Test 6: `all` or a comma list of profile codes/names.
|
||||
pub burst_profiles: Option<String>,
|
||||
/// Test 7: pinned TLS version, `1.2` or `1.3`.
|
||||
/// Test 6: pinned TLS version, `1.2` or `1.3`.
|
||||
pub burst_tls: Option<String>,
|
||||
/// Test 7: ALPN to offer, `h2` or `http/1.1`.
|
||||
/// Test 6: ALPN to offer, `h2` or `http/1.1`.
|
||||
pub burst_alpn: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ use dpi_core::probe::domains::{
|
|||
};
|
||||
use dpi_core::probe::telegram::run_telegram_full;
|
||||
use dpi_core::probe::burst::{
|
||||
burst_targets, BurstAlpn, BurstSettings, BurstTarget, BurstTlsVersion, BURST_DEFAULT_ATTEMPTS,
|
||||
burst_targets, BurstAlpn, BurstObserver, BurstSettings, BurstTarget, BurstTlsVersion,
|
||||
BURST_DEFAULT_ATTEMPTS,
|
||||
BURST_DEFAULT_TIMEOUT_SECS,
|
||||
};
|
||||
use dpi_core::probe::whitelist::run_whitelist_sni;
|
||||
|
|
@ -49,8 +50,8 @@ use render::{
|
|||
SummaryData, TcpRow,
|
||||
};
|
||||
/// Splits a test selection string into per-test flags (mirrors `_selection_flags`).
|
||||
/// Tests: 0 netinfo, 1 DNS, 2 domains, 3 TCP, 4 white-SNI, 5 Telegram, 6 legend,
|
||||
/// 7 fingerprint stress (burst).
|
||||
/// Tests: 0 netinfo, 1 DNS, 2 domains, 3 TCP, 4 white-SNI, 5 Telegram,
|
||||
/// 6 fingerprint/burst, 7 legend.
|
||||
fn selection_flags(selection: &str) -> (bool, bool, bool, bool, bool, bool, bool, bool, bool) {
|
||||
let has = |c: char| selection.contains(c);
|
||||
let net = has('0');
|
||||
|
|
@ -59,8 +60,8 @@ fn selection_flags(selection: &str) -> (bool, bool, bool, bool, bool, bool, bool
|
|||
let tcp = has('3');
|
||||
let sni = has('4');
|
||||
let tg = has('5');
|
||||
let burst = has('7');
|
||||
let legend = has('6');
|
||||
let burst = has('6');
|
||||
let legend = has('7');
|
||||
let only_legend = legend && !(net || dns || dom || tcp || sni || tg || burst);
|
||||
(net, dns, dom, tcp, sni, tg, burst, legend, only_legend)
|
||||
}
|
||||
|
|
@ -650,14 +651,14 @@ async fn main() {
|
|||
let mut selection = tests_str.clone();
|
||||
|
||||
loop {
|
||||
// Test 7 is destructive for its targets, so it asks for its own settings
|
||||
// Test 6 is destructive for its targets, so it asks for its own settings
|
||||
// screen before it runs instead of starting with guesses: cancelling the
|
||||
// screen drops test 7 from the selection and runs the rest. The screen
|
||||
// belongs to the test itself, so an explicit `-t 7` gets it too — but
|
||||
// screen drops test 6 from the selection and runs the rest. The screen
|
||||
// belongs to the test itself, so an explicit `-t 6` gets it too — but
|
||||
// only on a terminal at both ends: with stdin or stdout redirected there
|
||||
// is nobody to answer the screen, and the CLI flags stand in for it.
|
||||
let settings_screen = !args.json
|
||||
&& selection.contains('7')
|
||||
&& selection.contains('6')
|
||||
&& std::io::stdin().is_terminal()
|
||||
&& std::io::stdout().is_terminal()
|
||||
&& tui_available();
|
||||
|
|
@ -674,9 +675,9 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
None => {
|
||||
selection = selection.replace('7', "");
|
||||
selection = selection.replace('6', "");
|
||||
if selection.is_empty() {
|
||||
// Test 7 was the only selection and its settings were
|
||||
// Test 6 was the only selection and its settings were
|
||||
// cancelled: leave instead of printing an empty report.
|
||||
println_out("");
|
||||
return;
|
||||
|
|
@ -843,13 +844,13 @@ fn timeout_family() -> NetFamilyInfo {
|
|||
}
|
||||
}
|
||||
|
||||
/// Everything test 7 needs: the burst shape and the hosts to fire it at.
|
||||
/// Everything test 6 needs: the burst shape and the hosts to fire it at.
|
||||
struct BurstPlan {
|
||||
settings: BurstSettings,
|
||||
targets: Vec<String>,
|
||||
}
|
||||
|
||||
/// Reports an unknown test 7 axis value (TLS version, ALPN) and keeps the
|
||||
/// Reports an unknown test 6 axis value (TLS version, ALPN) and keeps the
|
||||
/// default, the way an unknown profile name is reported rather than swapped
|
||||
/// silently.
|
||||
fn warn_unknown_axis(msg: &Messages, args: &CliArgs, flag: &str, value: &str, fallback: &str) {
|
||||
|
|
@ -865,7 +866,7 @@ fn warn_unknown_axis(msg: &Messages, args: &CliArgs, flag: &str, value: &str, fa
|
|||
);
|
||||
}
|
||||
|
||||
/// Builds test 7's plan from the CLI. An unknown profile name is reported (the
|
||||
/// Builds test 6's plan from the CLI. An unknown profile name is reported (the
|
||||
/// test then runs the profiles it did understand) rather than silently swapped
|
||||
/// for a different set.
|
||||
fn burst_plan_from_cli(args: &CliArgs, domains: &[String], msg: &Messages) -> BurstPlan {
|
||||
|
|
@ -875,7 +876,8 @@ fn burst_plan_from_cli(args: &CliArgs, domains: &[String], msg: &Messages) -> Bu
|
|||
};
|
||||
if !args.json {
|
||||
for token in unknown {
|
||||
let fallback = profiles.iter().map(|f| f.token()).collect::<Vec<_>>().join(", ");
|
||||
let fallback =
|
||||
profiles.iter().map(|f| f.display_label()).collect::<Vec<_>>().join(", ");
|
||||
eprintln!(
|
||||
"{}",
|
||||
msg.warn_unknown_fingerprint.replacen("{}", &token, 1).replacen("{}", &fallback, 1)
|
||||
|
|
@ -913,6 +915,34 @@ fn burst_plan_from_cli(args: &CliArgs, domains: &[String], msg: &Messages) -> Bu
|
|||
BurstPlan { settings, targets }
|
||||
}
|
||||
|
||||
/// Draws the burst's live line: the shape on the wire right now, which round
|
||||
/// that is of how many, how many hosts of the round are done, and the clock.
|
||||
struct BurstLine {
|
||||
live: Arc<LiveProgress>,
|
||||
msg: Messages,
|
||||
}
|
||||
|
||||
impl BurstObserver for BurstLine {
|
||||
fn round_started(&self, fingerprint: TlsFingerprint, index: usize, total: usize, hosts: usize) {
|
||||
// `LiveProgress` resets its clock on every `set`, so the timer reads as
|
||||
// the round's elapsed time rather than the whole run's.
|
||||
self.live.set(
|
||||
format!(
|
||||
"{}: {} {}/{}",
|
||||
self.msg.burst_testing,
|
||||
fingerprint.display_label(),
|
||||
index + 1,
|
||||
total
|
||||
),
|
||||
hosts,
|
||||
);
|
||||
}
|
||||
|
||||
fn host_finished(&self) {
|
||||
self.live.tick();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_test_suite(
|
||||
tests_str: &str,
|
||||
|
|
@ -990,7 +1020,6 @@ async fn run_test_suite(
|
|||
let mut dom_stats = None;
|
||||
let mut tcp_summary = None;
|
||||
let mut tg_full = None;
|
||||
let mut burst_summary = None;
|
||||
|
||||
// ── Test 0: network & system ──
|
||||
if run_net {
|
||||
|
|
@ -1373,49 +1402,29 @@ async fn run_test_suite(
|
|||
tg_full = Some(rep);
|
||||
}
|
||||
|
||||
// ── Test 7: fingerprint stress (simultaneous handshakes) ──
|
||||
// ── Test 6: fingerprint / Siberian blocking (simultaneous handshakes) ──
|
||||
if run_burst {
|
||||
let settings = &burst.settings;
|
||||
if !args.json {
|
||||
let profile_label = if settings.profiles.len() == TlsFingerprint::ALL.len() {
|
||||
msg.burst_profiles_all.to_string()
|
||||
} else {
|
||||
settings.profiles.iter().map(|f| f.token()).collect::<Vec<_>>().join(", ")
|
||||
};
|
||||
emitter.emit(&format!(
|
||||
"\n{} {}: {} | {}: {} | {}: {}s | {}: {} | {}: {} | {}: {}\n\n",
|
||||
msg.burst_title,
|
||||
msg.targets_label,
|
||||
burst.targets.len(),
|
||||
msg.burst_attempts_label,
|
||||
settings.attempts,
|
||||
msg.timeout_label,
|
||||
settings.timeout.as_secs(),
|
||||
msg.burst_field_tls.trim_end_matches(':'),
|
||||
settings.tls.token(),
|
||||
msg.burst_field_http.trim_end_matches(':'),
|
||||
settings.alpn.token(),
|
||||
msg.burst_field_profiles.trim_end_matches(':'),
|
||||
profile_label,
|
||||
));
|
||||
}
|
||||
let spinner = (!args.json).then(|| Spinner::start(msg.burst_title));
|
||||
// Profile-major: `burst_targets` probes every target with the first
|
||||
// shape to the end before the next shape starts, so a block one shape
|
||||
// triggers can never be read as the other's result. The simultaneity the
|
||||
// test measures is *within* a host (its N handshakes all leave together);
|
||||
// hosts inside one shape overlap like any other phase of the suite.
|
||||
let targets: Vec<BurstTarget> = burst.targets.iter().map(BurstTarget::new).collect();
|
||||
let reports = burst_targets(cfg, &targets, settings, concurrency).await;
|
||||
if let Some(spinner) = spinner {
|
||||
spinner.finish();
|
||||
// No banner and no header block: the test says what it is doing on one
|
||||
// live line that names the shape currently on the wire and how far the
|
||||
// run has got, and nothing else is printed until the table.
|
||||
let line = BurstLine { live: Arc::clone(&live), msg: *msg };
|
||||
let observer: &dyn BurstObserver = if args.json || !std::io::stderr().is_terminal() {
|
||||
&()
|
||||
} else {
|
||||
&line
|
||||
};
|
||||
let reports = burst_targets(cfg, &targets, settings, concurrency, observer).await;
|
||||
if !args.json && std::io::stderr().is_terminal() {
|
||||
live.finish();
|
||||
}
|
||||
|
||||
let answered: usize = reports.iter().map(|r| r.answered()).sum();
|
||||
let total: usize = reports.iter().map(|r| r.total()).sum();
|
||||
let lossy = reports.iter().filter(|r| r.has_losses()).count();
|
||||
burst_summary = Some((answered, total, lossy));
|
||||
|
||||
if !args.json {
|
||||
emitter.emit(&render_burst_table(&reports, settings, msg));
|
||||
} else {
|
||||
|
|
@ -1459,7 +1468,7 @@ async fn run_test_suite(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Test 6: legend ──
|
||||
// ── Test 7: legend ──
|
||||
if run_legend && !args.json {
|
||||
print_out(&legend_text(lang, msg));
|
||||
}
|
||||
|
|
@ -1472,7 +1481,6 @@ async fn run_test_suite(
|
|||
dns: dns_stats.as_ref(),
|
||||
domains: dom_stats.as_ref(),
|
||||
tcp: tcp_summary,
|
||||
burst: burst_summary,
|
||||
run_telegram: run_tg,
|
||||
telegram: tg_full.as_ref(),
|
||||
},
|
||||
|
|
@ -1543,11 +1551,17 @@ mod tests {
|
|||
assert!(run_leg);
|
||||
assert!(!only_leg);
|
||||
|
||||
let (_, _, _, _, _, _, _, run_leg, only_leg) = selection_flags("6");
|
||||
// The legend is test 7 now, and the fingerprint burst test 6.
|
||||
let (_, _, _, _, _, _, run_burst, run_leg, only_leg) = selection_flags("6");
|
||||
assert!(run_burst);
|
||||
assert!(!run_leg);
|
||||
assert!(!only_leg);
|
||||
|
||||
let (_, _, _, _, _, _, _, run_leg, only_leg) = selection_flags("7");
|
||||
assert!(run_leg);
|
||||
assert!(only_leg);
|
||||
|
||||
let (run_net, _, _, _, _, _, _, run_leg, only_leg) = selection_flags("06");
|
||||
let (run_net, _, _, _, _, _, _, run_leg, only_leg) = selection_flags("07");
|
||||
assert!(run_net);
|
||||
assert!(run_leg);
|
||||
assert!(!only_leg);
|
||||
|
|
|
|||
|
|
@ -599,8 +599,8 @@ fn get_test_options(msg: &Messages) -> [(char, &'static str); 8] {
|
|||
('3', msg.menu_test_tcp),
|
||||
('4', msg.menu_test_sni),
|
||||
('5', msg.menu_test_telegram),
|
||||
('6', msg.menu_test_legend),
|
||||
('7', msg.menu_test_burst),
|
||||
('6', msg.menu_test_burst),
|
||||
('7', msg.menu_test_legend),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -624,6 +624,18 @@ const BURST_ROW_PROFILES: usize = 5;
|
|||
const BURST_LABEL_WIDTH: usize = 24;
|
||||
/// Inner cells of the domain input box (`[ ` … ` ]` is drawn around them).
|
||||
const BURST_INPUT_WIDTH: usize = 34;
|
||||
/// Background of the domain input box while its row is selected but not being
|
||||
/// typed into, and while it is: grey reads as "a field you can open", the blue
|
||||
/// as "the keyboard goes here". The text colors keep the two states apart on a
|
||||
/// light console theme too. All five are the 16-colour set on purpose: the
|
||||
/// legacy console translator maps exactly those to `SetConsoleTextAttribute`,
|
||||
/// while a 256-colour code would leave a Windows 7/8 console with no background
|
||||
/// at all.
|
||||
const IDLE_BG: &str = "\x1b[100m";
|
||||
const IDLE_FG: &str = "\x1b[37m";
|
||||
const TYPED_FG: &str = "\x1b[97m";
|
||||
const EDITING_BG: &str = "\x1b[44m";
|
||||
const EDITING_FG: &str = "\x1b[97m";
|
||||
|
||||
/// The last `width` characters of the text: the caret sits at the end, so a long
|
||||
/// host keeps its end in view rather than its beginning.
|
||||
|
|
@ -636,12 +648,12 @@ fn tail_of(text: &str, width: usize) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Test 7's own screen: how many handshakes at once, how long each may take,
|
||||
/// Test 6's own screen: how many handshakes at once, how long each may take,
|
||||
/// which host, and which ClientHello profiles.
|
||||
///
|
||||
/// Returns `None` when the user cancels (Q/Esc/Ctrl-C): the caller then skips
|
||||
/// test 7 instead of running it with guesses. The screen is only shown for an
|
||||
/// interactive run; a piped `-t 7` takes the values from the CLI.
|
||||
/// test 6 instead of running it with guesses. The screen is only shown for an
|
||||
/// interactive run; a piped `-t 6` takes the values from the CLI.
|
||||
pub async fn burst_settings_menu(
|
||||
lang: Language,
|
||||
initial: &BurstSettings,
|
||||
|
|
@ -852,27 +864,29 @@ fn burst_settings_rows(
|
|||
lines.push(field(BURST_ROW_TLS, msg.burst_field_tls, steer(BURST_ROW_TLS, tls.token().to_string())));
|
||||
lines.push(field(BURST_ROW_HTTP, msg.burst_field_http, steer(BURST_ROW_HTTP, alpn.token().to_string())));
|
||||
|
||||
// Domain: a grey input box. Right makes it active (caret, letters become
|
||||
// text), Left returns it to grey; what an empty field means is spelled out
|
||||
// on the line below it.
|
||||
// Domain: an input box whose own background carries the state — dark grey
|
||||
// while the row is only selected (letters are still hotkeys), blue while it
|
||||
// is being typed into (letters become text). The brackets and the pad are
|
||||
// painted with the same background, so the box reads as one field instead of
|
||||
// a color behind some text.
|
||||
let domain_value = {
|
||||
let placeholder = label(msg.burst_domain_placeholder);
|
||||
let (inner, visible) = if editing {
|
||||
let (background, foreground, inner, visible) = if editing {
|
||||
let shown = tail_of(text, BURST_INPUT_WIDTH - 1);
|
||||
let visible = shown.chars().count() + 1;
|
||||
(format!("\x1b[1;36m{}\x1b[0m\x1b[1;33m▏\x1b[0m", shown), visible)
|
||||
(EDITING_BG, EDITING_FG, format!("{}\x1b[1;33m▏", shown), visible)
|
||||
} else if text.is_empty() {
|
||||
let placeholder = label(msg.burst_domain_placeholder);
|
||||
let visible = placeholder.chars().count();
|
||||
(format!("\x1b[2m{}\x1b[0m", placeholder), visible)
|
||||
(IDLE_BG, IDLE_FG, placeholder, visible)
|
||||
} else {
|
||||
let shown = tail_of(text, BURST_INPUT_WIDTH);
|
||||
let visible = shown.chars().count();
|
||||
(format!("\x1b[2m{}\x1b[0m", shown), visible)
|
||||
(IDLE_BG, TYPED_FG, shown, visible)
|
||||
};
|
||||
let brackets = if editing { "\x1b[1;33m" } else { "\x1b[2m" };
|
||||
format!(
|
||||
"{}[ \x1b[0m{}\x1b[2m{} ]\x1b[0m",
|
||||
brackets,
|
||||
"{}{}[ {}{} ]\x1b[0m",
|
||||
background,
|
||||
foreground,
|
||||
inner,
|
||||
" ".repeat(BURST_INPUT_WIDTH.saturating_sub(visible))
|
||||
)
|
||||
|
|
@ -894,13 +908,14 @@ fn burst_settings_rows(
|
|||
let name = if index == 0 {
|
||||
label(msg.burst_profiles_all)
|
||||
} else {
|
||||
// Canonical tokens, like the table headers: the cycler names the
|
||||
// profile the run will actually use (rule 4, never translated).
|
||||
TlsFingerprint::ALL[index - 1].token().to_string()
|
||||
// Latin with the pinned version, like the table headers: the
|
||||
// cycler names the exact shape the run will use (rule 4, never
|
||||
// translated).
|
||||
TlsFingerprint::ALL[index - 1].display_label().to_string()
|
||||
};
|
||||
format!("{} \x1b[2m[{}/{}]\x1b[0m", name, index + 1, PROFILE_CHOICES)
|
||||
}
|
||||
None => profiles.iter().map(|f| f.token()).collect::<Vec<_>>().join(", "),
|
||||
None => profiles.iter().map(|f| f.display_label()).collect::<Vec<_>>().join(", "),
|
||||
};
|
||||
lines.push(field(BURST_ROW_PROFILES, msg.burst_field_profiles, profile_value));
|
||||
let lines: Vec<String> = lines.into_iter().map(|l| asc(&l)).collect();
|
||||
|
|
@ -1036,12 +1051,55 @@ mod tests {
|
|||
assert!(!joined.contains("Переключитесь для ввода"), "{joined}");
|
||||
|
||||
// A single-profile selection is what the cycler shows after a press: the
|
||||
// position counts the `all` entry, so CHROME is the fourth of five.
|
||||
// position counts the `all` entry, so CHROME is the fourth of five — and
|
||||
// it is named with the version it reproduces, not with the bare family.
|
||||
let chrome = [TlsFingerprint::Chrome];
|
||||
let single = burst_settings_rows(
|
||||
&msg, Language::Ru, 3, 4, 8, BurstTlsVersion::Tls12, BurstAlpn::Http2, "", false, &chrome, profile_index_of(&chrome), 35,
|
||||
);
|
||||
assert!(strip_ansi(&single.join("\n")).contains("CHROME [4/5]"));
|
||||
assert!(strip_ansi(&single.join("\n")).contains("CHROME 107 [4/5]"));
|
||||
}
|
||||
|
||||
/// The domain box announces its state with its own background — grey while
|
||||
/// the row is only selected, blue while it is being typed into — and the
|
||||
/// brackets and pad carry it too, so the box reads as one field. A pair of
|
||||
/// screenshots is not something a test can hold, so the escapes are.
|
||||
#[test]
|
||||
fn burst_domain_box_paints_its_state() {
|
||||
let msg = get_messages(Language::Ru);
|
||||
let all = vec![TlsFingerprint::Chrome];
|
||||
let box_row = |editing: bool, text: &str| {
|
||||
burst_settings_rows(
|
||||
&msg,
|
||||
Language::Ru,
|
||||
BURST_ROW_DOMAIN,
|
||||
4,
|
||||
8,
|
||||
BurstTlsVersion::Tls13,
|
||||
BurstAlpn::Http2,
|
||||
text,
|
||||
editing,
|
||||
&all,
|
||||
Some(0),
|
||||
35,
|
||||
)
|
||||
.into_iter()
|
||||
.find(|row| row.contains("[ ") && row.contains(" ]"))
|
||||
.expect("domain input box")
|
||||
};
|
||||
|
||||
let idle = box_row(false, "");
|
||||
assert!(idle.contains(IDLE_BG) && idle.contains(IDLE_FG), "{idle:?}");
|
||||
assert!(!idle.contains(EDITING_BG), "a selected row is not an open field: {idle:?}");
|
||||
|
||||
let typed = box_row(false, "www.google.com");
|
||||
assert!(typed.contains(IDLE_BG) && typed.contains(TYPED_FG), "{typed:?}");
|
||||
assert!(typed.contains("www.google.com"), "{typed:?}");
|
||||
|
||||
let editing = box_row(true, "www.google.com");
|
||||
assert!(editing.contains(EDITING_BG) && editing.contains(EDITING_FG), "{editing:?}");
|
||||
assert!(!editing.contains(IDLE_BG), "an open field must not look selected: {editing:?}");
|
||||
assert!(editing.contains('▏'), "the caret marks the active field: {editing:?}");
|
||||
}
|
||||
|
||||
/// The keycaps of both screens must not exceed an 80-column console.
|
||||
|
|
|
|||
|
|
@ -1767,16 +1767,19 @@ pub fn render_dns_availability(report: &DnsAvailReport, cfg: &AppConfig, msg: &M
|
|||
|
||||
// ─── Test 2: domains ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Test 7's table: one row per host, one column per profile, the cell being how
|
||||
/// many of the simultaneous handshakes came back. The header carries the
|
||||
/// canonical profile tokens (rule 4: never translated), and the detail column
|
||||
/// names the failure that happened most often across the profile columns.
|
||||
/// Test 6's table: one row per host, one column per profile, the cell being how
|
||||
/// many of the simultaneous handshakes came back. The header names each profile
|
||||
/// with the pinned version it reproduces (rule 4: Latin, never translated), and
|
||||
/// the detail column names the failure that happened most often across the
|
||||
/// profile columns.
|
||||
pub fn render_burst_table(reports: &[BurstReport], settings: &BurstSettings, msg: &Messages) -> String {
|
||||
let profiles: &[TlsFingerprint] = &settings.profiles;
|
||||
let mut table = Table::new();
|
||||
let mut header = vec![Cell::new(format_bidi(msg.domain, msg.lang))];
|
||||
for fingerprint in profiles {
|
||||
header.push(Cell::new(fingerprint.token()));
|
||||
// The version, not just the family: two runs of "CHROME" can differ in
|
||||
// the shape they sent, and the header is where that is read off.
|
||||
header.push(Cell::new(fingerprint.display_label()));
|
||||
}
|
||||
header.push(Cell::new(format_bidi(msg.detail, msg.lang)));
|
||||
table
|
||||
|
|
@ -2196,8 +2199,6 @@ pub struct SummaryData<'a> {
|
|||
pub dns: Option<&'a dpi_core::dns::availability::DnsAvailStats>,
|
||||
pub domains: Option<&'a DomainStats>,
|
||||
pub tcp: Option<(usize, usize, usize, usize)>, // ok, blocked, mixed, total
|
||||
/// Test 7: handshakes answered, handshakes fired, hosts that lost at least one.
|
||||
pub burst: Option<(usize, usize, usize)>,
|
||||
pub run_telegram: bool,
|
||||
pub telegram: Option<&'a TelegramFullReport>,
|
||||
}
|
||||
|
|
@ -2265,19 +2266,6 @@ pub fn render_summary(data: &SummaryData, msg: &Messages) -> String {
|
|||
));
|
||||
}
|
||||
|
||||
// Test 7's totals ride in the same panel: a burst that lost handshakes is a
|
||||
// different finding from a host that is simply unreachable, and the two are
|
||||
// read side by side.
|
||||
if let Some((answered, total, lossy)) = data.burst {
|
||||
let color = frac_color(answered, total);
|
||||
let value = msg
|
||||
.burst_summary_value
|
||||
.replacen("{}", &answered.to_string(), 1)
|
||||
.replacen("{}", &total.to_string(), 1)
|
||||
.replacen("{}", &lossy.to_string(), 1);
|
||||
items.push((msg.burst_summary_label.to_string(), format!("[{}]{}[/]", color, value)));
|
||||
}
|
||||
|
||||
if let Some((ok, blocked, mixed, total)) = data.tcp {
|
||||
let pct = ok.checked_mul(100).and_then(|v| v.checked_div(total)).unwrap_or(0);
|
||||
let mut value = format!("[green]√ {}/{} OK[/]", ok, total);
|
||||
|
|
@ -2838,7 +2826,6 @@ mod tests {
|
|||
dns: None,
|
||||
domains: None,
|
||||
tcp: Some((104, 0, 0, 110)),
|
||||
burst: None,
|
||||
run_telegram: false,
|
||||
telegram: None,
|
||||
},
|
||||
|
|
@ -2904,7 +2891,6 @@ mod tests {
|
|||
dns: Some(&stats),
|
||||
domains: None,
|
||||
tcp: None,
|
||||
burst: None,
|
||||
run_telegram: false,
|
||||
telegram: None,
|
||||
},
|
||||
|
|
|
|||
12
vendor/rustls/README-PATCH.md
vendored
12
vendor/rustls/README-PATCH.md
vendored
|
|
@ -140,6 +140,18 @@ failing handshake or a real mismatched fingerprint, not a theoretical concern:
|
|||
and registers a verbatim body. Listing GREASE only in `raw_extensions` — as the
|
||||
first version did — silently drops it, because the encoder iterates the order.
|
||||
|
||||
* **The GREASE key share is load-bearing on a censored link — but it is not the
|
||||
whole shape.** A greasing profile also offers a key share for its GREASE group
|
||||
(one dummy byte, Chrome's placement at the head of the list), which is what
|
||||
`curl_chrome107`/`curl_safari155` send. Measured against `standby-rezka.tv`
|
||||
over TLS 1.3: with the share our chrome/safari hellos get a fatal alert every
|
||||
time (12/12 across three interleaved rounds), without it they complete, while
|
||||
the bundle's own `curl_chrome107` — which also carries a GREASE share —
|
||||
completes. So the peer reads more than the share: the hello we still do not
|
||||
reproduce exactly is `supported_versions` (`[0x0304]` here against
|
||||
`[GREASE, 0x0304, 0x0303]` in the bundle), and until that matches, a
|
||||
chrome/safari block on such a host is *our* hello being refused, not evidence
|
||||
that the pinned browser shape is.
|
||||
* **`sent_extensions` must be the set that reaches the wire.** rustls builds
|
||||
`ClientHelloDetails::sent_extensions` from its typed fields, but a profile can
|
||||
add extensions rustls has no typed field for (`ec_point_formats`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue