build(mqtt): make the preset table byte-identical across channels

The preset table is fleet state, not a build detail. A slot's preset is stored
in /mqtt.json by name, and firmware that does not know a name does not merely
ignore it: MQTTPrefsSerializer repairs it to "none" and CommonCLI writes the
repaired file back to flash. A node that rolls back to a channel missing a
preset therefore loses that slot permanently, and re-upgrading does not bring
it back.

The parity gate compared preset names only, deliberately allowing URL, CA and
credential drift. It now requires src/helpers/MQTTPresets.h to be byte-identical
between the channels, which also catches that drift — the two channels are meant
to dial the same brokers.

That is only workable if the file holds no channel-specific code, so
mqttPresetEnforcesTokenExp() moves to the new MQTTPresetPolicy.h. It was the
sole difference between the two channels' copies, and with it moved they match
exactly today. Policy keyed off the table belongs there from now on; the table
itself stays pure data.

The older name-only comparison stays available without --exact for ad-hoc use,
and the checker's self-test now covers both modes, including that --exact
rejects a config-only change the name check waves through.
This commit is contained in:
agessaman
2026-09-19 16:46:55 -07:00
parent 01c0131a59
commit 1bfbbf4bd8
5 changed files with 145 additions and 27 deletions
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "MQTTPresets.h"
#include <string.h>
// Policy that keys off a preset rather than describing one.
//
// MQTTPresets.h is data: it is the broker table, and it is kept byte-identical
// between the observer-firmware and observer-firmware-dev channels so a node
// that rolls back cannot meet a preset name its firmware does not know. An
// unknown name is not merely ignored — MQTTPrefsSerializer repairs it to "none"
// and the repaired /mqtt.json is written back, so the operator's slot is gone
// for good. Channel-specific behaviour therefore lives here instead, where the
// two channels are free to differ.
// True when the broker tears down a live session once its JWT passes exp, so the
// renewal must proactively bounce the connection to present a fresh token.
//
// Default true, because getting this wrong the safe way costs a re-handshake and
// getting it wrong the unsafe way costs an outage. waev is the exception: its
// operator confirmed (2026-08-11) that their servers do not disconnect on expiry,
// so a live session there needs only its credentials refreshed for the next
// reconnect. waev is also the only preset with a short token_lifetime, so it was
// the only one bouncing often — every ~47 min, and each bounce's re-handshake can
// cost ~10 KB of contiguous internal DRAM on a non-PSRAM board.
//
// Keyed by name rather than a struct field on purpose: adding a field would mean
// re-ordering a dozen positional initialisers in the table, where a mistake is
// silent.
static inline bool mqttPresetEnforcesTokenExp(const MQTTPresetDef* preset) {
if (!preset || !preset->name) return true; // custom/audience slots: assume enforced
return strcmp(preset->name, "waev") != 0;
}
-18
View File
@@ -47,24 +47,6 @@ struct MQTTPresetDef {
// Braces match topic placeholders ({device}/{iata}); never send this string to the broker.
static const char MQTT_USERPASS_USERNAME_PUBKEY[] = "{pubkey}";
// True when the broker tears down a live session once its JWT passes exp, so the
// renewal must proactively bounce the connection to present a fresh token.
//
// Default true, because getting this wrong the safe way costs a re-handshake and
// getting it wrong the unsafe way costs an outage. waev is the exception: its
// operator confirmed (2026-08-11) that their servers do not disconnect on expiry,
// so a live session there needs only its credentials refreshed for the next
// reconnect. waev is also the only preset with a short token_lifetime, so it was
// the only one bouncing often — every ~47 min, and each bounce's re-handshake can
// cost ~10 KB of contiguous internal DRAM on a non-PSRAM board.
//
// Keyed by name rather than a struct field on purpose: adding a field would mean
// re-ordering a dozen positional initialisers below, where a mistake is silent.
static inline bool mqttPresetEnforcesTokenExp(const MQTTPresetDef* preset) {
if (!preset || !preset->name) return true; // custom/audience slots: assume enforced
return strcmp(preset->name, "waev") != 0;
}
static inline bool mqttPresetUsesDevicePubkeyUsername(const MQTTPresetDef* preset) {
return preset && preset->auth_type == MQTT_AUTH_USERPASS &&
preset->userpass_username &&
+1
View File
@@ -10,6 +10,7 @@
#include "helpers/JWTHelper.h"
#include "helpers/MQTTPacketFilter.h"
#include "helpers/MQTTPresets.h"
#include "helpers/MQTTPresetPolicy.h"
#include "helpers/MQTTLifecycle.h"
#include "helpers/AlertFaultPolicy.h"
#include "helpers/MQTTEffectiveConfig.h"