docs: sync readme and architecture with the final module and flag set

readme's sample console output showed a multi-line summary table
(connection/egress/vpn/latency) the tool has never printed; the real
stdout is three lines (banner, verdict, file paths). also documented:
the tcptrace/ndt7 extras install command, bare-asn --target syntax,
ndt7's interactive consent prompt, tcp-trace's cascade fallback, and
that --watch writes one artifact per session rather than per tick.

architecture's storage section had the peeringdb cache path wrong
(.cache/peeringdb/*.json vs the real .cache/pdb-net-*.json) and never
mentioned the watch_*.json artifact from task 39. the file-size
exceptions note only covered exporter.py; interpret.py, cli.py,
models.py, bgp.py, ip_geo.py, netinfo.py, speed.py, reputation.py and
probes/traceroute.py all cleared ~200 lines too and are now listed
with the reason each stayed one file. test count updated from ~120
(written before a single module existed) to the real ~385.
This commit is contained in:
Andrew Pimenov 2026-08-09 03:58:54 +03:00
parent 5da12ae629
commit 4497e2a871
4 changed files with 66 additions and 38 deletions

View file

@ -20,17 +20,28 @@ src/netcheck/
traceparse.py Pure text -> TraceHop parsers for Windows, Linux and BSD/macOS output. traceparse.py Pure text -> TraceHop parsers for Windows, Linux and BSD/macOS output.
interpret.py Pure verdict engine: thresholds -> Finding[], VPN scoring, bufferbloat grade. interpret.py Pure verdict engine: thresholds -> Finding[], VPN scoring, bufferbloat grade.
speed.py Cascading speedtest, throughput math, cfL4 header parsing, bufferbloat probe. speed.py Cascading speedtest, throughput math, cfL4 header parsing, bufferbloat probe.
exporter.py JSON and Markdown rendering, atomic writes into ./logs/. Deliberately over exporter.py JSON and Markdown rendering, atomic writes into ./logs/.
the file-size guideline to keep both output formats in one place.
compare.py --compare: diff two saved JSON reports. compare.py --compare: diff two saved JSON reports.
watch.py --watch: periodic re-run loop with a live Rich dashboard. watch.py --watch: periodic re-run loop with a live Rich dashboard.
probes/ probes/
latency.py Ping fan-out and jitter/loss/mdev statistics. latency.py Ping fan-out and jitter/loss/mdev statistics.
traceroute.py Cascade: mtr --json -> icmp_win -> icmplib -> system binary text. traceroute.py Cascade: mtr --json -> icmp_win -> icmplib -> system binary text, plus the opt-in tcp_trace tier.
icmp_win.py ctypes IcmpSendEcho2 / Icmp6SendEcho2 engine (Windows only). icmp_win.py ctypes IcmpSendEcho2 / Icmp6SendEcho2 engine (Windows only).
dns_leak.py Per-adapter resolver enumeration, echo probes, ECS detection. dns_leak.py Per-adapter resolver enumeration, echo probes, ECS detection.
``` ```
**File-size exceptions.** The ~200-line guideline (Global Constraints) held for most modules, but these grew past it during implementation and were kept as single files rather than split, because each one is a single cohesive responsibility that splitting would only scatter across more files with a thinner reason to draw the line anywhere in particular:
- `exporter.py` (~475 lines) — JSON and Markdown are two renderers of the same report shape; keeping them together is what guarantees they can never drift on which sections exist.
- `cli.py` (~465 lines) — the Typer app, every flag, and the full `diagnose()` phase pipeline; splitting flag parsing from what the flags toggle would just add an import hop.
- `models.py` (~330 lines) — every shared dataclass plus `to_jsonable()`; this is deliberately the one place that defines the report's vocabulary.
- `interpret.py` (~320 lines) — threshold bands, the latency/path/speed finding generators, VPN scoring and bufferbloat grading all reason over the same `Finding[]` shape.
- `bgp.py` (~305 lines) — four independent providers (RIPEstat, CAIDA ASRank, Team Cymru, PeeringDB) plus the disk cache they share.
- `ip_geo.py` (~280 lines) — six independent provider normalizers plus the field-wise merge that reconciles them.
- `netinfo.py`, `speed.py` (~260 lines each) — OS-specific capability probing and the four-tier speedtest cascade both fan out into several backends that don't split cleanly by single responsibility.
- `reputation.py` (~250 lines) — FireHOL netsets, Shodan InternetDB and DNSBL decoding are three unrelated reputation sources sharing one result shape.
- `probes/traceroute.py` (~225 lines) — the four-tier cascade plus the opt-in `tcp_trace` tier.
## Data flow ## Data flow
``` ```
@ -78,9 +89,10 @@ Two hard concurrency constraints:
## Storage ## Storage
- `./logs/report_<ASN>_<YYYYMMDDTHHMMSSZ>.{md,json}` — one pair per run. Compact ISO timestamps because Windows forbids `:` in filenames. Falls back to `report_unknown_…` when the ASN lookup fails entirely. Written temp-file-plus-`os.replace()`, always atomic. Gitignored: reports contain the external IP, city, coordinates and ISP name. - `./logs/report_<ASN>_<YYYYMMDDTHHMMSSZ>.{md,json}` — one pair per run. `<ASN>` is the target's subject in target mode (an ASN, IP or domain) and the local egress ASN in auto mode. Compact ISO timestamps because Windows forbids `:` in filenames. Falls back to `report_unknown_…` when the ASN lookup fails entirely. Written temp-file-plus-`os.replace()`, always atomic. Gitignored: reports contain the external IP, city, coordinates and ISP name.
- `./logs/watch_<ASN>_<YYYYMMDDTHHMMSSZ>.json` — one time-series artifact per `--watch` session, not one report per tick.
- `./.cache/firehol/*.netset` — downloaded blocklists, refreshed per `providers.firehol_refresh_hours`. - `./.cache/firehol/*.netset` — downloaded blocklists, refreshed per `providers.firehol_refresh_hours`.
- `./.cache/peeringdb/*.json` — PeeringDB responses, valid for `providers.peeringdb_cache_hours`. - `./.cache/pdb-net-<asn>.json`, `./.cache/pdb-netixlan-<net_id>.json` — PeeringDB responses, valid for `providers.peeringdb_cache_hours`.
- `./config.yaml` — all non-secret settings. `./.env` — the three optional API keys and nothing else. - `./config.yaml` — all non-secret settings. `./.env` — the three optional API keys and nothing else.
## Configuration ## Configuration
@ -89,4 +101,4 @@ Two hard concurrency constraints:
## Tests ## Tests
`uv run pytest -q`. Roughly 120 tests covering the parsers, normalizers, verdict engine, DNSBL decoding, throughput math, ping statistics, serialization and the report diff. Glue is deliberately untested: live HTTP, real ICMP, Typer wiring, `psutil`/`ctypes` calls and the `--watch` timing loop. Traceroute fixtures live in `tests/fixtures/traceroute/{windows,linux,darwin}/` and include a cp866-encoded Russian Windows sample, because that encoding is exactly what breaks naive parsers. `uv run pytest -q`. Roughly 385 tests covering the parsers, normalizers, verdict engine, DNSBL decoding, throughput math, ping statistics, serialization and the report diff. Glue is deliberately untested: live HTTP, real ICMP, Typer wiring, `psutil`/`ctypes` calls and the `--watch` timing loop. Traceroute fixtures live in `tests/fixtures/traceroute/{windows,linux,darwin}/` and include a cp866-encoded Russian Windows sample, because that encoding is exactly what breaks naive parsers.

View file

@ -20,17 +20,28 @@ src/netcheck/
traceparse.py Чистые парсеры текста в TraceHop для Windows, Linux и BSD/macOS. traceparse.py Чистые парсеры текста в TraceHop для Windows, Linux и BSD/macOS.
interpret.py Чистый движок вердиктов: пороги -> Finding[], скоринг VPN, оценка bufferbloat. interpret.py Чистый движок вердиктов: пороги -> Finding[], скоринг VPN, оценка bufferbloat.
speed.py Каскадный замер скорости, математика пропускной способности, разбор cfL4, bufferbloat. speed.py Каскадный замер скорости, математика пропускной способности, разбор cfL4, bufferbloat.
exporter.py Рендер JSON и Markdown, атомарная запись в ./logs/. Намеренно превышает exporter.py Рендер JSON и Markdown, атомарная запись в ./logs/.
лимит размера файла, чтобы оба формата вывода жили в одном месте.
compare.py --compare: сравнение двух сохранённых JSON-отчётов. compare.py --compare: сравнение двух сохранённых JSON-отчётов.
watch.py --watch: цикл периодических прогонов с живой панелью Rich. watch.py --watch: цикл периодических прогонов с живой панелью Rich.
probes/ probes/
latency.py Веер пингов и статистика джиттера/потерь/mdev. latency.py Веер пингов и статистика джиттера/потерь/mdev.
traceroute.py Каскад: mtr --json -> icmp_win -> icmplib -> системный бинарник. traceroute.py Каскад: mtr --json -> icmp_win -> icmplib -> системный бинарник, плюс опциональный уровень tcp_trace.
icmp_win.py Движок ctypes IcmpSendEcho2 / Icmp6SendEcho2 (только Windows). icmp_win.py Движок ctypes IcmpSendEcho2 / Icmp6SendEcho2 (только Windows).
dns_leak.py Перечисление резолверов по адаптерам, echo-пробы, детект ECS. dns_leak.py Перечисление резолверов по адаптерам, echo-пробы, детект ECS.
``` ```
**Исключения по размеру файла.** Лимит ~200 строк (Общие ограничения) выдержан для большинства модулей, но эти выросли за него в процессе реализации и остались одним файлом, а не были разбиты, потому что каждый — единая цельная ответственность, и разбиение только раскидало бы её по файлам без чёткой причины проводить границу где-то конкретно:
- `exporter.py` (~475 строк) — JSON и Markdown — два рендера одной и той же формы отчёта; то, что они вместе, гарантирует, что они никогда не разойдутся в том, какие разделы существуют.
- `cli.py` (~465 строк) — приложение Typer, все флаги и весь конвейер фаз `diagnose()`; разделение разбора флагов и того, что они переключают, добавило бы только лишний импорт.
- `models.py` (~330 строк) — все общие датаклассы плюс `to_jsonable()`; это намеренно единственное место, определяющее словарь отчёта.
- `interpret.py` (~320 строк) — пороговые полосы, генераторы находок по задержке/маршруту/скорости, скоринг VPN и оценка bufferbloat — всё рассуждает над одной формой `Finding[]`.
- `bgp.py` (~305 строк) — четыре независимых источника (RIPEstat, CAIDA ASRank, Team Cymru, PeeringDB) плюс общий для них дисковый кэш.
- `ip_geo.py` (~280 строк) — шесть независимых нормализаторов провайдеров плюс пополевое слияние, которое их примиряет.
- `netinfo.py`, `speed.py` (~260 строк каждый) — определение возможностей по ОС и каскад замера скорости из четырёх уровней оба веерятся по нескольким бэкендам, которые не разбиваются чисто по единой ответственности.
- `reputation.py` (~250 строк) — netset'ы FireHOL, Shodan InternetDB и декодирование DNSBL — три несвязанных источника репутации с одной общей формой результата.
- `probes/traceroute.py` (~225 строк) — каскад из четырёх уровней плюс опциональный уровень `tcp_trace`.
## Поток данных ## Поток данных
``` ```
@ -78,9 +89,10 @@ src/netcheck/
## Хранение ## Хранение
- `./logs/report_<ASN>_<YYYYMMDDTHHMMSSZ>.{md,json}` — пара файлов на прогон. Компактный ISO-таймстамп, потому что Windows запрещает `:` в именах файлов. При полном провале определения ASN — `report_unknown_…`. Запись через временный файл и `os.replace()`, всегда атомарно. В gitignore: отчёты содержат внешний IP, город, координаты и название провайдера. - `./logs/report_<ASN>_<YYYYMMDDTHHMMSSZ>.{md,json}` — пара файлов на прогон. `<ASN>` — это цель в режиме target (ASN, IP или домен) и локальный ASN исходящего адреса в авто-режиме. Компактный ISO-таймстамп, потому что Windows запрещает `:` в именах файлов. При полном провале определения ASN — `report_unknown_…`. Запись через временный файл и `os.replace()`, всегда атомарно. В gitignore: отчёты содержат внешний IP, город, координаты и название провайдера.
- `./logs/watch_<ASN>_<YYYYMMDDTHHMMSSZ>.json` — один тайм-ряд на сессию `--watch`, а не отдельный отчёт на каждый тик.
- `./.cache/firehol/*.netset` — скачанные блоклисты, обновляются по `providers.firehol_refresh_hours`. - `./.cache/firehol/*.netset` — скачанные блоклисты, обновляются по `providers.firehol_refresh_hours`.
- `./.cache/peeringdb/*.json` — ответы PeeringDB, действительны `providers.peeringdb_cache_hours`. - `./.cache/pdb-net-<asn>.json`, `./.cache/pdb-netixlan-<net_id>.json` — ответы PeeringDB, действительны `providers.peeringdb_cache_hours`.
- `./config.yaml` — все несекретные настройки. `./.env` — три опциональных API-ключа и ничего больше. - `./config.yaml` — все несекретные настройки. `./.env` — три опциональных API-ключа и ничего больше.
## Конфигурация ## Конфигурация
@ -89,4 +101,4 @@ src/netcheck/
## Тесты ## Тесты
`uv run pytest -q`. Около 120 тестов: парсеры, нормализаторы, движок вердиктов, декодирование DNSBL, математика пропускной способности, статистика пингов, сериализация и сравнение отчётов. Клей намеренно не тестируется: живой HTTP, настоящий ICMP, обвязка Typer, вызовы `psutil`/`ctypes` и цикл `--watch`. Фикстуры трассировок лежат в `tests/fixtures/traceroute/{windows,linux,darwin}/` и включают образец русской Windows в cp866 — именно эта кодировка ломает наивные парсеры. `uv run pytest -q`. Около 385 тестов: парсеры, нормализаторы, движок вердиктов, декодирование DNSBL, математика пропускной способности, статистика пингов, сериализация и сравнение отчётов. Клей намеренно не тестируется: живой HTTP, настоящий ICMP, обвязка Typer, вызовы `psutil`/`ctypes` и цикл `--watch`. Фикстуры трассировок лежат в `tests/fixtures/traceroute/{windows,linux,darwin}/` и включают образец русской Windows в cp866 — именно эта кодировка ломает наивные парсеры.

View file

@ -7,17 +7,13 @@ Cross-platform CLI for deep network diagnostics: who your ISP actually is, wheth
```console ```console
$ netcheck --quick $ netcheck --quick
netcheck 0.1.0 · auto mode · Windows netcheck 0.1.0 · auto mode · Windows
Verdict: ok (100/100) — No problems found on this connection.
Connection Wi-Fi 2 · 192.168.1.34 · MTU 1500 · gw 192.168.1.1
Egress 203.0.113.44 (AS64500 Example Telecom, Amsterdam NL)
VPN none confidence 0.10
Latency 1.1.1.1 12.4 ms avg · jitter 1.9 ms · loss 0%
Verdict 🟢 healthy — no problems found
Report written to logs/report_AS64500_20260808T191200Z.md Report written to logs/report_AS64500_20260808T191200Z.md
logs/report_AS64500_20260808T191200Z.json logs/report_AS64500_20260808T191200Z.json
``` ```
Everything else — egress ASN, VPN verdict, latency, path, speed — lives in the two files it just wrote, not on the console; the terminal only tells you the headline and where to look.
## Install ## Install
Requirements: Requirements:
@ -35,6 +31,12 @@ uv sync
uv run netcheck uv run netcheck
``` ```
Want `--tcp-trace` and `--ndt7` too? They're optional extras, not installed by default:
```bash
uv sync --extra tcptrace --extra ndt7
```
Without `uv`: Without `uv`:
```bash ```bash
@ -61,14 +63,14 @@ No configuration is required. Copy `.env.example` to `.env` only if you have opt
|---|---| |---|---|
| `--quick` | ~10 s express run: reference-host pings only, no speedtest | | `--quick` | ~10 s express run: reference-host pings only, no speedtest |
| `--full` | ~60–120 s: full speedtest and full MTR cycles | | `--full` | ~60–120 s: full speedtest and full MTR cycles |
| `--target AS64500` / `--target 1.2.3.4` / `--target example.com` | Investigate a specific network instead of your own connection | | `--target AS64500` / `--target 64500` / `--target 1.2.3.4` / `--target example.com` | Investigate a specific network instead of your own connection — the `AS` prefix on a bare ASN is optional |
| `--target-host <ip\|domain>` | Add an extra host to the ping and traceroute fan-out | | `--target-host <ip\|domain>` | Add an extra host to the ping and traceroute fan-out |
| `--speedtest-server <id\|url>` | Pin the speedtest to a specific server | | `--speedtest-server <id\|url>` | Pin the speedtest to a specific server |
| `--watch` | Continuous monitoring with a live dashboard — for catching intermittent drops | | `--watch` | Continuous monitoring with a live dashboard — for catching intermittent drops. Writes one `logs/watch_*.json` time series per session, not one report per tick |
| `--compare a.json b.json` | Diff two earlier reports: before/after a VPN switch, before/after an ISP call | | `--compare a.json b.json` | Diff two earlier reports: before/after a VPN switch, before/after an ISP call. Read-only — no probing, no network calls |
| `--dnsbl` | Opt in to classic DNSBL reputation zones | | `--dnsbl` | Opt in to classic DNSBL reputation zones |
| `--ndt7` | Opt in to M-Lab NDT7 (publishes your measurement, including your IP, as CC0 open data) | | `--ndt7` | Opt in to M-Lab NDT7 (publishes your measurement, including your IP, as CC0 open data). On an interactive terminal it prints the consent notice and asks first; on a non-interactive one it proceeds without asking |
| `--tcp-trace` | Opt in to a scapy TCP-SYN traceroute through ICMP-filtering middleboxes (needs Npcap or root) | | `--tcp-trace` | Opt in to a scapy TCP-SYN traceroute through ICMP-filtering middleboxes (needs the `tcptrace` extra plus Npcap on Windows or root on Unix). Falls through to the normal cascade when it cannot run |
Every run writes two files into `./logs/`: a Markdown report to read and a JSON dump containing every raw provider response, untruncated. `./logs/` is gitignored — reports contain your external IP, city, coordinates and ISP name. Every run writes two files into `./logs/`: a Markdown report to read and a JSON dump containing every raw provider response, untruncated. `./logs/` is gitignored — reports contain your external IP, city, coordinates and ISP name.

View file

@ -6,18 +6,14 @@
```console ```console
$ netcheck --quick $ netcheck --quick
netcheck 0.1.0 · авто-режим · Windows netcheck 0.1.0 · auto mode · Windows
Verdict: ok (100/100) — No problems found on this connection.
Соединение Wi-Fi 2 · 192.168.1.34 · MTU 1500 · шлюз 192.168.1.1 Report written to logs/report_AS64500_20260808T191200Z.md
Внешний IP 203.0.113.44 (AS64500 Example Telecom, Амстердам NL)
VPN нет уверенность 0.10
Задержка 1.1.1.1 12.4 мс сред. · джиттер 1.9 мс · потери 0%
Вердикт 🟢 всё в порядке — проблем не найдено
Отчёт записан в logs/report_AS64500_20260808T191200Z.md
logs/report_AS64500_20260808T191200Z.json logs/report_AS64500_20260808T191200Z.json
``` ```
Всё остальное — ASN, вердикт по VPN, задержки, маршрут, скорость — попадает в два только что записанных файла, а не в консоль; терминал показывает только заголовок и куда смотреть дальше.
## Установка ## Установка
Требования: Требования:
@ -35,6 +31,12 @@ uv sync
uv run netcheck uv run netcheck
``` ```
Нужны `--tcp-trace` и `--ndt7`? Это опциональные extras, по умолчанию не ставятся:
```bash
uv sync --extra tcptrace --extra ndt7
```
Без `uv`: Без `uv`:
```bash ```bash
@ -61,14 +63,14 @@ netcheck
|---|---| |---|---|
| `--quick` | Экспресс-прогон ~10 с: только пинги эталонных хостов, без замера скорости | | `--quick` | Экспресс-прогон ~10 с: только пинги эталонных хостов, без замера скорости |
| `--full` | 60–120 с: полный замер скорости и полный MTR | | `--full` | 60–120 с: полный замер скорости и полный MTR |
| `--target AS64500` / `--target 1.2.3.4` / `--target example.com` | Исследовать конкретную сеть вместо своего подключения | | `--target AS64500` / `--target 64500` / `--target 1.2.3.4` / `--target example.com` | Исследовать конкретную сеть вместо своего подключения — префикс `AS` перед голым номером ASN необязателен |
| `--target-host <ip\|domain>` | Добавить хост в веер пингов и трассировок | | `--target-host <ip\|domain>` | Добавить хост в веер пингов и трассировок |
| `--speedtest-server <id\|url>` | Зафиксировать конкретный сервер замера скорости | | `--speedtest-server <id\|url>` | Зафиксировать конкретный сервер замера скорости |
| `--watch` | Непрерывный мониторинг с живой панелью — чтобы поймать плавающие обрывы | | `--watch` | Непрерывный мониторинг с живой панелью — чтобы поймать плавающие обрывы. Пишет один тайм-ряд `logs/watch_*.json` за сессию, а не отдельный отчёт на каждый тик |
| `--compare a.json b.json` | Сравнить два прошлых отчёта: до/после смены VPN, до/после звонка провайдеру | | `--compare a.json b.json` | Сравнить два прошлых отчёта: до/после смены VPN, до/после звонка провайдеру. Только чтение — без проб и сетевых запросов |
| `--dnsbl` | Включить проверку по классическим зонам DNSBL | | `--dnsbl` | Включить проверку по классическим зонам DNSBL |
| `--ndt7` | Включить M-Lab NDT7 (замер публикуется как открытые данные CC0, включая ваш IP) | | `--ndt7` | Включить M-Lab NDT7 (замер публикуется как открытые данные CC0, включая ваш IP). В интерактивном терминале сначала печатает уведомление о согласии и спрашивает подтверждение; в неинтерактивном — продолжает без вопроса |
| `--tcp-trace` | Включить TCP-SYN-трассировку через scapy сквозь middlebox'ы, режущие ICMP (нужен Npcap или root) | | `--tcp-trace` | Включить TCP-SYN-трассировку через scapy сквозь middlebox'ы, режущие ICMP (нужны extras `tcptrace` и Npcap на Windows или root на Unix). Если не может запуститься — откатывается на обычный каскад |
Каждый прогон пишет в `./logs/` два файла: Markdown-отчёт для чтения и JSON-дамп со всеми сырыми ответами провайдеров без усечения. `./logs/` в gitignore — отчёты содержат ваш внешний IP, город, координаты и название провайдера. Каждый прогон пишет в `./logs/` два файла: Markdown-отчёт для чтения и JSON-дамп со всеми сырыми ответами провайдеров без усечения. `./logs/` в gitignore — отчёты содержат ваш внешний IP, город, координаты и название провайдера.