Skip to content

MeshCore deployment models B/C: flood-scope + dedicated preset (#24) - #167

Merged
M0LTE merged 2 commits into
masterfrom
meshcore-deployment-models
Jul 1, 2026
Merged

MeshCore deployment models B/C: flood-scope + dedicated preset (#24)#167
M0LTE merged 2 commits into
masterfrom
meshcore-deployment-models

Conversation

@M0LTE

@M0LTE M0LTE commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

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.

Model Config Isolation
A unscoped public preset REGION=uk-narrow, no scope none (leans on the good-citizen controls)
B scoped public preset REGION=uk-narrow + FLOOD_SCOPE_KEY=<name> repeaters without the scope drop our floods
C dedicated preset REGION=custom + CUSTOM_PRESET=freq=…;bw=…;sf=…;cr=…;pwr=… total physical isolation (own freq/SF)

How

Model CRegions.ParseCustom turns a freq=868.4;bw=62.5;sf=8;cr=8;pwr=14 spec into a range-validated RegionPreset; TX power already clamps to the preset max.

Model B — source-verified against companion-v1.16.0. MeshCoreClient.SetFloodScopeAsync sends CMD_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_KEY is a region name hashed SHA256("#"+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.

PlumbingMeshCoreCustomPreset + MeshCoreFloodScopeKey threaded through SystemOptions → Store (Parse+Save) → DbStartup → Settings UI + JS → BuildOptions; a DeploymentModel() 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.)

TestsMeshCoreDeploymentModelTests (17: custom-preset parse/validate, flood-scope key derivation, region resolution, DeploymentModel classification); MeshCoreConfigTests extended to round-trip both new options through the store.

🤖 Generated with Claude Code

M0LTE and others added 2 commits July 1, 2026 09:42
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
@M0LTE
M0LTE merged commit 8baf5fa into master Jul 1, 2026
4 checks passed
@M0LTE
M0LTE deleted the meshcore-deployment-models branch July 1, 2026 09:58
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