NetTrace — архитектура
Локальный desktop-анализатор PCAP/PCAPNG. Пользователь сам исследует захват; приложение показывает факты (пакеты, поля, потоки, счётчики) и не делает выводов.
Слои
apps/desktop/ Tauri-приложение
src-tauri/ тонкий IPC-слой: команды → engine, события прогресса
src/ React + TypeScript UI (не знает внутренностей парсера)
crates/
model/ DTO-контракт backend ↔ UI (Packet, Flow, Host, Conversation,
Protocol, TimelineEvent, PacketField …). Только serde.
packet/ безопасный байтовый курсор, адреса (MAC/IPv4/IPv6), link types, время
capture/ потоковое чтение PCAP/PCAPNG, запись PCAP/PCAPNG (экспорт), трейт PacketSource
live/ живой захват: Npcap/libpcap загружается в рантайме (единственный
модуль с unsafe — FFI), список интерфейсов, BPF-фильтр захвата
protocol/ диссекторы и реестр (Ethernet, 802.1Q, ARP, IPv4/6, ICMP/v6,
TCP, UDP, DNS, DHCP, HTTP/1.x, TLS, NTP), реестр полей фильтра
flow/ отслеживание TCP/UDP-потоков, TCP-анализ (ретрансмиссии, dup ACK,
out-of-order, zero window, RTT, рукопожатие, FIN/RST)
analysis/ агрегаты: узлы, соединения, иерархия протоколов, I/O-график,
длины пакетов, временная шкала, технические индикаторы
storage/ компактный индекс метаданных пакетов + ленивое чтение байтов из файла
query/ язык display filter: лексер → парсер → AST → типизированная
компиляция по реестру полей → вычисление по трейту FieldSource
engine/ оркестрация сессии: фоновая индексация, представления (фильтр +
сортировка), детали пакета, поиск, статистика
testkit/ построители пакетов и генератор PCAP-фикстур (только для тестов)
Граф зависимостей (стрелка = «зависит от»):
model ◄── protocol ◄──┐
packet ◄── capture │
▲ ◄── protocol ├── engine ◄── desktop (src-tauri)
├──── flow ◄────────┤
├──── storage ◄─────┤
├──── analysis ◄────┤
└──── live ◄────────┤
model ◄── query ◄─────┘
query не знает про индекс и диссекторы: движок реализует FieldSource. flow получает
узкую структуру FlowPacket, а не результат диссекции. UI получает только типы из model.
Конвейер обработки файла
PCAP/PCAPNG ──► capture::CaptureReader (BufReader, последовательно, offset каждой записи)
──► protocol::dissect (режим Summary: без дерева и строк)
──► batch (4096 пакетов; при живом захвате — сразу) ──► write-lock индекса:
storage::PacketIndex.push(PacketMeta 64 байта)
flow::FlowTable.process → stream id, флаги TCP-анализа, RTT
analysis::Accumulators → узлы, соединения, иерархия, события
──► событие прогресса в UI каждые ~100 мс
В памяти хранятся только метаданные. Байты пакета читаются позиционным чтением из файла по требованию: строки видимой части списка, детали выбранного пакета, поля фильтра, которых нет в индексе.
Как устроена память (рассчитано на десятки миллионов пакетов и миллионы потоков):
PacketMeta— 64 байта на пакет; в нём жеnext_in_flow, ссылка на следующий пакет того же потока (список пакетов потока — цепочка, без отдельных векторов).- Адреса интернированы (
AddressTable): в пакетах, узлах и соединениях — u32-номера; хэш-таблицы (hashbrown::HashTable) хранят только номера, ключ читается из вектора. - Поток — около 128 байт; итоги TCP-анализа (
TcpFlow) — 136 байт. Состояние, нужное только пока идут пакеты (окна последовательностей, неподтверждённые сегменты), лежит отдельно и освобождается вFlowTable::finish()в конце индексации. RTT-замеры всех потоков — один общий массив в порядке пакетов. - Узлы (56 байт) и соединения Ethernet/IP (48 байт) — плотные массивы по номерам
адресов. Лимитов на них нет: память растёт линейно с числом пакетов; предохранитель
один — 8 млн потоков (
MAX_FLOWS). - Худший случай (каждый пакет — новое TCP-соединение с нового адреса): ~0,5 КБ на пакет после индексации.
Таблицы «Узлы», «Соединения» и «Потоки» не передаются в UI целиком: движок
фильтрует и сортирует номера строк (engine::tables, порядок кешируется в сессии)
и собирает строки только для запрошенной страницы; UI подгружает страницы по мере
прокрутки (usePagedTable, PageCache). Индикаторы и временная шкала строят
подписи только для того, что попадёт в ответ.
«Содержимое потока» (engine::follow) ничего не хранит заранее: страница собирается
по цепочке пакетов потока (next_in_flow), байты читаются из файла. TCP-сегменты
упорядочиваются по направлениям (Reorder: повторы и перекрытия отбрасываются,
сегменты после «дыры» ждут до 64 штук или 512 пакетов, затем «дыра» считается
потерей). Курсор с этим состоянием запоминается каждые 1 МБ вывода или 25 тыс.
пакетов, так что страница в глубине потока не проходит его с начала; один запрос
смотрит не больше 100 тыс. пакетов. UI держит не больше 8 МБ потока и 200 тыс. строк,
дальше — «Следующая часть» или сохранение в файл.
Пока индексация идёт, UI уже может листать, фильтровать и открывать пакеты (чтение под read-lock, индексатор берёт write-lock только на применение батча).
Живой захват
Npcap/libpcap ──► live::LiveSource ──► поток-рекордер ──► временный PCAP (%TEMP%/NetTrace)
│ (дописывается)
engine::TailReader ◄──┘ ждёт новых байт вместо EOF
└──► тот же индексатор, что и для файлов
Индексатор в режиме захвата применяет каждый пакет сразу, поэтому список, фильтры, потоки и статистика обновляются в реальном времени; отфильтрованное представление пересчитывается раз в секунду. После остановки захват — обычный файл: его можно сохранить (PCAP/PCAPNG); временный файл удаляется при закрытии. Без установленного Npcap приложение работает в режиме файлов и объясняет, что нужно установить.
Display filter
Поля делятся на «индексные» (frame.number, ip.addr, tcp.port, tcp.stream, флаги TCP,
наличие протокола, tcp.analysis.*, frame.time_relative …) — вычисляются по PacketMeta
без чтения файла, параллельно (rayon) — и «глубокие» (dns.qry.name, http.host,
tls.handshake.extensions_server_name, ip.ttl …) — для них пакет читается и
диссектируется в режиме Values (дерево без строк отображения).
Безопасность
PCAP — недоверенный вход. Весь разбор идёт через packet::Cursor с проверкой границ,
без unsafe. Ошибка разбора слоя превращается в узел [Malformed] и не прерывает
обработку файла. Дополнительно каждый вызов диссектора обёрнут в catch_unwind.
Лимиты: размер записи ≤ 256 КиБ, размер блока PCAPNG ≤ 16 МиБ, глубина вложенности
слоёв, число переходов при DNS-компрессии. UI никогда не интерпретирует содержимое
пакетов как HTML/URL, не открывает ссылки и не делает сетевых запросов
(CSP Tauri запрещает внешние источники, плагины shell/opener не подключены).
SQLite
В MVP не используется: индекс в памяти достаточно компактен для миллионов пакетов.
storage скрывает хранение за типом PacketIndex, поэтому персистентный кэш индекса
(SQLite) можно добавить без изменения остальных слоёв.
Расширение
- Новый диссектор: реализовать
protocol::Dissector/AppDissector, зарегистрировать вRegistry::with_builtins, добавить поля вprotocol::fields. UI не меняется — дерево деталей и фильтр работают по общей модели. - Новый источник живого захвата: реализовать
live::LiveSource(как Npcap/libpcap вlive::ffiилиReplaySourceдля тестов). Запись идёт во временный PCAP, который индексируется как обычный файл. - Английский язык:
apps/desktop/src/i18n— словари; диссекторы отдают языконезависимые id полей, подписи переводит UI.