Skip to content

feat(meshcore): scope all originated flood traffic — phase 2 (#3667)#3673

Merged
Yeraze merged 2 commits into
mainfrom
feature/meshcore-scope-phase2
Jun 23, 2026
Merged

feat(meshcore): scope all originated flood traffic — phase 2 (#3667)#3673
Yeraze merged 2 commits into
mainfrom
feature/meshcore-scope-phase2

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 2 of MeshCore region/scope support (#3667), building on the merged Phase 1 (#3669). Phase 1 scoped channel/DM messages; this extends the same per-source scope mutex to every other flood-originating send, so MeshMonitor is fully usable in a region denyf * mesh (which drops un-scoped flood traffic).

Changes

  • New helpers on MeshCoreManager:
    • runSerialized(fn) — serialize a send on the per-source scope lock (extracted from Phase 1's inline chain; sendMessage now uses it).
    • sendWithDefaultScope(fn) — assert the source default scope (serialized) then run fn; if the scope can't be asserted, fn does not run (nothing leaves un-scoped).
  • Applied sendWithDefaultScope to the originated-flood sends:
    • sendAdvert (companion branch) — repeaters scope via their own region config, so the repeater branch is untouched.
    • loginToNode (remote admin login)
    • requestRemoteTelemetry
    • sendCliCommand (remote CLI)
  • Transitively covered: auto-announce / timer advert bursts and auto-ack/announce messages already route through sendAdvert / sendMessage.
  • Intentionally exempt: node discovery — it's zero-hop and never repeater-forwarded, so scope is irrelevant.

Why these and not others

Config/local-only commands (set_channel, radio params, get_*, reboot, etc.) don't transmit a packet onto the mesh, and direct-routed traffic (trace along a known path) isn't flood-forwarded — neither needs a scope assertion.

Testing

  • tsc server typecheck clean
  • ✅ Full Vitest suite green (7334 passed, 0 failed)
  • New tests: advert/login/telemetry assert default scope before the command; unscoped fallback; advert-concurrent-with-channel-send serialization (each scope asserted immediately before its own send); scope-assertion failure aborts the advert.

Follow-up

  • Phase 3: region discovery from nearby repeaters (companion-protocol mechanism still TBD).

Part of #3667.

🤖 Generated with Claude Code

Phase 1 scoped channel/DM messages. Phase 2 extends the same per-source scope
mutex to every other flood-originating send so MeshMonitor is fully usable in a
`region denyf *` mesh (which drops un-scoped flood traffic):

- Add `runSerialized` + `sendWithDefaultScope` helpers; refactor `sendMessage`
  to use `runSerialized`.
- Assert the source default scope (serialized) before: companion adverts
  (`sendAdvert`), remote logins (`loginToNode`), telemetry requests
  (`requestRemoteTelemetry`), and remote CLI commands (`sendCliCommand`).
  Auto-announce / timer advert bursts and auto-ack/announce messages are
  covered transitively (they call `sendAdvert`/`sendMessage`).
- Node discovery is intentionally exempt — it is zero-hop and never
  repeater-forwarded, so scope is irrelevant.
- If the scope can't be asserted the send does not go out un-scoped.

Tests: advert/login/telemetry assert default scope before the command;
unscoped fallback; advert-concurrent-with-channel-send serialization;
scope-failure aborts the advert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(meshcore): scope all originated flood traffic — phase 2

Tasks:

  • Gather context
  • Read changed files
  • Analyze code quality, bugs, performance, security
  • Review test coverage
  • Post review feedback

Overall Assessment

This is a well-structured, incremental PR. The core abstraction (runSerialized + sendWithDefaultScope) is clean and the scoping logic is applied consistently. The comments clearly explain the protocol rationale. A few items worth noting:


Code Quality

runSerialized extraction (meshcoreManager.ts:1985–1988) — Good refactor. Extracting the promise-chain pattern into a named helper makes the serialization intent explicit, eliminates the duplicate chain-maintenance code from Phase 1, and provides a single place to reason about lock semantics.

sendWithDefaultScope (meshcoreManager.ts:2000–2005) — The implementation is correct and the layering (runSerializedapplyFloodScope → caller's fn) mirrors performScopedSend closely enough to be immediately recognizable. One minor style note: await this.resolveScopeForSend(undefined) could just be await this.resolveScopeForSend() since channelIdx has a default of undefined, but this is trivial.


Potential Bug / Concern

sendCliCommand — double-lock nesting (meshcoreManager.ts:3119–3174)

sendCliCommand has its own per-prefix lock (cliCommandLocks), and runCliCommandLocked is called while holding that lock. Inside runCliCommandLocked, the void this.sendWithDefaultScope(...) call will then queue onto sendScopeLock. This creates a two-level lock chain:

cliCommandLocks[prefix] → sendScopeLock

In the current implementation this is safe because cliCommandLocks is per-prefix and sendScopeLock is global-per-source, and neither lock holder awaits the other lock before releasing its own. However, if a future refactor makes runCliCommandLocked await the sendWithDefaultScope call (rather than fire-and-forget), it would be easy to create a deadlock scenario where the outer cliCommandLocks lock is held while indefinitely waiting for sendScopeLock which is blocked by another op that tries to acquire cliCommandLocks.

This isn't a bug today, but the void pattern here (intentional fire-and-forget) means the scope-assertion failure path rejects the outer promise via the .catch() chain — the Promise rejection propagates correctly through reject(err). Worth a brief comment noting this is intentionally fire-and-forget to avoid the two-lock nesting problem.


Test Coverage

Good coverage for the happy paths:

  • Advert, login, telemetry all assert default scope before send ✅
  • Unscoped fallback (null) when no default scope set ✅
  • Concurrent advert+channel send serialization ✅
  • Scope-assertion failure prevents advert ✅

Missing test: sendCliCommand scope assertion

sendCliCommand is listed in the PR summary as one of the four flood-originating sends, but there is no test asserting that set_flood_scope is called before send_cli. The test infrastructure (makeManager) would support it via bridgeCalls. Given the fire-and-forget complexity in runCliCommandLocked, a test here would be particularly valuable — especially the failure case where scope assertion fails (should reject the outer promise, not silently drop the error).

Serialization test (scope.test.ts:219) — The concurrent advert+channel test is the strongest test in this PR. The assertion that ['scope:berlin', 'send_advert', 'scope:muenchen', 'send_message'] appears in order under Promise.all is a meaningful proof of the mutex property. However, because the underlying sendBridgeCommand mock is synchronous (returns immediately via async with no actual async delay), this test doesn't fully exercise the interleaving scenario — both sends will happen to complete before any microtask yields. It's still valuable as a regression test, but it might not catch a real concurrency bug introduced later if the mock remains synchronous.


Correctness / Design

Repeater exemption in sendAdvert (meshcoreManager.ts:2106–2114) — The design choice to leave the repeater branch unmodified (it uses region config directly) is correct and well-commented.

loginToNode scope wrapping (meshcoreManager.ts:2748) — The scope is applied even when the node has a known direct path (where scope is inert per the comment in performScopedSend). This is consistent behavior and the comment accurately notes this is harmless for direct-routed traffic.

Error propagation from sendWithDefaultScope — When applyFloodScope throws, the error propagates out of runSerialized and the lock chain is maintained correctly (the task.then(() => undefined, () => undefined) ensures a failed scope assert doesn't wedge the queue). Verified across sendAdvert (catches → returns false), loginToNode (catches → returns false), requestRemoteTelemetry (catches → returns null), and sendCliCommand (catches → rejects outer promise). All handled correctly.


Summary

Approve with one suggestion: Add a test for sendCliCommand scope assertion to match the coverage for the other three flood-originating sends. The existing test for scope-failure on sendAdvert is good; an analogous one for sendCliCommand (particularly verifying the reject path on scope-assertion failure) would complete the coverage.

The void fire-and-forget in runCliCommandLocked is an existing pattern that works correctly here, but a comment explaining it's intentional (to avoid two-lock nesting) would help the next reader.


…on test (#3667)

Address Claude review feedback on PR #3673:
- Add tests that sendCliCommand asserts the default scope before send_cli, and
  that a failed scope assertion rejects the command without sending.
- Strengthen the advert/channel serialization test: set_flood_scope now awaits a
  real macrotask (floodScopeDelayMs) so the lock is exercised under genuine
  interleaving, not just synchronous mock resolution.
- Comment that the CLI send is intentionally fire-and-forget to avoid nesting
  the per-prefix cliCommandLocks with the global sendScopeLock.
- Trivial: resolveScopeForSend() instead of resolveScopeForSend(undefined).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@Yeraze

Yeraze commented Jun 23, 2026

Copy link
Copy Markdown
Owner Author

Thanks! Addressed in 0a0d342:

  • Missing sendCliCommand coverage — added two tests: (1) set_flood_scope is asserted before send_cli, and (2) a failed scope assertion rejects the command without sending (the reject path you called out).
  • Synchronous-mock concern on the serialization test — strengthened it: set_flood_scope now awaits a real macrotask (floodScopeDelayMs), so the advert/channel ordering is exercised under genuine interleaving — a broken lock would now let the second send slip in and fail the assertion.
  • void fire-and-forget — added a comment explaining it's intentional: we're inside the per-prefix cliCommandLocks chain, and awaiting the global sendScopeLock would nest the two locks; keeping it non-awaited means neither lock is held while waiting on the other, and a scope-assert failure still surfaces via the .catch.
  • TrivialresolveScopeForSend() instead of resolveScopeForSend(undefined).

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: feat(meshcore): scope all originated flood traffic — phase 2

Tasks:

  • Gather context
  • Read changed files
  • Analyze code quality, bugs, performance, security
  • Review test coverage
  • Post review feedback

Overall Assessment

This is a clean, well-considered follow-up commit (0a0d342) that directly addresses all four concerns raised in the previous review. The implementation is correct, the added tests are meaningful, and the explanatory comments are thorough. Ready to merge.


Changes in the Follow-Up Commit (0a0d342)

sendCliCommand tests added (meshcoreManager.scope.test.ts:227–241) — Both cases are now covered:

  1. set_flood_scopesend_cli ordering is asserted (line 233)
  2. Scope-assertion failure rejects the outer promise without sending (lines 237–241) — the reject(err) path through .catch is verified

The test at line 231 uses timeoutMs: 50 so the promise resolves quickly (timeout fires before a CLI reply arrives in the test harness), which is a clean way to observe the scope-assert/send ordering without needing to mock a reply.

Concurrent serialization test strengthened (meshcoreManager.scope.test.ts:247) — floodScopeDelayMs: 5 forces a genuine macrotask yield between set_flood_scope and the subsequent send. This means a broken lock would let a concurrent send slip in between scope:berlin and send_advert, causing the sequence assertion at line 257 to fail. This is materially stronger than the previous synchronous version.

void fire-and-forget comment (meshcoreManager.ts:3175–3180) — The comment is accurate and complete. It correctly explains:

  • Why await is avoided (would nest cliCommandLockssendScopeLock)
  • That scope-assert failures still surface via .catchreject(err) → outer promise

Trivial: resolveScopeForSend() — Changed from resolveScopeForSend(undefined) to resolveScopeForSend() at line 2002.


One Minor Observation

loginToNode scope comment (meshcoreManager.ts:2747) — The comment says "carries the default scope (#3667)" but doesn't mention the "direct path" nuance that performScopedSend documents. This is fine since the performScopedSend comment at lines 2049–2053 already covers the "scope is inert when path is known" rationale in detail; no reason to repeat it everywhere.

Nothing new raised — the lock-nesting concern, synchronous-mock weakness, and missing CLI test are all resolved. No new concerns from reviewing the current state of the files.


Summary

All four items from the previous review are addressed. The implementation is correct, the comment on the void pattern is helpful for future readers, and the strengthened concurrency test now exercises real interleaving.

Approve.

View job

@Yeraze Yeraze merged commit 69bff1b into main Jun 23, 2026
20 checks passed
@Yeraze Yeraze deleted the feature/meshcore-scope-phase2 branch June 23, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant