netsleuth/ARCHITECTURE.ru.md
2026-08-08 21:24:33 +03:00

13 KiB
Raw Blame History

Архитектура

English

netcheck собирает сетевые факты из множества независимых, в основном безключевых источников, сводит их в типизированные структуры, интерпретирует чистыми функциями и на каждый прогон отдаёт два артефакта. Сбор, интерпретация и рендер никогда не смешиваются: модуль либо собирает данные, либо рассуждает об уже собранных — но не одновременно.

Структура каталогов

src/netcheck/
  cli.py          Typer-приложение: флаги, порядок фаз, прогресс Rich. Без бизнес-логики.
  config.py       Загрузчик pydantic-settings. Приоритет: env > .env > config.yaml > default.
  models.py       Все общие датаклассы плюс to_jsonable() для сериализации.
  orchestration.py  run_module(): тайминг, таймаут, классификация исключений в ProbeError.
  netinfo.py      Определение возможностей и локальные факты: интерфейс, шлюз, MTU, резолверы.
  ip_geo.py       Цепочка из шести провайдеров, нормализаторы, пополевое слияние.
  bgp.py          RIPEstat, CAIDA ASRank, Team Cymru через DNS, PeeringDB (кэш на диске).
  reputation.py   Кэш netset'ов FireHOL и локальный поиск, Shodan InternetDB, декодер DNSBL.
  stats.py        Чистая статистика RTT: потери, min/avg/max, mdev, джиттер.
  traceparse.py   Чистые парсеры текста в TraceHop для Windows, Linux и BSD/macOS.
  interpret.py    Чистый движок вердиктов: пороги -> Finding[], скоринг VPN, оценка bufferbloat.
  speed.py        Каскадный замер скорости, математика пропускной способности, разбор cfL4, bufferbloat.
  exporter.py     Рендер JSON и Markdown, атомарная запись в ./logs/.
  compare.py      --compare: сравнение двух сохранённых JSON-отчётов.
  watch.py        --watch: цикл периодических прогонов с живой панелью Rich.
  probes/
    latency.py    Веер пингов и статистика джиттера/потерь/mdev.
    traceroute.py Каскад: mtr --json -> icmp_win -> icmplib -> системный бинарник.
    icmp_win.py   Движок ctypes IcmpSendEcho2 / Icmp6SendEcho2 (только Windows).
    dns_leak.py   Перечисление резолверов по адаптерам, echo-пробы, детект ECS.

Поток данных

                +-- netinfo (локальные факты, возможности)
                |
  cli.py -------+-- цепочка ip_geo  ---> внешний IP + ASN   [блокирующая фаза: всё дальше зависит от ASN]
                |
                +-- bgp || reputation || dns_leak         (параллельно)
                |
                +-- latency || веер traceroute            (параллельно, ограничено семафором)
                |
                +-- speed                                  (эксклюзивная фаза)
                |
                +-- interpret  ---> Finding[] + общий вердикт
                |
                +-- exporter   ---> logs/report_<ASN>_<ts>.md + .json

Два жёстких ограничения по конкурентности:

  1. Изоляция измерений. Замер скорости никогда не пересекается с трассировкой, пингами и запросами к API — он держит эксклюзивную блокировку фазы. Единственное намеренное исключение — проба bufferbloat, которая по замыслу работает во время насыщения канала, внутри speed.py.
  2. Ограниченный веер трассировок. Параллельные трассировки к разным целям делят первые хопы и завышают задержки друг друга, поэтому число одновременных трассировок ограничено семафором (probing.trace_concurrency, по умолчанию 2).

Ключевые решения

Отказ — это значение, а не поток управления. Каждый вызов модуля проходит через run_module(): он замеряет время, применяет таймаут на модуль, перехватывает всё кроме CancelledError и возвращает ModuleResult со статусом ok | partial | failed | skipped. asyncio.gather(..., return_exceptions=True) — вторая страховка на уровне оркестрации. Ни один сбой провайдера не может прервать прогон, и оба формата вывода показывают каждый сбой явно: отчёт без раздела скорости не должен выглядеть так же, как отчёт, где со скоростью всё было хорошо.

Никогда не требовать повышения прав. Определение возможностей построено на попытках, а не на предположениях об ОС: сначала непривилегированный datagram ICMP, затем raw ICMP, затем (на Windows) API IcmpSendEcho2, затем откат на тайминг TCP-connect и заголовок Cloudflare Server-Timing: cfL4. В худшем случае инструмент всё равно выдаёт полный отчёт и один раз, простыми словами, сообщает о деградации и способе её устранить.

Трассировка на Windows использует Win32 ICMP API, а не текст tracert.exe. IcmpSendEcho2 из Iphlpapi.dll — тот же непривилегированный API, который tracert.exe использует внутри. Через ctypes он даёт RTT по хопам, определение истёкшего TTL и корректные таймауты без администратора, Npcap и сторонних зависимостей — строго лучше, чем разбор локализованной таблицы из трёх проб.

Разбор текста — крайняя мера, и он опирается на форму строки. Когда системный бинарник — единственный вариант, traceparse.py смотрит на структуру (ведущее целое хопа, токены RTT, регулярка IP) и никогда на слова в заголовке или подвале: вывод tracert зависит от кодовой страницы — cp866 на русской Windows, а не UTF-8. BSD/macOS вдобавок выдаёт несколько IP на хоп и аннотации !H/!N/!X, которых нет в Linux, поэтому три парсера действительно разные.

Замер задержек не разбирает текст ping. Это означало бы ещё три locale-зависимых парсера без всякой пользы. Каждый PingResult помечен использованным методом, потому что «потери» TCP (доля неудачных подключений) и потери ICMP — разные метрики, и молча смешивать их в отчёте нельзя.

Репутация по умолчанию опирается на источники, которые не узнают, что вы проверяете. Netset'ы FireHOL скачиваются один раз, кэшируются и сопоставляются локально — ни запроса наружу, ни лимитов, ни раскрытия. Shodan InternetDB для домашнего пользователя гораздо чаще говорит что-то полезное (открытый роутер, забытый проброс порта), чем почтовый блоклист. Классические DNSBL включаются явно флагом --dnsbl.

Ответы DNSBL в диапазоне 127.255.255.0/24 — это ошибки, а не листинг. Spamhaus возвращает 127.255.255.254 на «вы спросили через публичный резолвер» — а это все, кто сидит на 1.1.1.1 или 8.8.8.8 — и 127.255.255.255 на «превышен лимит». Наивная реализация «любой 127.x.x.x значит в списке» помечает красным большинство пользователей. Оба случая подаются как результат недоступен, но никогда как находка.

Массовые эндпоинты RIPEstat всегда ограничены. routing-history и подобные на крупных провайдерах возвращают 10 МБ+, поэтому каждый такой вызов несёт max_rows и временное окно из config.yaml.

PeeringDB кэшируется на диск. Анонимные лимиты — 20 запросов в минуту, а крупные ответы душатся до одного в час, поэтому ответ не запрашивается повторно внутри прогона и сохраняется в .cache/ между прогонами на providers.peeringdb_cache_hours.

В JSON-дампе есть раздел raw. Каждый ответ провайдера сохраняется дословно под ключом источника. Типизированный слой над ним неизбежно теряет поля; именно raw действительно выполняет требование «100% собранных данных без усечения».

Хранение

  • ./logs/report_<ASN>_<YYYYMMDDTHHMMSSZ>.{md,json} — пара файлов на прогон. Компактный ISO-таймстамп, потому что Windows запрещает : в именах файлов. При полном провале определения ASN — report_unknown_…. Запись через временный файл и os.replace(), всегда атомарно. В gitignore: отчёты содержат внешний IP, город, координаты и название провайдера.
  • ./.cache/firehol/*.netset — скачанные блоклисты, обновляются по providers.firehol_refresh_hours.
  • ./.cache/peeringdb/*.json — ответы PeeringDB, действительны providers.peeringdb_cache_hours.
  • ./config.yaml — все несекретные настройки. ./.env — три опциональных API-ключа и ничего больше.

Конфигурация

config.py собирает объект Settings на pydantic-settings, источники которого упорядочены init > переменные окружения > .env > config.yaml > значение по умолчанию. Три API-ключа объявлены как SecretStr | None = None на верхнем уровне: диагностический инструмент обязан работать с нулевой настройкой, поэтому отсутствие ключа переводит зависящее от него обогащение в skipped с предупреждением и никогда не роняет прогон.

Тесты

uv run pytest -q. Около 120 тестов: парсеры, нормализаторы, движок вердиктов, декодирование DNSBL, математика пропускной способности, статистика пингов, сериализация и сравнение отчётов. Клей намеренно не тестируется: живой HTTP, настоящий ICMP, обвязка Typer, вызовы psutil/ctypes и цикл --watch. Фикстуры трассировок лежат в tests/fixtures/traceroute/{windows,linux,darwin}/ и включают образец русской Windows в cp866 — именно эта кодировка ломает наивные парсеры.