# 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 не запускались.