ctf-ad-mirror-tool-rust/findings.md

41 lines
5.4 KiB
Markdown

# Findings and decisions
## Legacy behavior (source verified)
- Source: `../ad_scripts/mirror_tool`, commit `3011813` (2026-05-16); Downloads copy identical for 12 tracked files.
- Flask panel :6700, one subprocess per proxied port, threads per connection/replica.
- Primary returns responses; replica responses are never drained, so mirroring can stall.
- Replica queue drops oldest chunks on overflow (corrupts TCP application stream).
- Primary socket connect timeout remains a 3s read/write timeout.
- Shared stop flag breaks TCP half-close; peer direction can stop prematurely.
- `_pick_candidate` is undefined and its apparent body sits after an unconditional return.
- UI stops/restarts forwarders for primary/replica edits despite live-retarget backend support.
- Counters estimate speed from total bytes / time since last update, not interval deltas.
- Global mutable process state, no route-state restart recovery, unacknowledged stdin controls, startup success before bind.
- Canonical ForcAD `pomo-mondreganto/ForcAD`, inspected SHA `6ce3407be569b7f69f879501c5e2ca79231c1ff9`. Engine.IO 4 / Socket.IO namespace `/game_events`.
- Crucial legacy bug: `init_scoreboard` envelope contains `{data:{teams,tasks,state}}`; `update_scoreboard` contains `{data:{round,round_start,team_tasks,...}}` (state only). Keep cached metadata and merge updates. Sources: backend/services/events/app.py, backend/lib/storage/game.py, front/src/store/index.js.
- Use persistent Engine.IO polling (the upstream explicitly supports it), ping/pong, namespace ack, bounded HTTP bodies, bounded reconnect backoff and freshness based on received scoreboard data. No per-cycle Socket.IO reconnect.
- adplatf uses GET `/state`; map UP/CORRUPT/MUMBLE/DOWN to 101/102/103/104.
## Architecture direction
- Tokio tasks, bounded buffers, shared immutable route snapshots; no process/thread per connection.
- Direct paths reuse read buffers; mirrored paths share `Bytes` chunks across bounded channels. Always drain replica replies.
- Explicit strict/isolate replica policy; no silent chunk dropping. Strict backpressure trades throughput for bounded reliable delivery.
- Health and fresh scoreboard inform deterministic candidate selection with hysteresis/cooldown; no switch solely from stale/missing evidence.
- Axum control plane, embedded static UI, single authenticated SSE stream instead of four polling loops.
- No kernel routing work: user explicitly chose internal upstream routing.
## Final restoration and review
- Original base CSS retained (24037 characters modulo trailing whitespace); matching Bootstrap5.1.3/Font Awesome6.0.0 bundled locally. Main panel keeps original columns/rows/controls and four counters.
- Top status strip represents own-team main_team_ip independently of primary. Direct custom IP resolves its target atomically through standard service PUT.
- R1-R3 corrected with negative-control regressions; R4 redesigned journal removed; R5 old primary retained as replica. Absolute heartbeat and recoverable accept supervision also implemented.
- Final review discovered R6: cancellation of an HTTP request can cancel the controller future between disk/runtime/publication steps. Fix transaction ownership rather than merely trying to keep HTTP sockets alive.
- Browser Use fill does not trigger a native dirty-field change on blur in this environment; direct-IP behavior was verified with keyboard typing and Tab instead. Browser-wide viewport override was ineffective; tab-scoped CDP metrics produced actual390x844 and was cleared after verification.
- R6 resolved by one owned admitted transaction; cancelled waiters allocate no worker. Five cancellation regressions plus independent actual HTTP red/green prove cleanup/publication/rollback convergence. Final review approved, no remaining Critical/Important.
2026-10-23 flag protection: engine currently sends client bytes unchanged before replica fanout and primary bytes unchanged on return. ServiceConfig strict fields require adding defaulted opt-in and updating UI full-PUT serializer. A same-length authenticated ciphertext cannot generally fit original flag domain; use random format-preserving tokens plus encrypted durable map. Mapping must commit before upstream writes. SQLite encrypted records give bounded incremental durable writes; a bounded actor owns DB/maps, avoiding locks across network I/O. Generic TCP has no message boundaries: partial matching tails require bounded timeout and documented protocol limitation.
Flag implementation: first6 synthetic tests passed (all byte splits, restart/wrong-key/tamper, capacity/SQL write failure,24 concurrent repeats, TCP primary/replica byte identity, opt-out/new-vs-existing sessions, incomplete-tail timeout). Replaced naive per-position matching with precompiled byte regex to avoid repeated full-format checks over each input buffer. Actor serializes only detected matches, no lock is held across network I/O. Existing data path remains buffer reuse when disabled.
Final durability review: use SQLite rollback-journal synchronous=EXTRA, not FULL, so journal deletion also syncs its directory before acknowledging a new mapping. No live entries evicted; expired mappings intentionally become unknown and pass through, documented. Global format is fixed per vault, changing it with existing DB fails startup. Worker status/counters never include original/token/key/path. User explicitly reiterated per-service opt-in, default false; browser verified initial off, on round-trip, persisted off and cache bypass after disabling.