feat(network): on-device capture-replay transport + ingestion fuzzing/hardening#5846
Conversation
Adds ReplayRadioTransport: a RadioTransport that replays a pre-captured
FromRadio frame stream (exported from the burningmesh-replay tool via
"replay_server.py --export") entirely on-device - no network, no paired
radio. It honours the two-phase want_config handshake (config nonce ->
config/channels; node-info nonce -> node DB, then streams packets at a
steady configurable rate), injecting config_complete_id with the app's
own nonce per phase.
Selected via the new InterfaceId.REPLAY ('r') and a debug-only
"Demo Mode (Replay)" entry beside Demo Mode in Connections. When the
bundled asset (androidApp/src/debug/assets/burningmesh.fromradio,
gitignored - generated from a private capture) is absent, it falls back
to the synthetic MockRadioTransport so the entry always works.
Purpose: a deterministic, realistic (200-node / 1500-packet) traffic
source for Macrobenchmark / Baseline Profile journeys and populated-UI
tests - the "fake transport" follow-up requested in #5735.
Verified on-device: two-phase handshake completes, 202 NodeInfos accepted
in Stage 2, packets decode through the regular ingestion pipeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds ReplayFuzz, a seeded fuzz toolkit for the on-device FromRadio ingestion path (the same RadioTransportCallback.handleFromRadio entry every BLE/TCP/serial peer feeds), plus a suite asserting the untrusted-input boundary's invariants. It runs under allTests, so the parse/decode boundary is fuzzed on every CI run at zero app-size and zero runtime cost; any failure reproduces from its integer seed alone. Invariants (1000 seeds each): - a corrupt asset fails only with IllegalArgumentException (never OOM, hang, or a leaked okio error) - guards the hardened parser - handleSendToRadio absorbs arbitrary outbound bytes (undecodable ToRadio is dropped, not thrown) - a bit-flipped frame decodes to a catchable Exception, never an Error (an OOM/StackOverflow on the decode path would be a remote DoS) - adversarial-but-valid content replays through the transport intact Scope: covers the self-contained layers with clean oracles (asset parser, handshake input, protobuf decode). The adversarialFromRadio corpus is built to feed a future MeshMessageProcessor integration fuzzer - the post-decode handlers are not yet exception-isolated - while end-to-end framing/UI fuzzing belongs in the capture tool's live --fuzz mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The FromRadio decode boundary was guarded (runCatching + LogRecord
fallback), but the post-decode handler chain (processFromRadio ->
FromRadioPacketHandlerImpl.handleX) and the orchestrator's receive loop
(receivedData.onEach { handleFromRadio }) were not. So a single
malformed-but-decodable packet from any peer in radio range that made a
handler throw would cancel the receivedData collection and silently
deafen the radio for the rest of the session - a remote DoS, and likely a
crash on Android's default coroutine exception handler.
Contain handler failures at both layers with safeCatching (catches
Exception, re-throws CancellationException so structured concurrency is
preserved), logging and dropping the offending packet:
- MeshMessageProcessorImpl.processFromRadio - makes handleFromRadio total
(the root-cause fix).
- MeshServiceOrchestrator receive loop - backstops the single lifeline
that feeds all inbound traffic.
Adds a regression test (a throwing handler does not propagate out of
handleFromRadio) that forces a handler to throw and asserts the call
returns normally; it fails without the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🖼️ Preview staleness check — advisoryThis PR modifies UI composables but does not update any
Changed UI files: What to check:
Adding previews checklist:
If this PR does not require preview updates (e.g., logic-only change, non-visual refactor), add the |
📄 Docs staleness check — advisoryThis PR modifies user-facing UI source files but does not update any page under
Changed source files: What to check:
New page checklist (if adding a new doc page):
If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the
|
On-device packet-ingestion work needs a deterministic, radio-free way to drive realistic mesh traffic into the app, and assurance that the untrusted-input path can't be crashed by a malformed packet. This PR adds an on-device capture-replay transport, a seeded fuzz harness over the ingestion boundary, and the per-packet exception isolation that the fuzzing surfaced as missing.
🌟 New Features
InterfaceId.REPLAY— on-device capture-replay transport. A debug/test-only "Demo Mode (Replay)" connection entry replays a pre-capturedFromRadioframe stream entirely on-device — no network, no paired radio. It honours the two-phasewant_confighandshake (config → node DB → packet stream) and paces packets at a steady rate, giving a deterministic, self-contained traffic source for benchmarks, populated-UI tests, and manual QA. When the (gitignored) capture asset is absent it falls back to the existing syntheticMockRadioTransport, so the entry always works.🛠️ Refactoring & Architecture
IllegalArgumentExceptioninstead of an okio underflow or an oversized allocation;handleSendToRadiotolerates arbitrary inbound bytes (undecodableToRadiois dropped, not thrown). The on-wire asset format is documented authoritatively in theReplayRadioTransportKDoc.🐛 Bug Fixes
FromRadiodecode boundary was already guarded, but the post-decode handler chain (processFromRadio→ the per-variant handlers) and the orchestrator's receive loop (receivedData.onEach { handleFromRadio }) were not. A single malformed-but-decodable packet from any peer in radio range whose handler threw would cancel thereceivedDatacollection and silently deafen the radio for the rest of the session — a remote DoS, and likely a crash on Android's default coroutine exception handler. Both layers now contain handler failures withsafeCatching(which re-throwsCancellationException, preserving structured concurrency), logging and dropping the offending packet.Testing Performed
ReplayFuzz+ReplayFuzzTest(new,core:networkcommonTest): a seeded, deterministic fuzz harness over the ingestion boundary (random / mutated / structurally-valid-but-hostile inputs), asserting four invariants — corrupt assets fail only withIllegalArgumentException;handleSendToRadionever throws; a bit-flipped frame decodes to a catchableException, never anError; adversarial-but-valid content replays intact. Runs underallTestsat zero app-size cost, and every failure reproduces from its integer seed.ReplayRadioTransportTest(extended): added malformed-asset cases (truncated counts, oversized lengths, short/empty sections) alongside the handshake/stream tests — 9 total.MeshMessageProcessorImplTest(extended): addeda throwing handler does not propagate out of handleFromRadio, which forces a downstream handler to throw and asserts the call returns normally — it fails without the fix above.main(detekt2.0.0-alpha.5):spotlessCheck detekt assembleGoogleDebug kmpSmokeCompile, plus:core:network,:core:data, and:core:serviceallTests.