packmate/docs/ARCHITECTURE_REVIEW_RU.md
2026-10-10 03:46:23 +03:00

319 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Packmate: ревью перед крупным Attack/Defense CTF
Дата: 2026-10-10. Backend: `938031f1dedfbedabef5b049368ed5b6ba6a900a`;
frontend: `8f23d97100f11cd6671bb62aca5ce9b6a898b865`, плюс локальный патч IP-адресов.
Проверку выполнили три сабагента по направлениям capture/decoders, backend/DB/API,
frontend/deployment; основной агент сопоставил результаты с исходниками.
Обновление после ревью: реализована regex-фильтрация по IP и необходимое для неё
исправление live pattern-фильтра (пункт 9: `foundPatternsIds`). Остальные находки
ниже остаются открытыми. По просьбе пользователя работа идёт по одному этапу
с остановкой после проверки; следующий предложенный этап — зацикливание regex
по содержимому (пункт 1).
## Вывод
В текущем виде я бы не использовал Packmate как единственный анализатор трафика
на крупном соревновании. Есть подтверждённые пути зависания обработки, потери
содержимого, неверного отображения и неограниченного роста памяти. Их нужно
исправить до оценки максимальной пропускной способности. Добавление CPU/RAM или
реплик приложения само по себе эти ошибки не устраняет.
Это ревью исходников, а не сертификат производительности. Нагрузочный replay,
PostgreSQL EXPLAIN и испытание live capture не проводились. Предполагаемая цель —
инструмент команды, принимающий трафик её сервисов; центральный сбор всех команд
потребует дополнительно определить изоляцию данных, число sensors и бюджет хранения.
## Текущая архитектура
```mermaid
flowchart LR
NIC[Интерфейс / PCAP] --> PCAP[pcap4j: pcap-loop]
PCAP --> Q[Live: неограниченная очередь]
Q --> P[Один pcap-processor]
P --> M[Потоки целиком в heap]
M --> D[TLS / WebSocket / HTTP]
D --> F[Поиск паттернов]
F --> DB[(PostgreSQL через JPA)]
DB --> WS[Синхронная рассылка SockJS]
WS --> UI[Vue 2 SPA]
UI --> API[REST в том же JVM]
API --> DB
LB[Lookback / retention] --> DB
```
В FILE-режиме обработка идёт inline. Backend, capture, декодирование, REST,
WebSocket и фоновые задачи работают в одной JVM. PostgreSQL хранит метаданные,
payload `bytea` и совпадения: `Stream → Packet → FoundPattern`, плюс связь
`Stream ↔ Pattern`. Vue поставляется внутри JAR. Compose запускает приложение
и одну PostgreSQL на host-network. Конфигурация предполагает один `localIp`,
сервис идентифицируется портом; caches и подписчики локальны для процесса.
## Находки с высоким приоритетом
P1 означает исправление до большого соревнования; P2 — следующий обязательный
этап укрепления. Приоритет учитывает условия воспроизведения, а не измеренную частоту.
Пути ниже относительно корня репозитория; номера строк относятся к проверенной версии.
### 1. P1: обработка может зациклиться на обычном regex
`src/main/java/ru/serega6531/packmate/service/PatternMatcher.java:52–59`:
цикл вызывает `matcher.find(startPos)` и присваивает `startPos = matcher.end()`.
При пустом совпадении (`^`, `a*`) позиция не сдвигается. Для `^` каждый поиск
снова находит диапазон `[0,0)`. Live processor перестаёт сохранять последующие
потоки, очередь продолжает расти.
Исправить продвижение поиска: использовать `Matcher.find()` без ручного сброса
позиции и определить политику пустых совпадений. Ограничить стоимость regex на
недоверенном payload; один timeout вокруг Java regex не гарантирует остановку
вычисления. При необходимости использовать движок с ограниченным временем
поиска/линейной сложностью или изолированный worker.
### 2. P1: один consumer и очередь без лимита
`pcap/LivePcapWorker.java:39`, `pcap/AbstractPcapWorker.java:202`,
`service/StreamService.java:107–141` под `src/main/java/ru/serega6531/packmate/`.
`LinkedBlockingQueue<>` не ограничена практически полезным бюджетом. Единственный
обработчик синхронно декодирует поток, ищет паттерны, сохраняет его и рассылает WS.
Медленная БД, дорогой regex или клиент UI увеличивают backlog захваченных payload
в heap вплоть до OOM.
Разделить этапы, ограничить очереди по байтам и количеству элементов, добавить
spool на диск и явную политику перегрузки. Блокирование callback захвата при
полной очереди само по себе переносит потери в capture/kernel buffer. Нужны
счётчики потерь и возможность повторной обработки сохранённого сырого трафика.
### 3. P1: активные соединения не имеют лимита размера и возраста
`pcap/AbstractPcapWorker.java:180,237`, `tasks/TimeoutStreamsSaver.java:16`.
Сохраняется весь список пакетов; проверяется только idle timeout последнего
пакета. Постоянный WebSocket/UDP/keep-alive поток может расти весь раунд и
появиться в UI только после завершения/простоя. FILE-режим не запускает timeout
saver: UDP и незакрытые TCP могут копиться до EOF большого дампа.
Ввести общий budget активных flow, предел байтов на flow, максимальную длительность
сегмента и периодическое сохранение частей с общим connection ID. Для FILE
эвикция должна использовать время пакетов, а не wall clock обработки дампа.
### 4. P1: отсутствует TCP reassembly по sequence numbers
`pcap/AbstractPcapWorker.java:96`, `utils/PacketUtils.java:21`.
SEQ не учитывается: payload конкатенируется в порядке захвата. Retransmission
дублирует байты, out-of-order меняет содержимое, gaps не обозначаются. Это
влияет на поиск флагов, воспроизводимость запросов и декодирование протоколов.
Нужен двунаправленный reassembly с SYN/SEQ, retransmit/overlap policy, ограниченным
окном reorder и признаками incomplete/gap/truncated. Сохранять исходный capture
и производное представление раздельно. TLS parser также должен получать поток
байтов: сейчас `service/optimization/TlsDecryptor.java:70,275` пытается разобрать
TLS record из каждого отдельного TCP payload до объединения.
### 5. P1: HTTP pipelining теряет данные, распаковка не ограничена
`service/optimization/HttpProcessor.java:34–43,51–63`,
`service/optimization/StreamOptimizer.java:54`.
После merge парсится одно HTTP-сообщение; весь исходный блок заменяется им.
Два запроса в одном блоке теряют второй перед поиском паттернов. Декодированное
тело пишется в растущий `ByteArrayOutputStream`; аргумент `256` у
`writeDecodedTo` — размер рабочего буфера, а не лимит результата.
Потеря второго запроса подтверждена запуском собранного `HttpProcessor` через
временный Java harness: вход 87 байт с `/first` и `/second`, выход 43 байта,
`HTTP_FIRST_PRESENT=true`, `HTTP_SECOND_PRESENT=false`. Ошибочное равенство
ключей flow из раздела P2 также подтверждено исполнением `UnfinishedStream`:
разные пары endpoint имеют `equals=true` и одинаковый hash.
Разбирать все сообщения с учётом framing, сохранять необработанный остаток,
ограничить размер декодированного тела, коэффициент расширения и бюджет обработки.
Оставлять raw bytes доступными независимо от результата декодирования.
Воспроизводимый сетевой PoC находится в `poc/http_gzip_expansion.py`. Он
отправляет gzip HTTP POST на указанный host/port и позволяет задать точный
объём после распаковки. Проверочный запуск с `100MiB` передал 101 942 байта
сжатого тела при ожидаемых 104 857 600 байтах после декодирования (1028,6×).
Инструкция и ограничения: `poc/README.md`.
### 6. P1: страница пакетов может загрузить весь оставшийся поток
`repository/StreamRepository.java:21–27`, `service/StreamService.java:261`.
`LEFT JOIN FETCH p.matches` объединён с `Pageable`. При fetch join коллекции
Hibernate ограничивает результат в памяти: небольшой pageSize не ограничивает
объём прочитанных packets/payload/matches. Повторные страницы большого потока
снова выполняют дорогую загрузку. Это вывод по семантике Hibernate; SQL-трасса
на реальной PostgreSQL в этом ревью не снималась.
Сначала получать страницу ID с SQL LIMIT, затем выбранные packets и matches.
Кандидат индекса: `(stream_id,id)`. Включить
`hibernate.query.fail_on_pagination_over_collection_fetch=true` как регрессионную
защиту. Дополнительно ограничить pageSize и байты ответа на сервере.
### 7. P1: lookback и retention работают слишком крупными транзакциями
`service/StreamService.java:145–159,270,309`, `repository/StreamRepository.java:19`.
Lookback выбирает весь интервал и держит загружаемые packet graphs в одном
persistence context. Derived delete retention удаляет сущности через ORM
cascade, а не ограниченный bulk SQL. Большая история создаёт heap pressure,
длинные транзакции и конкуренцию с ingest. `docker-compose.yml:11` дополнительно
отключает cleanup по умолчанию, позволяя БД расти до заполнения диска.
Lookback: durable jobs, ограниченный executor, курсор, небольшие транзакции,
отмена и прогресс. Retention: ограниченные SQL batches с продуманным FK cascade,
затем временные партиции. Перед удалением партиции избранные потоки нужно
перенести/сохранить отдельно. Политика хранения должна включать бюджет диска.
### 8. P1: общее изменяемое состояние небезопасно для concurrency
`service/PatternService.java:39,67,139`, `service/ServicesService.java:35,76,119`:
HTTP изменяет обычные HashMap, пока capture и async job их читают/итерируют.
Возможны CME и несогласованные настройки. `service/PatternMatcher.java:19–21,141–147`
кеширует общие mutable KMP searchers; `utils/KMPStringSearcher.java:16,46,70`
содержит изменяемый `lastEnd`. Live и lookback могут портить позиции совпадений
друг друга даже после замены map на concurrent variant.
Публиковать immutable snapshots конфигурации после commit. Кешировать только
неизменяемые скомпилированные паттерны; состояние поиска создавать на операцию.
Не увеличивать число capture workers без явного владения состоянием каждого flow.
### 9. P1: live-фильтр по паттерну ломает новые события
`frontend/src/components/Sidebar.vue:206` читает `stream.foundPatterns.some`,
а `model/pojo/StreamDto.java:16` отдаёт `foundPatternsIds`. Воспроизведено запуском
реального метода компонента в Node: корректный DTO с `foundPatternsIds:[1]`
вызывает TypeError; новый поток при активном pattern-фильтре не появляется.
Использовать `foundPatternsIds.includes(Number(route.query.pattern))` и общий
контракт REST/WS с проверкой события активного фильтра.
### 10. P1: UI не ограничивает историю и смешивает ответы разных запросов
`frontend/src/components/Sidebar.vue:35,210`: каждый NEW_STREAM добавляется
в неограниченный массив, весь массив рендерится. При условных 100 events/s за час
это 360 000 элементов, если вкладка успевает принять события. Это арифметический
пример роста, не результат нагрузочного теста.
`frontend/src/views/ContentStream.vue:62–77`: запоздалый ответ A добавляется
в массив после перехода к B. Воспроизведено вызовом исходного метода с отложенным
HTTP-ответом. Аналогичная гонка есть у смены фильтра sidebar.
Ограничить окно истории, виртуализировать списки, пакетировать live updates.
Отменять запросы и проверять generation/request key до изменения данных и
состояния infinite loader. Виртуализация без ограничения массива не решает память.
### 11. P1 при доступности из игровой сети: небезопасные значения запуска
`docker-compose.yml:9–10,20`, `docker/Dockerfile_app:26`: известный fallback
`BinaryBears/123456`, host-network и bind `0.0.0.0`. Если UI-порт достижим
противником и пароль не переопределён, доступны захваченные данные и управление.
Это условный риск конфигурации, а не утверждение о доступности данного компьютера.
Сделать web password обязательным, отделить management network/VPN и capture
interface, настроить TLS для доступа. `configuration/SecurityConfiguration.java:60`
также пишет введённый пароль при ошибке входа — убрать его из логов.
## Дополнительные исправления P2
| Проблема | Место | Что изменить |
|---|---|---|
| Ключ flow независимо переставляет IP и порты: `A:80→B:50000` равен `A:50000→B:80` | `model/pojo/UnfinishedStream.java:21` | Сравнивать пары `(IP,port)` целиком, затем протокол; выделять новые поколения TCP-соединений |
| Shutdown закрывает pcap, но не дренирует очередь и не сохраняет активные потоки | `pcap/LivePcapWorker.java:63` | Остановить producer, drain consumer, flush flows, завершить executors до закрытия БД |
| HEX искажает байты: `FF 80 00` превращается в `FD FD 00` | `frontend/src/components/Packet.vue`, computed `hexdata`, `atou`, `hexdump` | Строить HEX из Uint8Array без UTF-8 преобразования; дефект воспроизведён в Node |
| WS не имеет восстановления пропущенных событий | `service/SubscriptionService.java:53–59`, `frontend/src/App.vue:62` | Отдельная очередь на сессию, последовательная отправка, лимиты, cursor/resync при reconnect/overflow |
| Lookback продвигает checkpoint до выполнения, FINISH уходит до commit | `service/PatternService.java:153–158`, `service/StreamService.java:159` | Durable job/checkpoint после успешного чанка, after-commit/outbox, идемпотентность повторов |
| Lookback не применяет ограничение паттерна по сервису | `service/StreamService.java:148,211` | Одинаковая фильтрация сервиса для live и historical поиска |
| Список streams платит за count и lazy N+1; индексы не соответствуют фильтрам | `service/StreamService.java:302`, `configuration/ApplicationConfiguration.java:62`, `model/Stream.java:33` | Slice/limit+1, projections/batch fetch; проверить индексы service+id, timestamps, favorite через EXPLAIN |
| Minute counters обновляются и очищаются без синхронизации | `service/CountingService.java:20–46` | Атомарный snapshot/reset; раздельные captured/persisted/ignored/dropped counters |
| Размер страницы не ограничен | `model/pojo/PacketPagination.java:12`, `model/pojo/StreamPagination.java:13` | Validation min/max, предел байтов ответа и времени запроса |
| Публикация Docker образа не требует Java test | `docker/Dockerfile_app:10`, `.gitlab-ci.yml:20` | Обязательный test gate и frontend lint/build; npm ci, фиксированные версии образов |
Пути Java в таблице находятся под `src/main/java/ru/serega6531/packmate/`.
CSRF отключён при browser Basic auth (`SecurityConfiguration.java:36`):
пересмотреть защиту изменяющих состояние endpoints; браузерная эксплуатация
в рамках ревью не проверялась. У приложения нет отдельного healthcheck и
использования pcap drop statistics; лог executor раз в минуту не определяет
сохранность захвата. Поддержка RSA TLS в коде не означает поддержку расшифровки
ECDHE/TLS 1.3 без session secrets.
## Предлагаемая архитектура развития
```mermaid
flowchart LR
S[Capture sensors] --> R[Ограниченный дисковый spool / raw segments]
R --> A[Reassembly: владелец каждого flow]
A --> W[Workers: decode и matching]
W --> D[(PostgreSQL: metadata / matches)]
W --> B[(Сжатые payload segments)]
D --> O[Outbox / events]
O --> G[WS gateway с cursors]
G --> U[UI с ограниченным окном]
U --> Q[Query API]
Q --> D
Q --> B
```
Сначала эти границы можно реализовать в одном развёртывании с отдельными
очередями и worker pools. Отдельные сервисы и общая шина нужны после измерений.
Сборщик должен владеть всем состоянием конкретного соединения; распределение
делать по стабильному ключу `(sensor, endpoint A, endpoint B, protocol)`,
с учётом направления и поколения соединения. Shared mutable maps нельзя просто
обслуживать несколькими потоками.
Для нескольких sensors ввести `sensorId`, самостоятельный `serviceId`,
endpoint host/IP+port+protocol, capture timestamps и признаки неполноты.
Сырые данные должны иметь retention и ссылку из метаданных. Вынос payload
из PostgreSQL рассматривать по измеренному IO/объёму; он усложняет атомарность,
удаление, доступ к данным и обработку orphan segments. Kafka/ClickHouse не
обязательны на первом этапе.
## Порядок работ и критерии готовности
1. **Корректность и остановка зависаний.** Исправить regex, KMP concurrency,
endpoint equality, HTTP pipelining, frontend filter/races/HEX. Добавить
проверки на retransmission/reorder/gaps и корректный TCP reassembly.
Acceptance: контрольные байты/флаги воспроизводятся без молчаливого искажения.
2. **Ограниченные ресурсы и восстановление.** Бюджеты очередей/flow/decode/UI,
сохранение частей долгих соединений, spool, graceful shutdown, WS resync.
Acceptance: искусственное замедление БД/браузера не ведёт к бесконечному heap;
пропуски явно измеряются, backlog восстанавливается из доступного spool.
3. **БД и фоновые задачи.** Двухэтапная пагинация, bounded lookback и retention,
индексы по EXPLAIN, durable checkpoints, отдельно сохранённые favorites.
Acceptance: запрос одной страницы ограничен объёмом страницы, а не историей;
фоновые задачи не исчерпывают DB pool и память ingest.
4. **Эксплуатация.** Обязательные credentials, management-доступ, CI gates,
метрики и alerting, проверяемый restart/recovery, управление версией схемы
через миграции вместо единственного `ddl-auto=update`.
5. **Измерение и масштабирование.** PCAP replay на целевом оборудовании с
реальным распределением packets/s, bytes/s, flows/s и размеров flow.
Проверить ожидаемый пик и запас, например 2×, плюс длительный прогон на
продолжительность соревнования. Только после этого обещать конкретный масштаб.
Набор нагрузочных сценариев: короткие TCP, длительные TCP/UDP, retransmit/reorder,
HTTP pipelining и сжатие, TLS segmentation, крупные payload, частые regex matches;
одновременно несколько операторов, lookback и retention. Отдельно проверять
медленный WS-клиент, reconnect, замедление/недоступность БД, перезапуск процесса,
исчерпание бюджета spool и диска. Сравнивать входной manifest пакетов/флагов
с сохранёнными данными, включая корректное устранение retransmission.
Измерять capture/kernel drops, accepted/persisted/ignored/truncated counts,
queue bytes и oldest age, active flow bytes, decode/match время, DB commit
p95/p99, capture→searchable и capture→UI lag, heap/RSS/GC, disk throughput,
retention duration, API p95/p99, browser heap/DOM. Пороговые значения согласовать
с фактическим объёмом и задачами команды. Для ориентира бюджета: непрерывный
1 Gbit/s — около 450 GB за час до сжатия и накладных расходов хранения.
## Границы текущих изменений
В этой рабочей копии реализован запрос на IP-адреса пакетов и добавлены проверки
их сохранения при merge/DTO mapping, включая IPv6 и старые записи. Рекомендации
ревью выше являются планом доработок; архитектура по ним ещё не переписана.
Инструкция по патчу: [IP_ADDRESSES.md](IP_ADDRESSES.md).
Проверки рабочей копии: backend `bootJar` собран; все 5 новых тестов IP прошли
при первом прогоне. Полный прогон первоначально дал 12/13: существующий TLS-тест
падает на CRLF в PEM (`Illegal base64 character d`), потому что `RsaKeysHolder`
удаляет только LF. После нормализации **копии** fixture в `build/resources/test`
на LF и запуска `test -x processTestResources` прошли 13/13. Исходник PEM и TLS
loader не изменялись; Windows-совместимость loader требует отдельного исправления.
Frontend lint изменённого компонента и production build прошли; сборщик
предупреждает о размере vendor bundle. Live capture, миграция схемы и полный
браузерный сценарий с работающим backend не запускались.