MeshCore deployment models B/C: flood-scope + dedicated preset (#24) - #167
Merged
Conversation
A private channel gives privacy (PSK) but NOT containment - channel messages
flood unscoped and any same-preset repeater relays them network-wide without
the PSK. Expose preset + scope as first-class operator config so a node can run
model A (unscoped public), B (scoped public), or C (dedicated preset).
Model C - dedicated preset:
- Region "custom" + a spec (freq=868.4;bw=62.5;sf=8;cr=8;pwr=14) parsed into a
validated RegionPreset (range-checked); TX power already clamps to the preset
max. Lets an operator run DAPPS on its own isolated frequency/SF.
Model B - flood-scope (source-verified against companion-v1.16.0):
- Apply Companion CMD_SET_FLOOD_SCOPE_KEY (0x36) at every (re)configure (RAM-only,
reset on reboot, so re-applied after each watchdog recovery). The 16-byte key is
never transmitted - the radio HMACs it to a transport code and marks our floods
scoped, so repeaters/room-servers without a matching region silently DROP them.
- FloodScopeKey is a region NAME hashed SHA256("#"+name)[..16] (MeshCore's public
region-key derivation) so our own repeaters (region put <name>, flood-allowed)
carry it and others drop it; a 32-char hex value is used verbatim. Tolerant of
older firmware: RSP_ERR -> warn + fall back to unscoped (model A), never crash.
- Caveats documented in README "Containment": key is a routing label not a secret
($-private keystore stubbed in v1.16.x), scope is global-per-node, off by default.
Plumbing: MeshCoreCustomPreset + MeshCoreFloodScopeKey threaded through SystemOptions
-> Store (Parse+Save) -> DbStartup -> Settings UI + JS -> bearer BuildOptions; a
DeploymentModel() label in the link-up log.
Validated on air: both radios up on the custom preset (model=C), flood-scope key
accepted by real v1.16 firmware (RSP_OK), scoped nodes exchange 0% loss with
compression + reliability + discovery. Tests: MeshCoreDeploymentModelTests (17) +
MeshCoreConfigTests extended to round-trip both new options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLbwvhE2cKCe8WPZNg8k17
…stency Adversarial review of the deployment-models diff confirmed one high-severity bug plus lower-severity consistency/doc issues. 1. (high) A bad/empty custom preset crashed the daemon. `new MeshCoreLink(opts)` sat OUTSIDE RunAsync's try/catch, and its ctor resolves the region - so an invalid model-C preset (blank, or e.g. sf=99) made ParseCustom throw before the first await, faulting the hosted service and taking down the whole node (crash-loop under systemd) instead of the graceful log-and-disable every other startup failure gets. Construct inside the try so a config error is caught. 2. (low) An all-zero flood-scope key was labelled/logged model B but applied unscoped (SetFloodScopeAsync sends the clear frame for all-zero). ResolveFloodScopeKey now returns null for an all-zero key, and DeploymentModel() consults the resolved key, so label and wire agree. 3. (low) The link-up "model=" line reported configured intent, not effective state: when the radio rejects the scope key it now reads model A (not B), matching the warning logged just above. 4. (low) README documented DAPPS_MESHCORE_* env vars, but the code derives DAPPS_MESH_CORE_* (ToScreamingSnake splits MeshCore -> MESH_CORE). Corrected the whole family so env seeding/override actually fires under the documented names. Test: ResolveFloodScopeKey_AllZeroHex_IsUnscoped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLbwvhE2cKCe8WPZNg8k17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A MeshCore private channel gives privacy (PSK) but not containment — channel messages flood unscoped and any same-preset Repeater/Room-Server relays them network-wide without the PSK. This exposes preset + scope as first-class config so a node runs one of three deployment models.
REGION=uk-narrow, no scopeREGION=uk-narrow+FLOOD_SCOPE_KEY=<name>REGION=custom+CUSTOM_PRESET=freq=…;bw=…;sf=…;cr=…;pwr=…How
Model C —
Regions.ParseCustomturns afreq=868.4;bw=62.5;sf=8;cr=8;pwr=14spec into a range-validatedRegionPreset; TX power already clamps to the preset max.Model B — source-verified against
companion-v1.16.0.MeshCoreClient.SetFloodScopeAsyncsendsCMD_SET_FLOOD_SCOPE_KEY (0x36)at every (re)configure (RAM-only → re-applied after each watchdog recovery). The 16-byte key is never transmitted — the radio HMACs it to a transport code and marks our floods scoped, so repeaters without a matching region drop them.FLOOD_SCOPE_KEYis a region name hashedSHA256("#"+name)[..16](MeshCore's public region-key derivation), so your own repeaters (region put <name>, flood-allowed) carry it and others drop it; a 32-char hex value is used verbatim. Tolerant of older firmware —RSP_ERR→ warn and fall back to unscoped, never crash.Caveats (documented in README "Containment"): the key is a routing label not a secret (
$-private keystore stubbed in v1.16.x), scope is global-per-node, off by default. Model B needs your own scoped Repeater nodes to carry between non-adjacent DAPPS nodes.Plumbing —
MeshCoreCustomPreset+MeshCoreFloodScopeKeythreaded through SystemOptions → Store (Parse+Save) → DbStartup → Settings UI + JS →BuildOptions; aDeploymentModel()label in the link-up log.Validation
On air (radio1 ↔ radio2): both up on the custom preset (
868.4/SF8,model=C), flood-scope key accepted by real v1.16 firmware (flood-scope applied), scoped nodes exchange 0% loss with compression + reliability + discovery. (Repeater drop of out-of-scope floods needs a repeater node — not benchable here; documented.)Tests —
MeshCoreDeploymentModelTests(17: custom-preset parse/validate, flood-scope key derivation, region resolution,DeploymentModelclassification);MeshCoreConfigTestsextended to round-trip both new options through the store.🤖 Generated with Claude Code