Add firmware settings validation reference#111
Conversation
Add a comprehensive firmware-side settings validation document at standards/audits/data/settings-validation-firmware.md describing defaults, per-field validation rules, module/channel behaviors, role-based overrides, authorization/session passkey rules, and inbound rate limits. Also add a .gitignore entry to ignore .DS_Store. This documents the device-side validation and defaulting logic to complement client-side references and aid audits and developer understanding.
There was a problem hiding this comment.
Pull request overview
Adds a firmware-side settings validation/reference document to complement the existing Android/iOS settings validation docs, improving auditability and developer understanding of which defaults/validation are applied on-device.
Changes:
- Add
settings-validation-firmware.mddocumenting firmware defaults, validation behaviors, authorization/session-passkey rules, and inbound rate limits. - Add
.DS_Storeto.gitignore.
Reviewed changes
Copilot reviewed 1 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
standards/audits/data/settings-validation-firmware.md |
New comprehensive reference for firmware defaulting/validation and related admin/security behaviors. |
.gitignore |
Ignores macOS Finder metadata files (.DS_Store). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | `username` | `"meshdev"` | **none** | | | ||
| | `password` | `"large4cats"` | **none** | nanopb limit 32 bytes. | |
| | `team` | `Unspecifed_Color` (firmware renders Cyan) | **none** (no-op) | `handleSetModuleConfig()` has **no `case` for `tak`** — a `set_module_config` carrying the `tak` variant is silently a no-op (nothing stored, no error returned). TAK config cannot be set via `set_module_config` in this firmware revision. | | ||
| | `role` | `Unspecifed` (firmware renders TeamMember) | **none** (no-op) | Same. | |
jamesarich
left a comment
There was a problem hiding this comment.
Thorough and well-sourced document — cross-checked against the cited firmware commit 91f930d5c. One material bug in the NeighborInfo section (dead-code clamp documented as working), plus a few precision improvements for the session passkey section. Everything else verified correct against source.
| | Field | Default | Validation | Notes | | ||
| |-------|---------|------------|-------| | ||
| | `enabled` | `false` | **none** | | | ||
| | `update_interval` | `0` | **clamp** | If below `14400` (`min_neighbor_info_broadcast_secs`) it is reset to `21600` (`default_neighbor_info_broadcast_secs`). | |
There was a problem hiding this comment.
Bug: This clamp is dead code in the firmware
The AdminModule.cpp handler (lines 1013–1020) checks and clamps the old stored value, then immediately overwrites the entire struct with the inbound payload:
case meshtastic_ModuleConfig_neighbor_info_tag:
moduleConfig.has_neighbor_info = true;
if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) {
moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs;
}
moduleConfig.neighbor_info = c.payload_variant.neighbor_info; // ← overwrites the clamped value
break;The guard never takes effect — any inbound update_interval (including values below 14400) is stored verbatim.
| | `update_interval` | `0` | **clamp** | If below `14400` (`min_neighbor_info_broadcast_secs`) it is reset to `21600` (`default_neighbor_info_broadcast_secs`). | | |
| | `update_interval` | `0` | **none** (intended clamp is dead code) | Firmware checks the *previous* stored value then overwrites with the inbound struct — the intended floor of `14400` (`min_neighbor_info_broadcast_secs`) never takes effect. See `AdminModule.cpp` lines 1016–1020. | |
There was a problem hiding this comment.
@thebentern - this is the call-out i was most concerned over
|
|
||
| **Session passkey** — a state-changing message from a *remote* node must echo | ||
| back an 8-byte `session_passkey` issued on a prior `get_*` response. It is valid | ||
| for 300 s; a mismatch or timeout ⇒ `ADMIN_BAD_SESSION_KEY`. Locally-connected |
There was a problem hiding this comment.
Two precision improvements here:
-
The passkey check condition is
mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)— so remoteget_*requests (classified as "requests" bymessageIsRequest()) also skip the check, not just locally-connected clients. -
The passkey is included in every admin
get_*response (owner, config, module_config, channel, metadata, connection_status, remote_hardware_pins, plus module API responses), not just a single prior response. It regenerates only if 150+ seconds have elapsed since last generation, creating an overlapping validity window (regenerates at 150s, valid until 300s).
| for 300 s; a mismatch or timeout ⇒ `ADMIN_BAD_SESSION_KEY`. Locally-connected | |
| for 300 s; a mismatch or timeout ⇒ `ADMIN_BAD_SESSION_KEY`. Locally-connected | |
| clients skip this check, as do remote `get_*` requests (only non-request, | |
| non-response admin messages are gated). The passkey is regenerated after 150 s | |
| but remains valid for 300 s, creating an overlapping validity window. |
|
|
||
| | Field | Default | Validation | Notes | | ||
| |-------|---------|------------|-------| | ||
| | `team` | `Unspecifed_Color` (firmware renders Cyan) | **none** (no-op) | `handleSetModuleConfig()` has **no `case` for `tak`** — a `set_module_config` carrying the `tak` variant is silently a no-op (nothing stored, no error returned). TAK config cannot be set via `set_module_config` in this firmware revision. | |
There was a problem hiding this comment.
Confirmed accurate — no meshtastic_ModuleConfig_tak_tag case exists in the switch. Worth noting that get_module_config_request for TAK does respond correctly (the response switch at line 1236 is missing it too actually — let me double-check... actually the response handler has no TAK_CONFIG case either in the get_module_config_request handler). So TAK config is fully inert in both directions via AdminModule. Good as documented.
Minor nit: Consider noting that the ModuleConfigType enum does define TAK_CONFIG = 15 in the proto, so clients can send the request — it just does nothing firmware-side.
| | `index` | `0` | **reject** | Must be `0 ≤ index < 8`; otherwise `BAD_REQUEST`. `get_channel_request` is rejected the same way. | | ||
| | `role` | `PRIMARY` | **sanitize** | Setting a channel to `PRIMARY` demotes every other `PRIMARY` channel to `SECONDARY`. | | ||
| | `name` | `""` | **none** | nanopb limit 12 bytes. Empty ⇒ rendered as the modem-preset name. | | ||
| | `psk` | `{0x01}` (default-key shorthand) | **none** | Not validated at write time. `getKey()` later interprets it: `0` = off, `1`–`10` = built-in keys, 16 / 32 bytes = AES-128 / AES-256. A short key (2–15 or 17–31 bytes) is silently **zero-padded** to 16 / 32 — not rejected. | |
There was a problem hiding this comment.
Accurate but could note the timing distinction for SDK implementors:
| | `psk` | `{0x01}` (default-key shorthand) | **none** | Not validated at write time. `getKey()` later interprets it: `0` = off, `1`–`10` = built-in keys, 16 / 32 bytes = AES-128 / AES-256. A short key (2–15 or 17–31 bytes) is silently **zero-padded** to 16 / 32 — not rejected. | | |
| | `psk` | `{0x01}` (default-key shorthand) | **none** | Not validated at write time; stored verbatim. `getKey()` at *read time* interprets it: `0` = off, `1`–`10` = built-in keys, 16 / 32 bytes = AES-128 / AES-256. A short key (2–15 or 17–31 bytes) is silently **zero-padded** to 16 / 32 at use time — not rejected or transformed on write. | |
|
Clanker came up with a couple of potential corrections/nits |
|
|
||
| | Field | Default | Validation | Notes | | ||
| |-------|---------|------------|-------| | ||
| | `public_key` | empty | **sanitize** | Regenerated from the private key when a valid region is set. | |
There was a problem hiding this comment.
Why sanitize public key? It's PUBLIC
Add a comprehensive firmware-side settings validation document at standards/audits/data/settings-validation-firmware.md describing defaults, per-field validation rules, module/channel behaviors, role-based overrides, authorization/session passkey rules, and inbound rate limits. Also add a .gitignore entry to ignore .DS_Store. This documents the device-side validation and defaulting logic to complement client-side references and aid audits and developer understanding.