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.
interpret.py Pure verdict engine: thresholds -> Finding[], VPN scoring, bufferbloat grade.
speed.py Cascading speedtest, throughput math, cfL4 header parsing, bufferbloat probe.
exporter.py JSON and Markdown rendering, atomic writes into ./logs/. Deliberately over
the file-size guideline to keep both output formats in one place.
exporter.py JSON and Markdown rendering, atomic writes into ./logs/.
compare.py --compare: diff two saved JSON reports.
watch.py --watch: periodic re-run loop with a live Rich dashboard.
probes/
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).
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
```
@ -78,9 +89,10 @@ Two hard concurrency constraints:
## 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/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.
## Configuration
@ -89,4 +101,4 @@ Two hard concurrency constraints:
## 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.
interpret.py Чистый движок вердиктов: пороги -> Finding[], скоринг VPN, оценка bufferbloat.
speed.py Каскадный замер скорости, математика пропускной способности, разбор cfL4, bufferbloat.
exporter.py Рендер JSON и Markdown, атомарная запись в ./logs/. Намеренно превышает
лимит размера файла, чтобы оба формата вывода жили в одном месте.
exporter.py Рендер JSON и Markdown, атомарная запись в ./logs/.
compare.py --compare: сравнение двух сохранённых JSON-отчётов.
watch.py --watch: цикл периодических прогонов с живой панелью Rich.
probes/
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).
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/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-ключа и ничего больше.
## Конфигурация
@ -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
$ netcheck --quick
netcheck 0.1.0 · auto mode · Windows
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
Verdict: ok (100/100) — No problems found on this connection.
Report written to logs/report_AS64500_20260808T191200Z.md
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
Requirements:
@ -35,6 +31,12 @@ uv sync
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`:
```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 |
| `--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 |
| `--speedtest-server <id\|url>` | Pin the speedtest to a specific server |
| `--watch` | Continuous monitoring with a live dashboard — for catching intermittent drops |
| `--compare a.json b.json` | Diff two earlier reports: before/after a VPN switch, before/after an ISP call |
| `--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. Read-only — no probing, no network calls |
| `--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) |
| `--tcp-trace` | Opt in to a scapy TCP-SYN traceroute through ICMP-filtering middleboxes (needs Npcap or root) |
| `--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 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.

View file

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