mesh,web: add frozen Hello discovery layer for cross-firmware onboarding - #452
Conversation
PreviewURL: https://batterylight-pr-452.blackpond-ef672c92.germanywestcentral.azurecontainerapps.io Hosted on Azure Container Apps — accessible from the internet. |
Two devices on the same channel could fail to find each other whenever their PresenceMsg schema/version differed, since MeshManager silently drops any mismatched frame (by design — see mesh-compatibility.md). That blocked the entire onboarding path for a new device: it must be discoverable, receive a WiFi-config push, and get an update nudge before it's guaranteed to run compatible firmware. Add MsgType::Hello, a minimal, permanently-frozen broadcast (name + fwVersion only) carved out as the one deliberate exception to the same-firmware policy. Every device sends it every heartbeat regardless of onboarding state; PeerRegistry tracks hello-only peers separately from full peers via a new helloOnly flag. Receiving a Hello also feeds ChannelManager's peer-heard signal, so channel-lock convergence no longer depends on PresenceMsg compatibility either. The dashboard gets a "Devices found (setup required)" card for hello-only peers, reusing the existing config-push modal and check-update flow unchanged, since both already operate purely on a target MAC address. Mock server updated for parity.
0ac9076 to
bc01aae
Compare
A hello-only device (incompatible firmware, see mesh-compatibility.md) can't report wifiConnected/hasWifiNetworks via PresenceMsg, so there was no way to tell whether "Check for update" on it could succeed right now versus needing a WiFi-config push first. Extend HelloMsg with those two fields while it's still pre-release (mirrors PresenceMsg's own "reset before first real deployment" convention), and surface the status as a badge on the discovered-devices card.
The old two-button/badge UI (Push config, Check for update, plus a WiFi-status badge) left the sequencing up to the user, never actually installed anything (check-only), and gave no feedback beyond a native alert() on failure. Replace it with a single "Set up device" button that pushes this device's WiFi config only if the target isn't already connected, waits for it to reconnect, then triggers a check-and-install update (TriggerUpdateMsg already does both) — narrating each phase honestly from what HelloMsg actually carries, with bounded stall timeouts instead of a fake progress bar. Two PeerRegistry fixes were required for the dashboard to notice these transitions promptly: updateHello() only fired onChange() for brand-new entries, missing a later wifiConnected flip; update() only fired onChange() based on name/light diffs, missing a device graduating from hello-only to a full peer with no other change. Mock server simulates both transitions (WiFi reconnect after a config push, graduating to a full peer after a triggered update) so the flow is testable without hardware.
…utton flow Leftover text still described the old separate push-config/check-update buttons.
|
Follow-up on the adversarial review findings: 1. Missing compile-time freeze guard — agreed, we should do that. 2. 3. Dead Two ways to fix, pick one:
Either way, the 1.5s poll should stay as a backstop, not the primary mechanism it currently is by accident. 4. Open-redirect via 5. Possible OOB read via non-null-terminated attacker payload — mitigation. 6. Test coverage — filed #454 to track a general review of test coverage across the project (this PR's ~250 new lines of frontend state-machine logic and new mock-server endpoints with no test additions being the trigger example). Generated by Claude Code |
- Add a static_assert freeze guard on HelloMsg's size, so a future edit to that struct fails to compile instead of silently reopening the same-firmware compatibility gate it's meant to be exempt from. - Wire PeerRegistry::setOnChange in main.cpp — it was defined but never called, so the change-detection added earlier (wifiConnected flip, hello-only -> full-peer graduation) never actually pushed a WS update on real hardware; only the frontend's REST poll fallback papered over it. - Fix an open-redirect in the discovered-device card's mDNS link: device name is attacker-controlled (unauthenticated broadcast), so only build the link for names that look like a plain hostname, and percent-encode as defense in depth. - Reject a Hello frame whose name/fwVersion aren't null-terminated within their declared size, instead of letting updateHello()'s strlcpy() scan past the buffer looking for one.
|
Pushed fixes for the review findings (
Left as-is per your note in the follow-up:
Generated by Claude Code |
Follow-up to the PR #452 review discussion — the limit itself is fine, just wasn't documented anywhere user-facing.
Summary
PresenceMsgschema/version differed —MeshManager::_onRecvsilently drops any mismatched frame, by design (same-firmware-only policy, seedocs/mesh-compatibility.md). That blocked the whole initial-setup path for a new device: it must be discoverable, receive a WiFi-config push, and get a firmware-update nudge before it's guaranteed to run compatible firmware.MsgType::Hello/HelloMsg— a minimal, permanently frozen broadcast (name + fwVersion only, no version field, never to gain fields) carved out as the one deliberate exception to the same-firmware policy. Every device sends it every heartbeat regardless of onboarding state.PeerRegistrytracks hello-only peers via a newhelloOnlyflag (reusing the existing registry rather than adding a second one); receiving aHelloalso feedsChannelManager's peer-heard signal, so channel-lock convergence no longer silently depends onPresenceMsgcompatibility either./api/peers(REST + WSpeerspush) now exposes adiscoveredPeersarray for hello-only devices. Dashboard gets a new "Devices found (setup required)" card that reuses the existing config-push modal and check-update flow unchanged, since both already key off a MAC address rather thanPeerRegistrycontents.server/index.js) updated for parity:MOCK_DISCOVERED_PEERSseeded on both the REST route and the WS pushes.docs/mesh-compatibility.mddocuments theHelloexception and why it doesn't reopen the "no compatibility code" rule for the rest of the protocol.Explicitly out of scope (separate, independent issue):
ChannelManager's search sequence only tries channels {1, 6, 11} on a fresh device and never reconverges onto a router's channel outside that set.Test plan
npm run lint— cleannpm test— 36/36 passing/api/peers/checkupdatePRESENCE_MSG_VERSION, confirm mutual discovery via the new card, confirm WiFi-config push and update-check both succeed against a hello-only peer, confirm the device migrates into the normal peers list once it reboots onto matching firmwareGenerated by Claude Code