mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-24 09:12:27 +00:00
Merge observer firmware with persistent opt-in advert signature verification
Add Waveshare LR1121 observer recipes. Adapt observer advert verification to existing transactional CLI, append-only binary preferences and Companion NVS, preserving default file lengths and WiFi credentials. Reject oversized unsigned advert tails consistently with Mesh::onRecvPacket. Keep current BLE recovery and GPS claim accounting.
This commit is contained in:
commit
b5e493996c
27 files changed
+641
-48
No files matched your search
@@ -606,6 +606,7 @@ These settings apply across all MQTT slots:
|
||||
- `get mqtt.packets` - Get packet message setting (on/off)
|
||||
- `get mqtt.raw` - Get raw message setting (on/off)
|
||||
- `get mqtt.rx` - Get RX packet uplinking setting (on/off)
|
||||
- `get mqtt.advert.verify` - Get advert signature filtering setting (on/off)
|
||||
- `get mqtt.tx` - Get TX packet uplinking setting (on/off/advert)
|
||||
- `get mqtt.interval` - Get status publish interval
|
||||
- `get mqtt.neighbors` - Get periodic neighbors publishing setting (on/off; neighbors-enabled builds)
|
||||
@@ -622,6 +623,7 @@ These settings apply across all MQTT slots:
|
||||
- `set mqtt.packets on|off` - Enable/disable packet messages
|
||||
- `set mqtt.raw on|off` - Enable/disable raw messages
|
||||
- `set mqtt.rx on|off` - Enable/disable RX (received) packet uplinking
|
||||
- `set mqtt.advert.verify on|off` - Skip received adverts whose signature fails (default off)
|
||||
- `set mqtt.tx on|off|advert` - Set TX packet uplinking mode:
|
||||
- `on` - Uplink all transmitted packets
|
||||
- `advert` - Uplink only this node's own advert packets (self-originated)
|
||||
|
||||
@@ -5213,6 +5213,29 @@ USB logging remains independent. On images with both outputs,
|
||||
|
||||
---
|
||||
|
||||
#### View or change MQTT advert signature filtering
|
||||
|
||||
**Usage:**
|
||||
|
||||
- `get mqtt.advert.verify`
|
||||
- `set mqtt.advert.verify <on|off>`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `on`: verify the Ed25519 signature of each received advert and skip publishing any that fail (the same check the mesh applies before relaying)
|
||||
- `off`: publish received adverts as heard
|
||||
|
||||
**Default:** `off`
|
||||
|
||||
> **Note:** Adverts are uploaded before the mesh validates them, so a frame corrupted over the air that still passes the LoRa CRC is published with a mangled name or location. Leaving this off keeps that corruption visible to collectors; turning it on hides it from them. Dropped adverts are counted as `badsig=` in `get mqtt.stats`. Takes effect immediately.
|
||||
|
||||
On Companion builds, enabling this setting opts into a marked MQTT preference
|
||||
record. Before rolling back to firmware without this setting, turn it off to
|
||||
restore the older record format and retain the saved MQTT setup. Ordinary
|
||||
default-off updates keep the existing record size.
|
||||
|
||||
---
|
||||
|
||||
#### View or change periodic neighbors publishing (MQTT observer, neighbors feature)
|
||||
**Usage:**
|
||||
- `get mqtt.neighbors`
|
||||
|
||||
@@ -5189,6 +5189,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
else if (strcmp(key, "mqtt.packets") == 0) value = _mqtt_prefs.mqtt_packets_enabled ? "on" : "off";
|
||||
else if (strcmp(key, "mqtt.raw") == 0) value = _mqtt_prefs.mqtt_raw_enabled ? "on" : "off";
|
||||
else if (strcmp(key, "mqtt.rx") == 0) value = _mqtt_prefs.mqtt_rx_enabled ? "on" : "off";
|
||||
else if (strcmp(key, "mqtt.advert.verify") == 0) value = _mqtt_prefs.mqtt_advert_verify ? "on" : "off";
|
||||
else if (strcmp(key, "mqtt.tx") == 0) value = _mqtt_prefs.mqtt_tx_enabled == 2 ? "advert" : _mqtt_prefs.mqtt_tx_enabled ? "on" : "off";
|
||||
else if (strcmp(key, "mqtt.ntp") == 0) value = _mqtt_prefs.mqtt_ntp_server;
|
||||
else if (strcmp(key, "mqtt.owner") == 0) value = _mqtt_prefs.mqtt_owner_public_key;
|
||||
@@ -5459,6 +5460,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
}
|
||||
} else if (strcmp(key, "mqtt.status") == 0 || strcmp(key, "mqtt.packets") == 0
|
||||
|| strcmp(key, "mqtt.raw") == 0 || strcmp(key, "mqtt.rx") == 0
|
||||
|| strcmp(key, "mqtt.advert.verify") == 0
|
||||
|| strcmp(key, "snmp") == 0) {
|
||||
bool enabled;
|
||||
if (!wcParseBool(value, enabled)) {
|
||||
@@ -5469,6 +5471,7 @@ void MyMesh::execCommand(char* cmd, char* reply) {
|
||||
else if (strcmp(key, "mqtt.packets") == 0) _mqtt_prefs.mqtt_packets_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.raw") == 0) _mqtt_prefs.mqtt_raw_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.rx") == 0) _mqtt_prefs.mqtt_rx_enabled = enabled;
|
||||
else if (strcmp(key, "mqtt.advert.verify") == 0) _mqtt_prefs.mqtt_advert_verify = enabled;
|
||||
else _mqtt_prefs.snmp_enabled = enabled;
|
||||
changed = true;
|
||||
} else if (strcmp(key, "mqtt.tx") == 0) {
|
||||
|
||||
@@ -3436,8 +3436,8 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
|
||||
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
|
||||
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
|
||||
&& discover_limiter.allow(rtc_clock.getCurrentTime())
|
||||
&& !isHiddenNode() // this node wishes to NOT be discoverable
|
||||
&& discover_limiter.allow(rtc_clock.getCurrentTime())
|
||||
) {
|
||||
int i = 1;
|
||||
uint8_t filter = packet->payload[i++];
|
||||
|
||||
@@ -121,7 +121,7 @@ def default_config(setup_mode):
|
||||
"mqtt": {
|
||||
"enabled": False,
|
||||
"origin": "" if setup_mode else "MockNode", "iata": "" if setup_mode else "DEN",
|
||||
"status": True, "packets": True, "raw": False, "tx": "advert", "rx": True,
|
||||
"status": True, "packets": True, "raw": False, "tx": "advert", "rx": True, "advert_verify": False,
|
||||
"interval": 5, "timezone": "MST7MDT,M3.2.0,M11.1.0", "timezone_offset": -7,
|
||||
"ntp": "pool.ntp.org", "owner": "", "email": "", "snmp": False,
|
||||
"snmp_community": "public",
|
||||
@@ -228,7 +228,8 @@ BOOL_KEYS = {"mqtt.enabled": ("mqtt", "enabled"), "cad": ("radio", "cad"), "radi
|
||||
"powersaving": ("radio", "powersaving"),
|
||||
"repeat": ("radio", "repeat"), "mqtt.status": ("mqtt", "status"),
|
||||
"mqtt.packets": ("mqtt", "packets"), "mqtt.raw": ("mqtt", "raw"),
|
||||
"mqtt.rx": ("mqtt", "rx"), "snmp": ("mqtt", "snmp"),
|
||||
"mqtt.rx": ("mqtt", "rx"), "mqtt.advert.verify": ("mqtt", "advert_verify"),
|
||||
"snmp": ("mqtt", "snmp"),
|
||||
"mqtt.neighbors": ("mqtt", "neighbors")}
|
||||
INT_KEYS = {"tx": ("radio", "tx"), "flood.max": ("radio", "flood_max"),
|
||||
"flood.max.advert": ("radio", "flood_max_advert"),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <MeshCore.h>
|
||||
|
||||
// Signed-message layout of an ADVERT payload, mirroring Mesh::onRecvPacket:
|
||||
// pub_key(32) + timestamp(4) + signature(64) + app_data. Oversized app_data is
|
||||
// rejected, like Mesh::onRecvPacket: truncation would accept an unsigned tail.
|
||||
namespace AdvertSignature {
|
||||
|
||||
static const int kHeaderLen = PUB_KEY_SIZE + 4 + SIGNATURE_SIZE;
|
||||
static const int kMaxMessageLen = PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE;
|
||||
|
||||
// Builds the signed message into `out` (kMaxMessageLen bytes) and points `sig`
|
||||
// at the signature. Returns the message length, or -1 for invalid arguments or
|
||||
// a payload outside the complete signed advert layout. Clears sig on failure.
|
||||
static inline int buildMessage(const uint8_t* payload, int payload_len,
|
||||
uint8_t* out, const uint8_t** sig) {
|
||||
if (sig == nullptr) return -1;
|
||||
*sig = nullptr;
|
||||
if (payload == nullptr || out == nullptr || payload_len < kHeaderLen ||
|
||||
payload_len > kHeaderLen + MAX_ADVERT_DATA_SIZE) return -1;
|
||||
int app_data_len = payload_len - kHeaderLen;
|
||||
memcpy(out, payload, PUB_KEY_SIZE + 4);
|
||||
memcpy(out + PUB_KEY_SIZE + 4, payload + kHeaderLen, app_data_len);
|
||||
*sig = payload + PUB_KEY_SIZE + 4;
|
||||
return PUB_KEY_SIZE + 4 + app_data_len;
|
||||
}
|
||||
|
||||
} // namespace AdvertSignature
|
||||
@@ -2776,11 +2776,11 @@ void CommonCLI::loadMQTTPrefs(
|
||||
if (MQTTPrefsCodec::repairDisplayPrefs(&_mqtt_prefs)) {
|
||||
// Old payloads retain valid defaults because the object was defaulted before
|
||||
// their shorter prefix was copied. This branch repairs only a full/newer
|
||||
// payload carrying invalid display values.
|
||||
// payload carrying invalid display or advert-verification values.
|
||||
if (!_mqtt_prefs_hold) {
|
||||
mqtt_rewrite_pending = true;
|
||||
}
|
||||
MESH_DEBUG_PRINTLN("MQTT: invalid display preferences reset to defaults");
|
||||
MESH_DEBUG_PRINTLN("MQTT: invalid display/advert preferences reset to defaults");
|
||||
}
|
||||
_legacy_tail.valid = false;
|
||||
|
||||
|
||||
@@ -330,6 +330,19 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
|
||||
_mqtt_prefs.mqtt_rx_enabled = memcmp(&config[8], "on", 2) == 0 ? 1 : 0;
|
||||
saveObserverPrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "mqtt.advert.verify ", 19) == 0) {
|
||||
if (strcmp(config + 19, "on") != 0 && strcmp(config + 19, "off") != 0) {
|
||||
strcpy(reply, "Error: must be on or off");
|
||||
return true;
|
||||
}
|
||||
const uint8_t previous = _mqtt_prefs.mqtt_advert_verify;
|
||||
_mqtt_prefs.mqtt_advert_verify = strcmp(config + 19, "on") == 0;
|
||||
if (!saveObserverPrefs()) {
|
||||
_mqtt_prefs.mqtt_advert_verify = previous;
|
||||
strcpy(reply, "Error: settings could not be saved");
|
||||
return true;
|
||||
}
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(config, "mqtt.interval ", 14) == 0) {
|
||||
uint32_t minutes = _atoi(&config[14]);
|
||||
if (minutes >= 1 && minutes <= 60) {
|
||||
@@ -956,6 +969,8 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
||||
sprintf(reply, "> %s", tx_str);
|
||||
} else if (memcmp(config, "mqtt.rx", 7) == 0) {
|
||||
sprintf(reply, "> %s", _mqtt_prefs.mqtt_rx_enabled ? "on" : "off");
|
||||
} else if (strcmp(config, "mqtt.advert.verify") == 0) {
|
||||
sprintf(reply, "> %s", _mqtt_prefs.mqtt_advert_verify ? "on" : "off");
|
||||
} else if (memcmp(config, "mqtt.interval", 13) == 0) {
|
||||
uint32_t minutes = (_mqtt_prefs.mqtt_status_interval + 29999) / 60000;
|
||||
sprintf(reply, "> %u minutes (%lu ms)", minutes, (unsigned long)_mqtt_prefs.mqtt_status_interval);
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
// The ESP32 Companion setup portal stores a raw MQTTPrefs blob in NVS rather
|
||||
// than the observer's versioned /mqtt_prefs file. Display preferences are not
|
||||
// used by Companion firmware, so keep writing the last pre-display boundary:
|
||||
// older firmware can then read a record saved by a newer image after rollback.
|
||||
// used by Companion firmware. With advert verification off, keep writing the
|
||||
// last pre-display boundary so rollback preserves a normally configured node.
|
||||
// Enabling verification opts into a separately marked blob: an old recovery
|
||||
// record's former padding must never silently enable a new packet filter.
|
||||
namespace CompanionMqttPrefsNvs {
|
||||
|
||||
static constexpr size_t kWriteSize = MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE;
|
||||
@@ -20,10 +22,24 @@ static constexpr size_t kWriteSize = MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE;
|
||||
static constexpr size_t kRecoverySize = 2880;
|
||||
static_assert(sizeof(MQTTPrefs) >= kRecoverySize,
|
||||
"MQTTPrefs no longer contains the recoverable NVS prefix");
|
||||
static constexpr size_t kVerifiedSize = 2881;
|
||||
static constexpr uint8_t kVerifiedMarker = 0xa7;
|
||||
struct Record {
|
||||
MQTTPrefs prefs;
|
||||
uint8_t marker;
|
||||
};
|
||||
static_assert(offsetof(Record, marker) == kRecoverySize,
|
||||
"the marked NVS blob must append exactly one format byte");
|
||||
static_assert(sizeof(Record) >= kVerifiedSize,
|
||||
"the marked NVS record must contain its whole serialized blob");
|
||||
|
||||
inline size_t writeSizeFor(const MQTTPrefs& prefs) {
|
||||
return prefs.mqtt_advert_verify != 0 ? kVerifiedSize : kWriteSize;
|
||||
}
|
||||
|
||||
inline bool accepts(uint16_t version, size_t length) {
|
||||
return version == MQTT_PREFS_VERSION
|
||||
&& (length == kWriteSize || length == kRecoverySize);
|
||||
&& (length == kWriteSize || length == kRecoverySize || length == kVerifiedSize);
|
||||
}
|
||||
|
||||
} // namespace CompanionMqttPrefsNvs
|
||||
|
||||
@@ -594,12 +594,20 @@ bool CompanionMqttSetupPortal::loadStoredConfig(MQTTPrefs& prefs) {
|
||||
// default its append-only display tail so adding observer UI preferences
|
||||
// cannot discard an existing Companion MQTT setup after an update.
|
||||
const bool shape_ok = CompanionMqttPrefsNvs::accepts(version, length);
|
||||
MQTTPrefs loaded;
|
||||
CompanionMqttPrefsNvs::Record record = {};
|
||||
MQTTPrefs& loaded = record.prefs;
|
||||
applyMQTTDefaults(&loaded);
|
||||
const size_t read = shape_ok ? nvs.getBytes(NVS_PREFS_KEY, &loaded, length) : 0;
|
||||
const size_t read = shape_ok ? nvs.getBytes(NVS_PREFS_KEY, &record, length) : 0;
|
||||
nvs.end();
|
||||
if (!shape_ok || read != length) return false;
|
||||
|
||||
// The old 2880-byte recovery blob ended with unspecified padding, including
|
||||
// potentially 1. Only an explicitly marked new blob can opt into filtering.
|
||||
// Invalid optional metadata must not discard otherwise valid credentials.
|
||||
if (length != CompanionMqttPrefsNvs::kVerifiedSize ||
|
||||
record.marker != CompanionMqttPrefsNvs::kVerifiedMarker ||
|
||||
loaded.mqtt_advert_verify > 1) loaded.mqtt_advert_verify = 0;
|
||||
|
||||
if (!storedStringsAreValid(loaded)
|
||||
|| loaded.mqtt_status_enabled > 1
|
||||
|| loaded.mqtt_packets_enabled > 1
|
||||
@@ -663,15 +671,20 @@ bool CompanionMqttSetupPortal::saveEnabled(bool enabled) {
|
||||
bool CompanionMqttSetupPortal::saveStoredConfig(const MQTTPrefs& prefs) {
|
||||
Preferences nvs;
|
||||
if (!nvs.begin(NVS_NAMESPACE, false)) return false;
|
||||
// Companion does not consume the appended observer display tail. Keeping
|
||||
// this record at the pre-display boundary makes a firmware rollback retain
|
||||
// the MQTT setup. loadStoredConfig() still accepts the briefly written 2880
|
||||
// byte runtime shape so development images remain recoverable.
|
||||
const size_t written = nvs.putBytes(
|
||||
NVS_PREFS_KEY, &prefs, CompanionMqttPrefsNvs::kWriteSize);
|
||||
// Default configuration keeps the rollback-readable pre-display boundary.
|
||||
// Advert verification and its format marker are one atomic NVS blob write,
|
||||
// not separate preference keys that can disagree after power loss.
|
||||
const size_t payload_size = CompanionMqttPrefsNvs::writeSizeFor(prefs);
|
||||
size_t written;
|
||||
if (prefs.mqtt_advert_verify != 0) {
|
||||
const CompanionMqttPrefsNvs::Record record = {prefs, CompanionMqttPrefsNvs::kVerifiedMarker};
|
||||
written = nvs.putBytes(NVS_PREFS_KEY, &record, payload_size);
|
||||
} else {
|
||||
written = nvs.putBytes(NVS_PREFS_KEY, &prefs, payload_size);
|
||||
}
|
||||
const size_t version_written = nvs.putUShort(NVS_VERSION_KEY, MQTT_PREFS_VERSION);
|
||||
nvs.end();
|
||||
return written == CompanionMqttPrefsNvs::kWriteSize
|
||||
return written == payload_size
|
||||
&& version_written == sizeof(uint16_t);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ static inline void applyMQTTDefaults(MQTTPrefs* prefs) {
|
||||
prefs->mqtt_raw_enabled = 0;
|
||||
prefs->mqtt_tx_enabled = 2;
|
||||
prefs->mqtt_rx_enabled = 1;
|
||||
prefs->mqtt_advert_verify = 0;
|
||||
prefs->mqtt_status_interval = 300000;
|
||||
prefs->wifi_power_save = mesh::wifi::kDefaultPowerSave;
|
||||
|
||||
|
||||
@@ -40,21 +40,24 @@ static const size_t kV1PreObserverPayloadSize = MQTT_PREFS_V1_PRE_OBSERVER_PAYLO
|
||||
static const size_t kV1PreNeighborsPayloadSize = MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE;
|
||||
static const size_t kV1PreFilterPayloadSize = MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE;
|
||||
static const size_t kV1PreDisplayPayloadSize = MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE;
|
||||
static const size_t kV1PreAdvertPayloadSize = MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE;
|
||||
static const size_t kV1BaselinePayloadSize = MQTT_PREFS_V1_FULL_PAYLOAD_SIZE;
|
||||
static const size_t kEncodedSize = sizeof(MQTTPrefsHeader) + kV1BaselinePayloadSize;
|
||||
|
||||
// Shortest payload length that still round-trips this configuration.
|
||||
//
|
||||
// The packet-filter and display tails are optional. Their defaults are exactly
|
||||
// The packet-filter, display and advert-verification tails are optional. Their
|
||||
// defaults are exactly
|
||||
// what a decoder supplies when the corresponding tail is missing, so merely
|
||||
// upgrading does not lengthen the file. That matters because /mqtt_prefs also
|
||||
// carries WiFi credentials: old firmware that cannot read a longer payload can
|
||||
// otherwise lose its network configuration after a downgrade. Changing a
|
||||
// filter or display control deliberately opts the node into the required tail.
|
||||
// filter, display or verification control deliberately opts into its tail.
|
||||
inline size_t payloadLenFor(const MQTTPrefs& prefs) {
|
||||
if (prefs.mqtt_advert_verify != 0) return kV1BaselinePayloadSize;
|
||||
if (prefs.display_timeout_secs != DISPLAY_TIMEOUT_DEFAULT_SECS ||
|
||||
prefs.display_flip != 0) {
|
||||
return kV1BaselinePayloadSize;
|
||||
return kV1PreAdvertPayloadSize;
|
||||
}
|
||||
return MQTTPacketFilter::allMasksDefault(prefs.mqtt_slot_packet_filter,
|
||||
MQTT_PREFS_SLOT_COUNT)
|
||||
@@ -62,7 +65,8 @@ inline size_t payloadLenFor(const MQTTPrefs& prefs) {
|
||||
: kV1PreDisplayPayloadSize;
|
||||
}
|
||||
|
||||
// Clamp the display tail after loading. Returns true when a persisted value was
|
||||
// Clamp the optional display/verification tails after loading. Retain the helper
|
||||
// name for callers predating advert verification. Returns true when a value was
|
||||
// repaired and should be rewritten (unless the source file is being held).
|
||||
inline bool repairDisplayPrefs(MQTTPrefs* prefs) {
|
||||
if (prefs == nullptr) return false;
|
||||
@@ -75,6 +79,10 @@ inline bool repairDisplayPrefs(MQTTPrefs* prefs) {
|
||||
prefs->display_flip = 0;
|
||||
repaired = true;
|
||||
}
|
||||
if (prefs->mqtt_advert_verify > 1) {
|
||||
prefs->mqtt_advert_verify = 0;
|
||||
repaired = true;
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
|
||||
@@ -156,6 +164,10 @@ inline DecodePlan classify(const uint8_t* prefix, size_t prefix_read, size_t fil
|
||||
if (header.payload_len == kV1BaselinePayloadSize) {
|
||||
return {Source::Current, false, false, true, kV1BaselinePayloadSize};
|
||||
}
|
||||
if (header.payload_len == kV1PreAdvertPayloadSize) {
|
||||
// Former complete layout. The default keeps advert verification off.
|
||||
return {Source::Current, false, false, true, kV1PreAdvertPayloadSize};
|
||||
}
|
||||
if (header.payload_len == kV1PreDisplayPayloadSize) {
|
||||
// Written before runtime display controls. Defaults supply timeout and
|
||||
// flip while preserving the packet-filter tail.
|
||||
|
||||
@@ -203,10 +203,10 @@ class MQTTPrefsSerializer : public ConfigSerializer {
|
||||
|
||||
class MqttPrefs : public ConfigSerializer {
|
||||
MQTTPrefs* _prefs;
|
||||
int32_t _packets_enabled, _raw_enabled, _tx_enabled, _rx_enabled;
|
||||
int32_t _packets_enabled, _raw_enabled, _tx_enabled, _rx_enabled, _advert_verify;
|
||||
char _default_iata[sizeof(MQTTPrefs::mqtt_iata)];
|
||||
bool _seen_origin = false, _seen_iata = false, _seen_packets = false;
|
||||
bool _seen_raw = false, _seen_tx = false, _seen_rx = false;
|
||||
bool _seen_raw = false, _seen_tx = false, _seen_rx = false, _seen_advert_verify = false;
|
||||
bool _seen_status = false, _seen_neighbors = false, _seen_owner = false;
|
||||
bool _seen_slot1 = false, _seen_slot2 = false, _seen_slot3 = false;
|
||||
bool _seen_slot4 = false, _seen_slot5 = false, _seen_slot6 = false;
|
||||
@@ -224,6 +224,7 @@ class MQTTPrefsSerializer : public ConfigSerializer {
|
||||
defStrict("raw_enabled", _raw_enabled, _seen_raw);
|
||||
defStrict("tx_enabled", _tx_enabled, _seen_tx);
|
||||
defStrict("rx_enabled", _rx_enabled, _seen_rx);
|
||||
defStrict("advert_verify", _advert_verify, _seen_advert_verify);
|
||||
defStrict("status", _status, _seen_status);
|
||||
defStrict("neighbors", _neighbors, _seen_neighbors);
|
||||
defStrict("owner", _owner, _seen_owner);
|
||||
@@ -239,7 +240,8 @@ class MQTTPrefsSerializer : public ConfigSerializer {
|
||||
MQTTPrefsPresetValidator preset_valid)
|
||||
: _prefs(prefs), _packets_enabled(prefs->mqtt_packets_enabled),
|
||||
_raw_enabled(prefs->mqtt_raw_enabled), _tx_enabled(prefs->mqtt_tx_enabled),
|
||||
_rx_enabled(prefs->mqtt_rx_enabled), _status(prefs), _neighbors(prefs),
|
||||
_rx_enabled(prefs->mqtt_rx_enabled), _advert_verify(prefs->mqtt_advert_verify),
|
||||
_status(prefs), _neighbors(prefs),
|
||||
_owner(prefs), _slot1(prefs, 0, preset_valid),
|
||||
_slot2(prefs, 1, preset_valid), _slot3(prefs, 2, preset_valid),
|
||||
_slot4(prefs, 3, preset_valid), _slot5(prefs, 4, preset_valid),
|
||||
@@ -257,10 +259,12 @@ class MQTTPrefsSerializer : public ConfigSerializer {
|
||||
if (_raw_enabled < 0 || _raw_enabled > 1) { _raw_enabled = 0; *repaired = true; }
|
||||
if (_tx_enabled < 0 || _tx_enabled > 2) { _tx_enabled = 2; *repaired = true; }
|
||||
if (_rx_enabled < 0 || _rx_enabled > 1) { _rx_enabled = 1; *repaired = true; }
|
||||
if (_advert_verify < 0 || _advert_verify > 1) { _advert_verify = 0; *repaired = true; }
|
||||
_prefs->mqtt_packets_enabled = static_cast<uint8_t>(_packets_enabled);
|
||||
_prefs->mqtt_raw_enabled = static_cast<uint8_t>(_raw_enabled);
|
||||
_prefs->mqtt_tx_enabled = static_cast<uint8_t>(_tx_enabled);
|
||||
_prefs->mqtt_rx_enabled = static_cast<uint8_t>(_rx_enabled);
|
||||
_prefs->mqtt_advert_verify = static_cast<uint8_t>(_advert_verify);
|
||||
if (_prefs->mqtt_iata[0] != '\0') {
|
||||
if (!mqttIataValid(_prefs->mqtt_iata)) {
|
||||
memcpy(_prefs->mqtt_iata, _default_iata, sizeof(_default_iata));
|
||||
|
||||
@@ -125,10 +125,13 @@ struct MQTTPrefs {
|
||||
|
||||
// Runtime display controls. These are an append-only v1 tail: older payloads
|
||||
// stop before them and retain the defaults below. The serialized payload ends
|
||||
// after display_flip at byte 2879; the compiler's final alignment byte is not
|
||||
// part of the on-flash format.
|
||||
// after display_flip at byte 2879 on pre-verification firmware.
|
||||
uint16_t display_timeout_secs;
|
||||
uint8_t display_flip;
|
||||
// Drop received adverts whose Ed25519 signature fails before publishing.
|
||||
// Append-only v1 tail at byte 2879 (formerly final struct padding). Defaults
|
||||
// off; only enabling verification requires the 2880-byte serialized payload.
|
||||
uint8_t mqtt_advert_verify;
|
||||
};
|
||||
|
||||
// Neighbor discovery is scheduled with the wrap-safe millis() helpers, whose
|
||||
@@ -144,7 +147,7 @@ static const uint32_t MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS = MQTT_NEIGHBORS_DEFAUL
|
||||
static const uint16_t DISPLAY_TIMEOUT_DEFAULT_SECS = 60;
|
||||
static const uint16_t DISPLAY_TIMEOUT_MAX_SECS = 3600;
|
||||
|
||||
// Version-1 has five payload layouts this firmware can decode. Never infer a
|
||||
// Version-1 has six payload layouts this firmware can decode. Never infer a
|
||||
// compatible payload from an arbitrary SHORTER size: raw prefs have no
|
||||
// checksum, so a short length has to match a boundary that was really shipped.
|
||||
//
|
||||
@@ -157,18 +160,20 @@ static const uint16_t DISPLAY_TIMEOUT_MAX_SECS = 3600;
|
||||
// - PRE_NEIGHBORS (2860): full observer tail, no neighbors fields yet.
|
||||
// - PRE_FILTER (2864): neighbors tail, no per-slot packet filters.
|
||||
// - PRE_DISPLAY (2876): six filter masks, no runtime display controls.
|
||||
// - FULL (2879): timeout plus flip, excluding final struct padding.
|
||||
// - PRE_ADVERT (2879): timeout plus flip, excluding final struct padding.
|
||||
// - FULL (2880): advert verification consumes that former padding.
|
||||
//
|
||||
// FULL is the maximum written, not the default: MQTTPrefsCodec::payloadLenFor()
|
||||
// keeps emitting PRE_FILTER while every slot and display control has its
|
||||
// default. A non-default filter writes PRE_DISPLAY; a non-default display
|
||||
// control writes FULL. See the rollback note there -- /mqtt_prefs also carries
|
||||
// the WiFi credentials.
|
||||
// control writes PRE_ADVERT; enabling advert verification writes FULL. See the
|
||||
// rollback note there -- /mqtt_prefs also carries the WiFi credentials.
|
||||
static const size_t MQTT_PREFS_V1_PRE_OBSERVER_PAYLOAD_SIZE = 2736;
|
||||
static const size_t MQTT_PREFS_V1_PRE_NEIGHBORS_PAYLOAD_SIZE = 2860;
|
||||
static const size_t MQTT_PREFS_V1_PRE_FILTER_PAYLOAD_SIZE = 2864;
|
||||
static const size_t MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE = 2876;
|
||||
static const size_t MQTT_PREFS_V1_FULL_PAYLOAD_SIZE = 2879;
|
||||
static const size_t MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE = 2879;
|
||||
static const size_t MQTT_PREFS_V1_FULL_PAYLOAD_SIZE = 2880;
|
||||
|
||||
// /mqtt_prefs starts with a self-describing 8-byte header. Headerless files
|
||||
// are deployed legacy layouts and continue to be distinguished by size.
|
||||
@@ -303,8 +308,13 @@ static_assert(offsetof(MQTTPrefs, mqtt_slot_packet_filter) == MQTT_PREFS_V1_PRE_
|
||||
static_assert(offsetof(MQTTPrefs, display_timeout_secs) == MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE,
|
||||
"display timeout must begin at the pre-display payload boundary");
|
||||
static_assert(offsetof(MQTTPrefs, display_flip) + sizeof(MQTTPrefs::display_flip) ==
|
||||
MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
|
||||
MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE,
|
||||
"serialized display tail must end immediately after display_flip");
|
||||
static_assert(offsetof(MQTTPrefs, mqtt_advert_verify) == MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE,
|
||||
"advert verification must consume only the former final padding byte");
|
||||
static_assert(offsetof(MQTTPrefs, mqtt_advert_verify) + sizeof(MQTTPrefs::mqtt_advert_verify) ==
|
||||
MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
|
||||
"serialized advert verification tail must end immediately after its flag");
|
||||
static_assert(sizeof(OldMQTTPrefs) == 472, "frozen pre-slot /mqtt_prefs layout changed");
|
||||
static_assert(sizeof(PreWifiPowerOldMQTTPrefs) == 472, "frozen pre-WiFi-power /mqtt_prefs layout changed");
|
||||
static_assert(offsetof(OldMQTTPrefs, wifi_power_save) == 144,
|
||||
|
||||
@@ -29,7 +29,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
|
||||
// MQTTPrefs (WiFi / MQTT / misc observer)
|
||||
"wifi.ssid", "wifi.pwd", "wifi.powersave", "espnow.channel",
|
||||
"mqtt.enabled", "mqtt.origin", "mqtt.iata", "mqtt.status", "mqtt.packets", "mqtt.raw",
|
||||
"mqtt.tx", "mqtt.rx", "mqtt.interval", "mqtt.neighbors", "mqtt.neighbors.interval",
|
||||
"mqtt.tx", "mqtt.rx", "mqtt.advert.verify", "mqtt.interval", "mqtt.neighbors", "mqtt.neighbors.interval",
|
||||
"mqtt.ntp", "mqtt.owner", "mqtt.email",
|
||||
"timezone", "timezone.offset", "snmp", "snmp.community",
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../MQTTReplyFormat.h"
|
||||
#include "../MQTTRuntimeBufferLifecycle.h"
|
||||
#include "../MQTTTopicRouter.h"
|
||||
#include "../AdvertSignature.h"
|
||||
#include "../TxtDataHelpers.h"
|
||||
#include "../UsbLogging.h"
|
||||
#include "../WiFiPowerSave.h"
|
||||
@@ -519,6 +520,14 @@ void MQTTBridge::formatMqttStatsReply(char* buf, size_t bufsize) {
|
||||
if (b->_filtered_packets > 0) {
|
||||
replyAppendf(buf, bufsize, &pos, " filt=%lu", b->_filtered_packets);
|
||||
}
|
||||
// badsig=<n>: RX adverts dropped by mqtt.advert.verify. Omitted while zero.
|
||||
if (b->_bad_sig_adverts > 0) {
|
||||
replyAppendf(buf, bufsize, &pos, " badsig=%lu", b->_bad_sig_adverts);
|
||||
}
|
||||
// down=<n>: ready slots that are not connected. Without this the reply cannot
|
||||
// show an outage at all - sN= counts publishes, and a slot that never connects
|
||||
// never publishes, so its ok freezes and its err stays 0 for the whole outage.
|
||||
// Omitted while zero, like filt=, so a healthy node's reply keeps its length.
|
||||
int down = 0;
|
||||
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
|
||||
if (MQTTConnectionHealth::isOutageSlot(
|
||||
@@ -3931,6 +3940,19 @@ void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Opt-in: drop adverts that fail verification (RF corruption that passed the
|
||||
// LoRa CRC). The observer callback runs before Mesh::onRecvPacket validates
|
||||
// received adverts; its opt-in filter must enforce the same signed layout.
|
||||
if (_obs->mqtt_advert_verify && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = nullptr;
|
||||
int msg_len = AdvertSignature::buildMessage(packet->payload, packet->payload_len, message, &sig);
|
||||
if (msg_len < 0 || !mesh::Identity(packet->payload).verify(sig, message, msg_len)) {
|
||||
_bad_sig_adverts++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue packet for transmission
|
||||
queuePacket(packet, false);
|
||||
}
|
||||
|
||||
@@ -423,6 +423,7 @@ private:
|
||||
// on Core 1 (radio callbacks), read on Core 0 for `mqtt.stats`; a torn read of
|
||||
// a diagnostic counter is harmless, so no atomic is warranted.
|
||||
unsigned long _filtered_packets = 0;
|
||||
unsigned long _bad_sig_adverts = 0; // RX adverts dropped by mqtt.advert.verify
|
||||
|
||||
// Status publish retry tracking
|
||||
unsigned long _last_status_retry; // Track last retry attempt (separate from successful publish)
|
||||
|
||||
@@ -2188,6 +2188,7 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
|
||||
mqtt["tx"] = obs->mqtt_tx_enabled == 2 ? "advert"
|
||||
: obs->mqtt_tx_enabled == 1 ? "on" : "off";
|
||||
mqtt["rx"] = (bool)obs->mqtt_rx_enabled;
|
||||
mqtt["advert_verify"] = (bool)obs->mqtt_advert_verify;
|
||||
mqtt["interval"] = obs->mqtt_status_interval / 60000;
|
||||
mqtt["neighbors"] = (bool)obs->mqtt_neighbors_enabled;
|
||||
mqtt["neighbors_interval"] = obs->mqtt_neighbors_interval / 3600000UL;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "helpers/AdvertSignature.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<uint8_t> fromHex(const char* hex) {
|
||||
std::vector<uint8_t> out;
|
||||
for (const char* p = hex; p[0] && p[1]; p += 2) {
|
||||
char byte[3] = {p[0], p[1], 0};
|
||||
out.push_back(static_cast<uint8_t>(strtoul(byte, nullptr, 16)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ADVERT payload for "LCC Andice Observer" as heard by a working observer.
|
||||
const char* kAdvertPayload =
|
||||
"1EEB3527ED66C732185A2F29264A7450DBA40F1686CAE580F72BC80E65B2D835"
|
||||
"F18EC66A"
|
||||
"1A4FBA70C4BF0A2BCA115F131A6A0F2D54A6E3F8AFCD893F3414DB9DE31EC87E"
|
||||
"AD2D21F54C4C9ED1CD40147F10515BFE47CB83D27B8C20B29BF5B2A61BDA430B"
|
||||
"92FCEFD50106D52AFA4C434320416E64696365204F62736572766572";
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(AdvertSignature, BuildsPubKeyTimestampAndAppData) {
|
||||
std::vector<uint8_t> payload = fromHex(kAdvertPayload);
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = nullptr;
|
||||
|
||||
int len = AdvertSignature::buildMessage(payload.data(), static_cast<int>(payload.size()),
|
||||
message, &sig);
|
||||
|
||||
int app_data_len = static_cast<int>(payload.size()) - AdvertSignature::kHeaderLen;
|
||||
ASSERT_EQ(PUB_KEY_SIZE + 4 + app_data_len, len);
|
||||
EXPECT_EQ(payload.data() + PUB_KEY_SIZE + 4, sig);
|
||||
EXPECT_EQ(0, memcmp(message, payload.data(), PUB_KEY_SIZE + 4));
|
||||
EXPECT_EQ(0, memcmp(message + PUB_KEY_SIZE + 4, payload.data() + AdvertSignature::kHeaderLen,
|
||||
app_data_len));
|
||||
}
|
||||
|
||||
TEST(AdvertSignature, RejectsPayloadShorterThanHeader) {
|
||||
uint8_t payload[AdvertSignature::kHeaderLen - 1] = {};
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = nullptr;
|
||||
EXPECT_EQ(-1, AdvertSignature::buildMessage(payload, sizeof(payload), message, &sig));
|
||||
EXPECT_EQ(-1, AdvertSignature::buildMessage(nullptr, 0, message, &sig));
|
||||
}
|
||||
|
||||
TEST(AdvertSignature, HeaderOnlyPayloadSignsPubKeyAndTimestamp) {
|
||||
uint8_t payload[AdvertSignature::kHeaderLen];
|
||||
for (int i = 0; i < AdvertSignature::kHeaderLen; i++) payload[i] = static_cast<uint8_t>(i);
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = nullptr;
|
||||
EXPECT_EQ(PUB_KEY_SIZE + 4,
|
||||
AdvertSignature::buildMessage(payload, sizeof(payload), message, &sig));
|
||||
}
|
||||
|
||||
// Oversized unsigned tails must not bypass the observer's opt-in filter.
|
||||
TEST(AdvertSignature, RejectsOversizedAppDataLikeHardenedMesh) {
|
||||
uint8_t payload[AdvertSignature::kHeaderLen + MAX_ADVERT_DATA_SIZE + 10];
|
||||
for (size_t i = 0; i < sizeof(payload); i++) payload[i] = static_cast<uint8_t>(i);
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = nullptr;
|
||||
|
||||
int len = AdvertSignature::buildMessage(payload, sizeof(payload), message, &sig);
|
||||
|
||||
ASSERT_EQ(-1, len);
|
||||
EXPECT_EQ(nullptr, sig);
|
||||
}
|
||||
|
||||
TEST(AdvertSignature, ChecksOutputArgumentsAndClearsStaleSignature) {
|
||||
uint8_t payload[AdvertSignature::kHeaderLen + MAX_ADVERT_DATA_SIZE] = {};
|
||||
uint8_t message[AdvertSignature::kMaxMessageLen];
|
||||
const uint8_t* sig = payload;
|
||||
EXPECT_EQ(-1, AdvertSignature::buildMessage(payload, -1, message, &sig));
|
||||
EXPECT_EQ(nullptr, sig);
|
||||
EXPECT_EQ(-1, AdvertSignature::buildMessage(payload, sizeof(payload), nullptr, &sig));
|
||||
EXPECT_EQ(nullptr, sig);
|
||||
EXPECT_EQ(-1, AdvertSignature::buildMessage(payload, sizeof(payload), message, nullptr));
|
||||
EXPECT_EQ(AdvertSignature::kMaxMessageLen,
|
||||
AdvertSignature::buildMessage(payload, sizeof(payload), message, &sig));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -22,6 +22,7 @@ def function(source, signature):
|
||||
|
||||
PREAMBLE = r"""
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <helpers/CompanionMqttPrefsNvs.h>
|
||||
@@ -47,20 +48,33 @@ void applyMQTTDefaults(MQTTPrefs* p) {
|
||||
memset(p, 0, sizeof(*p));
|
||||
for (auto& preset : p->mqtt_slot_preset) strcpy(preset, "none");
|
||||
}
|
||||
MQTTPrefs saved;
|
||||
CompanionMqttPrefsNvs::Record persisted;
|
||||
MQTTPrefs& saved = persisted.prefs;
|
||||
size_t saved_length = CompanionMqttPrefsNvs::kWriteSize;
|
||||
struct Preferences {
|
||||
bool begin(const char*, bool) { return true; }
|
||||
bool isKey(const char*) { return true; }
|
||||
uint16_t getUShort(const char*, uint16_t) { return MQTT_PREFS_VERSION; }
|
||||
size_t getBytesLength(const char*) { return CompanionMqttPrefsNvs::kWriteSize; }
|
||||
size_t getBytesLength(const char*) { return saved_length; }
|
||||
size_t getBytes(const char*, void* dest, size_t length) {
|
||||
memcpy(dest, &saved, length);
|
||||
memcpy(dest, &persisted, length);
|
||||
return length;
|
||||
}
|
||||
size_t putBytes(const char*, const void* source, size_t length) {
|
||||
memset(&persisted, 0, sizeof(persisted));
|
||||
memcpy(&persisted, source, length);
|
||||
saved_length = length;
|
||||
return length;
|
||||
}
|
||||
size_t putUShort(const char*, uint16_t version) {
|
||||
assert(version == MQTT_PREFS_VERSION);
|
||||
return sizeof(version);
|
||||
}
|
||||
void end() {}
|
||||
};
|
||||
struct CompanionMqttSetupPortal {
|
||||
static bool loadStoredConfig(MQTTPrefs&);
|
||||
static bool saveStoredConfig(const MQTTPrefs&);
|
||||
static bool hasConfiguredSlot(const MQTTPrefs&);
|
||||
};
|
||||
"""
|
||||
@@ -76,6 +90,35 @@ int main() {
|
||||
assert(memcmp(&result, &saved, sizeof(saved)) == 0);
|
||||
assert(!CompanionMqttSetupPortal::hasConfiguredSlot(result));
|
||||
|
||||
// Enabling verification persists the appended flag, without lengthening
|
||||
// ordinary default-off Companion records or losing any broker credentials.
|
||||
MQTTPrefs enabled = result;
|
||||
enabled.mqtt_advert_verify = 1;
|
||||
assert(CompanionMqttSetupPortal::saveStoredConfig(enabled));
|
||||
assert(saved_length == 2881);
|
||||
assert(CompanionMqttSetupPortal::loadStoredConfig(result));
|
||||
assert(result.mqtt_advert_verify == 1);
|
||||
assert(strcmp(result.mqtt_slot_password[0], "keep-this-secret") == 0);
|
||||
// Every old recovery padding value is unconditionally default-off.
|
||||
saved_length=2880;
|
||||
for (uint8_t padding : {uint8_t(0), uint8_t(1), uint8_t(255)}) {
|
||||
saved.mqtt_advert_verify=padding;
|
||||
assert(CompanionMqttSetupPortal::loadStoredConfig(result));
|
||||
assert(result.mqtt_advert_verify==0);
|
||||
assert(strcmp(result.mqtt_slot_password[0], "keep-this-secret")==0);
|
||||
}
|
||||
saved_length=2881;
|
||||
saved.mqtt_advert_verify=1; persisted.marker=0; // malformed marker
|
||||
assert(CompanionMqttSetupPortal::loadStoredConfig(result));
|
||||
assert(result.mqtt_advert_verify==0);
|
||||
persisted.marker=CompanionMqttPrefsNvs::kVerifiedMarker;
|
||||
saved.mqtt_advert_verify=255;
|
||||
assert(CompanionMqttSetupPortal::loadStoredConfig(result));
|
||||
assert(result.mqtt_advert_verify == 0);
|
||||
assert(strcmp(result.mqtt_slot_password[0], "keep-this-secret") == 0);
|
||||
assert(CompanionMqttSetupPortal::saveStoredConfig(result));
|
||||
assert(saved_length == 2876);
|
||||
|
||||
// Saving a custom broker in stages must not reset it on the next boot.
|
||||
strcpy(saved.mqtt_slot_preset[0], "custom");
|
||||
assert(CompanionMqttSetupPortal::loadStoredConfig(result));
|
||||
@@ -143,6 +186,7 @@ class CompanionMqttConfigTests(unittest.TestCase):
|
||||
for signature in (
|
||||
"static bool isThreeLetterCode",
|
||||
"bool CompanionMqttSetupPortal::loadStoredConfig",
|
||||
"bool CompanionMqttSetupPortal::saveStoredConfig",
|
||||
"bool CompanionMqttSetupPortal::hasConfiguredSlot",
|
||||
):
|
||||
actual += function(source, signature)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Exercise the production MQTT advert gate and transactional CLI on the host."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def compile_run(source):
|
||||
with tempfile.TemporaryDirectory(prefix="mqtt-advert-verify-") as directory:
|
||||
cpp, binary = Path(directory) / "test.cpp", Path(directory) / "test"
|
||||
cpp.write_text(source, encoding="utf-8")
|
||||
result = subprocess.run([
|
||||
"g++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie", "-I", str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary),
|
||||
], text=True, capture_output=True)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
|
||||
class MqttAdvertVerifyTests(unittest.TestCase):
|
||||
def test_actual_queue_gate_rejects_bad_and_unsigned_tails_only_when_enabled(self):
|
||||
source = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text()
|
||||
method = extract_braced(source, "void MQTTBridge::onPacketReceived(")
|
||||
compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <helpers/AdvertSignature.h>
|
||||
constexpr unsigned PAYLOAD_TYPE_ADVERT=4, PAYLOAD_TYPE_TEXT=2;
|
||||
static unsigned verifies=0;
|
||||
static bool valid_signature=true;
|
||||
namespace mesh {
|
||||
struct Packet {
|
||||
uint8_t payload[184] = {};
|
||||
int payload_len=100;
|
||||
unsigned type=PAYLOAD_TYPE_ADVERT;
|
||||
unsigned getPayloadType() const { return type; }
|
||||
};
|
||||
struct Identity {
|
||||
const uint8_t* pub;
|
||||
explicit Identity(const uint8_t* p) : pub(p) {}
|
||||
bool verify(const uint8_t* sig, const uint8_t* message, int size) {
|
||||
++verifies;
|
||||
assert(sig == pub+36 && size>=36 && size<=68);
|
||||
assert(!memcmp(pub,message,36));
|
||||
if(size>36) assert(!memcmp(pub+100,message+36,size-36));
|
||||
return valid_signature;
|
||||
}
|
||||
};
|
||||
}
|
||||
struct MQTTBridge {
|
||||
struct Obs { bool mqtt_packets_enabled=true, mqtt_rx_enabled=true, mqtt_advert_verify=false; } obs;
|
||||
Obs* _obs=&obs;
|
||||
bool _initialized=true, type_allowed=true, scope_allowed=true;
|
||||
struct Stop { bool value=false; bool load(int)const{return value;} } _stop_requested;
|
||||
unsigned _filtered_packets=0, _bad_sig_adverts=0, queued=0;
|
||||
bool shouldQueuePacketType(unsigned, bool& filtered) { filtered=!type_allowed; return type_allowed; }
|
||||
bool allowsPacket(mesh::Packet*) { return scope_allowed; }
|
||||
void queuePacket(mesh::Packet*,bool tx){assert(!tx);++queued;}
|
||||
void onPacketReceived(mesh::Packet*);
|
||||
};
|
||||
namespace std { constexpr int memory_order_acquire=0; }
|
||||
''' + method + r'''
|
||||
int main() {
|
||||
MQTTBridge bridge; mesh::Packet packet;
|
||||
for(unsigned i=0;i<sizeof(packet.payload);++i) packet.payload[i]=uint8_t(i);
|
||||
// Default off preserves the observer's existing publish-as-heard behavior.
|
||||
valid_signature=false; bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==1 && verifies==0 && bridge._bad_sig_adverts==0);
|
||||
bridge.obs.mqtt_advert_verify=true;
|
||||
bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==1 && verifies==1 && bridge._bad_sig_adverts==1);
|
||||
valid_signature=true; packet.payload_len=132;
|
||||
bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==2 && verifies==2);
|
||||
// Valid signed prefixes cannot bless an appended unsigned byte.
|
||||
for(int size : {99,133,184}) {
|
||||
packet.payload_len=size; bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==2 && verifies==2);
|
||||
}
|
||||
assert(bridge._bad_sig_adverts==4);
|
||||
packet.type=PAYLOAD_TYPE_TEXT; bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==3 && verifies==2);
|
||||
packet.type=PAYLOAD_TYPE_ADVERT; packet.payload_len=100;
|
||||
bridge.scope_allowed=false; bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==3 && verifies==2 && bridge._filtered_packets==1);
|
||||
bridge.scope_allowed=true; bridge.type_allowed=false; bridge.onPacketReceived(&packet);
|
||||
assert(bridge.queued==3 && verifies==2 && bridge._filtered_packets==2);
|
||||
}
|
||||
''')
|
||||
|
||||
def test_actual_observer_setter_rejects_typos_and_rolls_back_failed_saves(self):
|
||||
source = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text()
|
||||
branch = extract_braced(source, 'if (memcmp(config, "mqtt.advert.verify ", 19) == 0)')
|
||||
compile_run(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
struct CLI {
|
||||
struct { uint8_t mqtt_advert_verify=0; } _mqtt_prefs;
|
||||
bool save_ok=true; unsigned saves=0;
|
||||
bool saveObserverPrefs(){++saves;return save_ok;}
|
||||
bool set(const char* config,char* reply) {
|
||||
''' + branch + r'''
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
CLI cli; char reply[160]={};
|
||||
assert(cli.set("mqtt.advert.verify on",reply));
|
||||
assert(cli.saves==1 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strcmp(reply,"OK"));
|
||||
assert(cli.set("mqtt.advert.verify onx",reply));
|
||||
assert(cli.saves==1 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strncmp(reply,"Error:",6));
|
||||
cli.save_ok=false; assert(cli.set("mqtt.advert.verify off",reply));
|
||||
assert(cli.saves==2 && cli._mqtt_prefs.mqtt_advert_verify==1 && !strncmp(reply,"Error:",6));
|
||||
cli.save_ok=true; assert(cli.set("mqtt.advert.verify off",reply));
|
||||
assert(cli.saves==3 && cli._mqtt_prefs.mqtt_advert_verify==0 && !strcmp(reply,"OK"));
|
||||
}
|
||||
''')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -75,6 +75,7 @@ struct MQTTBridge {
|
||||
void* _packet_queue_handle = reinterpret_cast<void*>(1);
|
||||
int _queue_count = 3, _skipped_publishes = 0;
|
||||
unsigned long _filtered_packets = 0;
|
||||
unsigned long _bad_sig_adverts = 0;
|
||||
static constexpr int MAX_QUEUE_SIZE = 50;
|
||||
bool isSlotReady(int) const { return true; }
|
||||
static void formatMqttStatsReply(char*, size_t);
|
||||
@@ -114,6 +115,10 @@ int main() {
|
||||
#endif
|
||||
// Normal five-slot stats still fit the fixed LoRa reply budget.
|
||||
assert(strstr(reply, "s5=123/4") != nullptr);
|
||||
assert(strstr(reply, "badsig=") == nullptr);
|
||||
bridge._bad_sig_adverts = 7;
|
||||
MQTTBridge::formatMqttStatsReply(reply, sizeof(reply));
|
||||
assert(strstr(reply, "badsig=7") != nullptr);
|
||||
|
||||
bridge.logMemoryStatus();
|
||||
#ifdef ESP_PLATFORM
|
||||
@@ -126,6 +131,7 @@ int main() {
|
||||
struct { char reply[32]; unsigned char guard[8]; } bounded;
|
||||
memset(&bounded, 0xA5, sizeof(bounded));
|
||||
bridge._filtered_packets = 4294967295UL;
|
||||
bridge._bad_sig_adverts = 4294967295UL;
|
||||
dma_free = 4294967295UL;
|
||||
dma_largest = 4294967295UL;
|
||||
MQTTBridge::formatMqttStatsReply(bounded.reply, sizeof(bounded.reply));
|
||||
|
||||
@@ -242,6 +242,7 @@ TEST(MQTTPrefsCodec, CurrentVersionedPayloadRoundTripsExactly) {
|
||||
}
|
||||
source.display_timeout_secs = 300;
|
||||
source.display_flip = 1;
|
||||
source.mqtt_advert_verify = 1;
|
||||
std::vector<uint8_t> bytes(Codec::kEncodedSize);
|
||||
ASSERT_EQ(Codec::kEncodedSize, Codec::encode(source, bytes.data(), bytes.size()));
|
||||
|
||||
@@ -331,11 +332,12 @@ TEST(MQTTPrefsCodec, EncodeRefusesAnOutputBufferShorterThanItsChosenPayload) {
|
||||
MQTTPrefs longest = defaults();
|
||||
longest.mqtt_slot_packet_filter[0] = 0;
|
||||
longest.display_flip = 1;
|
||||
longest.mqtt_advert_verify = 1;
|
||||
EXPECT_EQ(0u, Codec::encode(longest, bytes.data(), short_size));
|
||||
EXPECT_EQ(Codec::kEncodedSize, Codec::encode(longest, bytes.data(), bytes.size()));
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsCodec, EitherNonDefaultDisplayControlOptsIntoTheFullTail) {
|
||||
TEST(MQTTPrefsCodec, EitherNonDefaultDisplayControlKeepsThePreAdvertTail) {
|
||||
for (int scenario = 0; scenario < 2; ++scenario) {
|
||||
MQTTPrefs source = defaults();
|
||||
if (scenario == 0) {
|
||||
@@ -344,18 +346,70 @@ TEST(MQTTPrefsCodec, EitherNonDefaultDisplayControlOptsIntoTheFullTail) {
|
||||
source.display_flip = 1;
|
||||
}
|
||||
|
||||
ASSERT_EQ(Codec::kV1BaselinePayloadSize, Codec::payloadLenFor(source));
|
||||
ASSERT_EQ(Codec::kV1PreAdvertPayloadSize, Codec::payloadLenFor(source));
|
||||
std::vector<uint8_t> bytes(Codec::kEncodedSize);
|
||||
ASSERT_EQ(Codec::kEncodedSize,
|
||||
Codec::encode(source, bytes.data(), bytes.size()));
|
||||
const size_t written = Codec::encode(source, bytes.data(), bytes.size());
|
||||
ASSERT_EQ(sizeof(MQTTPrefsHeader) + Codec::kV1PreAdvertPayloadSize, written);
|
||||
bytes.resize(written);
|
||||
|
||||
const Codec::DecodePlan plan = classify(bytes);
|
||||
ASSERT_EQ(Codec::Source::Current, plan.source);
|
||||
ASSERT_EQ(Codec::kV1BaselinePayloadSize, plan.payload_len);
|
||||
ASSERT_EQ(Codec::kV1PreAdvertPayloadSize, plan.payload_len);
|
||||
MQTTPrefs loaded = defaults();
|
||||
memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
|
||||
EXPECT_EQ(source.display_timeout_secs, loaded.display_timeout_secs);
|
||||
EXPECT_EQ(source.display_flip, loaded.display_flip);
|
||||
EXPECT_EQ(0, loaded.mqtt_advert_verify);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsCodec, AdvertVerifyRoundTripsInOneAppendOnlyByteAndDefaultsOffOnOldFiles) {
|
||||
MQTTPrefs source = defaults();
|
||||
strcpy(source.wifi_ssid, "keep-network");
|
||||
strcpy(source.mqtt_slot_password[0], "keep-credentials");
|
||||
EXPECT_EQ(0, source.mqtt_advert_verify);
|
||||
EXPECT_EQ(2864u, Codec::payloadLenFor(source));
|
||||
source.mqtt_advert_verify = 1;
|
||||
EXPECT_EQ(2880u, Codec::payloadLenFor(source));
|
||||
std::vector<uint8_t> bytes(Codec::kEncodedSize);
|
||||
ASSERT_EQ(bytes.size(), Codec::encode(source, bytes.data(), bytes.size()));
|
||||
EXPECT_EQ(1, bytes[sizeof(MQTTPrefsHeader) + 2879]);
|
||||
const Codec::DecodePlan plan = classify(bytes);
|
||||
ASSERT_EQ(2880u, plan.payload_len);
|
||||
MQTTPrefs loaded = defaults();
|
||||
memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), plan.payload_len);
|
||||
EXPECT_EQ(1, loaded.mqtt_advert_verify);
|
||||
EXPECT_STREQ("keep-network", loaded.wifi_ssid);
|
||||
EXPECT_STREQ("keep-credentials", loaded.mqtt_slot_password[0]);
|
||||
|
||||
// Each shipped older v1 boundary leaves the new default intact.
|
||||
const auto complete = bytes;
|
||||
for (size_t boundary : {size_t(2736), size_t(2860), size_t(2864),
|
||||
size_t(2876), size_t(2879)}) {
|
||||
bytes = complete;
|
||||
bytes.resize(sizeof(MQTTPrefsHeader) + boundary);
|
||||
writeHeader(&bytes, MQTT_PREFS_VERSION, static_cast<uint16_t>(boundary));
|
||||
const Codec::DecodePlan older = classify(bytes);
|
||||
ASSERT_EQ(Codec::Source::Current, older.source) << boundary;
|
||||
ASSERT_EQ(boundary, older.payload_len);
|
||||
loaded = defaults();
|
||||
memcpy(&loaded, bytes.data() + sizeof(MQTTPrefsHeader), older.payload_len);
|
||||
EXPECT_EQ(0, loaded.mqtt_advert_verify) << boundary;
|
||||
EXPECT_STREQ("keep-network", loaded.wifi_ssid);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsCodec, InvalidAdvertVerifyRepairsWithoutDiscardingCredentials) {
|
||||
MQTTPrefs prefs = defaults();
|
||||
strcpy(prefs.wifi_ssid, "keep-network");
|
||||
prefs.mqtt_advert_verify = 1;
|
||||
EXPECT_FALSE(Codec::repairDisplayPrefs(&prefs));
|
||||
for (uint8_t value : {uint8_t(2), uint8_t(255)}) {
|
||||
prefs.mqtt_advert_verify = value;
|
||||
EXPECT_TRUE(Codec::repairDisplayPrefs(&prefs));
|
||||
EXPECT_EQ(0, prefs.mqtt_advert_verify);
|
||||
EXPECT_EQ(2864u, Codec::payloadLenFor(prefs));
|
||||
EXPECT_STREQ("keep-network", prefs.wifi_ssid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,6 +709,12 @@ TEST(MQTTPrefsCodec, LongerDeclaredLengthWithoutTheBytesIsStillCorrupt) {
|
||||
}
|
||||
|
||||
TEST(CompanionMqttPrefsNvs, WritesRollbackReadableShapeAndAcceptsRecoveryShape) {
|
||||
MQTTPrefs prefs = defaults();
|
||||
EXPECT_EQ(2876u, CompanionMqttPrefsNvs::writeSizeFor(prefs));
|
||||
prefs.mqtt_advert_verify = 1;
|
||||
EXPECT_EQ(2881u, CompanionMqttPrefsNvs::writeSizeFor(prefs));
|
||||
prefs.mqtt_advert_verify = 0;
|
||||
EXPECT_EQ(2876u, CompanionMqttPrefsNvs::writeSizeFor(prefs));
|
||||
EXPECT_EQ(MQTT_PREFS_V1_PRE_DISPLAY_PAYLOAD_SIZE,
|
||||
CompanionMqttPrefsNvs::kWriteSize);
|
||||
EXPECT_LT(CompanionMqttPrefsNvs::kWriteSize, sizeof(MQTTPrefs));
|
||||
@@ -663,6 +723,8 @@ TEST(CompanionMqttPrefsNvs, WritesRollbackReadableShapeAndAcceptsRecoveryShape)
|
||||
MQTT_PREFS_VERSION, CompanionMqttPrefsNvs::kWriteSize));
|
||||
EXPECT_TRUE(CompanionMqttPrefsNvs::accepts(
|
||||
MQTT_PREFS_VERSION, CompanionMqttPrefsNvs::kRecoverySize));
|
||||
EXPECT_TRUE(CompanionMqttPrefsNvs::accepts(
|
||||
MQTT_PREFS_VERSION, CompanionMqttPrefsNvs::kVerifiedSize));
|
||||
EXPECT_EQ(2880u, CompanionMqttPrefsNvs::kRecoverySize);
|
||||
EXPECT_LE(CompanionMqttPrefsNvs::kRecoverySize, sizeof(MQTTPrefs));
|
||||
EXPECT_FALSE(CompanionMqttPrefsNvs::accepts(
|
||||
|
||||
@@ -154,7 +154,7 @@ TEST(MQTTPrefsJsonImport, DecodesEveryObserverGroupAndDisplayTail) {
|
||||
"wifi:{ssid:\"mesh-net\",password:\"p\\\\\\\"ass\\nword\",power_save:2},"
|
||||
"time:{timezone:\"MST7MDT\",utc_offset:-6,ntp_server:\"time.example\"},"
|
||||
"mqtt:{origin:\"observer-one\",iata:\"sea\",packets_enabled:1,"
|
||||
"raw_enabled:1,tx_enabled:1,rx_enabled:0,"
|
||||
"raw_enabled:1,tx_enabled:1,rx_enabled:0,advert_verify:1,"
|
||||
"status:{enabled:1,interval_ms:60000},"
|
||||
"neighbors:{enabled:1,interval_ms:43200000},"
|
||||
"owner:{public_key:\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\","
|
||||
@@ -182,6 +182,7 @@ TEST(MQTTPrefsJsonImport, DecodesEveryObserverGroupAndDisplayTail) {
|
||||
EXPECT_STREQ("SEA", candidate.mqtt_iata);
|
||||
EXPECT_EQ(1, candidate.mqtt_raw_enabled);
|
||||
EXPECT_EQ(0, candidate.mqtt_rx_enabled);
|
||||
EXPECT_EQ(1, candidate.mqtt_advert_verify);
|
||||
EXPECT_EQ(60000u, candidate.mqtt_status_interval);
|
||||
EXPECT_EQ(MQTT_NEIGHBORS_MIN_INTERVAL_MS,
|
||||
candidate.mqtt_neighbors_interval);
|
||||
@@ -213,6 +214,7 @@ TEST(MQTTPrefsJsonImport, MissingFieldsRetainCurrentBuildDefaults) {
|
||||
EXPECT_EQ(0, candidate.wifi_power_save);
|
||||
EXPECT_EQ(300000u, candidate.mqtt_status_interval);
|
||||
EXPECT_EQ(60, candidate.display_timeout_secs);
|
||||
EXPECT_EQ(0, candidate.mqtt_advert_verify);
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsJsonImport, SemanticRepairsUseSchemaAndCurrentWifiDefaults) {
|
||||
@@ -221,7 +223,7 @@ TEST(MQTTPrefsJsonImport, SemanticRepairsUseSchemaAndCurrentWifiDefaults) {
|
||||
const char* text =
|
||||
"{version:1,wifi:{power_save:9},time:{utc_offset:99,ntp_server:\"bad/host\"},"
|
||||
"mqtt:{iata:\"bad!\",packets_enabled:-1,raw_enabled:2,tx_enabled:9,"
|
||||
"rx_enabled:2,status:{enabled:3,interval_ms:10},"
|
||||
"rx_enabled:2,advert_verify:5,status:{enabled:3,interval_ms:10},"
|
||||
"neighbors:{enabled:2,interval_ms:100},"
|
||||
"slot1:{preset:\"bogus\",port:-1,packet_filter:70000}},"
|
||||
"snmp:{enabled:2},radio:{watchdog_min:121},"
|
||||
@@ -238,6 +240,7 @@ TEST(MQTTPrefsJsonImport, SemanticRepairsUseSchemaAndCurrentWifiDefaults) {
|
||||
EXPECT_EQ(0, candidate.mqtt_raw_enabled);
|
||||
EXPECT_EQ(2, candidate.mqtt_tx_enabled);
|
||||
EXPECT_EQ(1, candidate.mqtt_rx_enabled);
|
||||
EXPECT_EQ(0, candidate.mqtt_advert_verify);
|
||||
EXPECT_EQ(300000u, candidate.mqtt_status_interval);
|
||||
EXPECT_EQ(MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS,
|
||||
candidate.mqtt_neighbors_interval);
|
||||
@@ -267,6 +270,9 @@ TEST(MQTTPrefsJsonImport, RejectsKnownTypeDuplicateLengthAndOverflowErrors) {
|
||||
"{version:{x:1}}",
|
||||
"{version:1,mqtt:1}",
|
||||
"{version:1,mqtt:{slot1:1}}",
|
||||
"{version:1,mqtt:{advert_verify:true}}",
|
||||
"{version:1,mqtt:{advert_verify:\"on\"}}",
|
||||
"{version:1,mqtt:{advert_verify:1,advert_verify:0}}",
|
||||
"{version:1,mqtt:{status:{enabled:1},status:{interval_ms:60000}}}"}) {
|
||||
MQTTPrefs candidate = defaults;
|
||||
EXPECT_FALSE(Import::loaded(decodeText(text, defaults, &candidate))) << text;
|
||||
@@ -339,16 +345,31 @@ TEST(MQTTPrefsJsonImport, ParsedDisplayPrefsRouteThroughFullAtomicBinaryCommit)
|
||||
ASSERT_EQ(Import::Result::Loaded,
|
||||
decodeText("{version:1,display:{timeout_s:45,flip:1}}",
|
||||
defaults, &candidate));
|
||||
EXPECT_EQ(MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
|
||||
EXPECT_EQ(MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE,
|
||||
MQTTPrefsCodec::payloadLenFor(candidate));
|
||||
|
||||
CommitStore store(true);
|
||||
ASSERT_EQ(Atomic::Result::Committed, commitPrefs(&store, candidate));
|
||||
ASSERT_EQ(sizeof(MQTTPrefsHeader) + MQTT_PREFS_V1_FULL_PAYLOAD_SIZE,
|
||||
ASSERT_EQ(sizeof(MQTTPrefsHeader) + MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE,
|
||||
store.primary.size());
|
||||
MQTTPrefsHeader header = {};
|
||||
memcpy(&header, store.primary.data(), sizeof(header));
|
||||
EXPECT_EQ(MQTT_PREFS_V1_FULL_PAYLOAD_SIZE, header.payload_len);
|
||||
EXPECT_EQ(MQTT_PREFS_V1_PRE_ADVERT_PAYLOAD_SIZE, header.payload_len);
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsJsonImport, AdvertVerifyDefaultsOffRoundTripsAndPersistsThroughBinaryCommit) {
|
||||
const MQTTPrefs defaults = makeDefaults();
|
||||
EXPECT_EQ(0, defaults.mqtt_advert_verify);
|
||||
MQTTPrefs candidate = defaults;
|
||||
ASSERT_EQ(Import::Result::Loaded,
|
||||
decodeText("{version:1,mqtt:{advert_verify:1}}", defaults, &candidate));
|
||||
ASSERT_EQ(1, candidate.mqtt_advert_verify);
|
||||
CommitStore store(true);
|
||||
ASSERT_EQ(Atomic::Result::Committed, commitPrefs(&store, candidate));
|
||||
ASSERT_EQ(sizeof(MQTTPrefsHeader) + 2880u, store.primary.size());
|
||||
MQTTPrefs loaded = defaults;
|
||||
memcpy(&loaded, store.primary.data() + sizeof(MQTTPrefsHeader), 2880);
|
||||
EXPECT_EQ(1, loaded.mqtt_advert_verify);
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsJsonImport, FailedAtomicPublishPreservesPreviousBinaryImage) {
|
||||
|
||||
@@ -47,6 +47,10 @@ HARNESS = r'''
|
||||
#define MAX_ESPNOW_PACKET_SIZE 256
|
||||
unsigned millis() { return 100; }
|
||||
namespace mesh {
|
||||
struct Identity {
|
||||
explicit Identity(const uint8_t*) {}
|
||||
bool verify(const uint8_t*,const uint8_t*,int) {assert(false);return false;}
|
||||
};
|
||||
namespace espnow {
|
||||
struct ESPNowRawFrames {uint8_t count=0;uint16_t lengths[2]={};uint8_t data[2][256]={};};
|
||||
bool encodeEspNowRawFrames(const uint8_t*,size_t,ESPNowRawFrames& f) {f.count=1;return true;}
|
||||
@@ -57,6 +61,10 @@ namespace bridge {
|
||||
const char* espNowFormatName(int) {return "raw";}
|
||||
}
|
||||
}
|
||||
namespace AdvertSignature {
|
||||
constexpr unsigned kMaxMessageLen=68;
|
||||
int buildMessage(const uint8_t*,int,uint8_t*,const uint8_t**) {assert(false);return -1;}
|
||||
}
|
||||
struct Seen {
|
||||
unsigned checks=0,marks=0;
|
||||
bool wasSeen(mesh::Packet*) {++checks;return false;}
|
||||
@@ -112,11 +120,11 @@ struct ESPNowBridge : BridgeBase {
|
||||
};
|
||||
struct MQTTBridge : BridgeBase {
|
||||
std::atomic<bool> _stop_requested{false};
|
||||
struct Obs {bool mqtt_packets_enabled=true,mqtt_rx_enabled=true;uint8_t mqtt_tx_enabled=1;} obs;
|
||||
struct Obs {bool mqtt_packets_enabled=true,mqtt_rx_enabled=true,mqtt_advert_verify=false;uint8_t mqtt_tx_enabled=1;} obs;
|
||||
Obs* _obs=&obs;
|
||||
struct Identity {uint8_t pub_key[32]={};} id;
|
||||
Identity* _identity=&id;
|
||||
unsigned _filtered_packets=0;
|
||||
unsigned _filtered_packets=0,_bad_sig_adverts=0;
|
||||
bool _staged_raw_valid=false;
|
||||
bool shouldQueuePacketType(uint8_t,bool&) {return true;}
|
||||
void queuePacket(mesh::Packet*,bool) {++queued;}
|
||||
|
||||
@@ -100,3 +100,74 @@ lib_deps =
|
||||
extends = Waveshare_S3_LR1121
|
||||
build_src_filter = ${Waveshare_S3_LR1121.build_src_filter}
|
||||
+<../examples/kiss_modem/>
|
||||
|
||||
[env:Waveshare_S3_LR1121_repeater_observer_mqtt]
|
||||
extends = Waveshare_S3_LR1121
|
||||
extra_scripts =
|
||||
${esp32_base.extra_scripts}
|
||||
pre:scripts/generate_cert_bundle.py
|
||||
board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${Waveshare_S3_LR1121.build_flags}
|
||||
-D ADVERT_NAME='"MQTT Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_MQTT_BRIDGE=1
|
||||
-D MQTT_MAX_PACKET_SIZE=1024
|
||||
-D MQTT_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
-D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||
-D ESP32_CPU_FREQ=160
|
||||
build_src_filter = ${Waveshare_S3_LR1121.build_src_filter}
|
||||
+<helpers/bridges/MQTTBridge.cpp>
|
||||
+<helpers/MQTTMessageBuilder.cpp>
|
||||
+<helpers/JWTHelper.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
${Waveshare_S3_LR1121.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
elims/PsychicMqttClient@^0.2.4
|
||||
bblanchon/ArduinoJson @ 7.4.3
|
||||
arduino-libraries/NTPClient
|
||||
JChristensen/Timezone
|
||||
paulstoffregen/Time@1.6.1
|
||||
|
||||
[env:Waveshare_S3_LR1121_room_server_observer_mqtt]
|
||||
extends = Waveshare_S3_LR1121
|
||||
extra_scripts =
|
||||
${esp32_base.extra_scripts}
|
||||
pre:scripts/generate_cert_bundle.py
|
||||
board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${Waveshare_S3_LR1121.build_flags}
|
||||
-D ADVERT_NAME='"Waveshare LR1121 Room Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D ROOM_PASSWORD='"hello"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_MQTT_BRIDGE=1
|
||||
-D MQTT_MAX_PACKET_SIZE=1024
|
||||
-D MQTT_DEBUG=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
-D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||
-D ESP32_CPU_FREQ=160
|
||||
build_src_filter = ${Waveshare_S3_LR1121.build_src_filter}
|
||||
+<helpers/bridges/MQTTBridge.cpp>
|
||||
+<helpers/MQTTMessageBuilder.cpp>
|
||||
+<helpers/JWTHelper.cpp>
|
||||
+<../examples/simple_room_server>
|
||||
lib_deps =
|
||||
${Waveshare_S3_LR1121.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
elims/PsychicMqttClient@^0.2.4
|
||||
bblanchon/ArduinoJson @ 7.4.3
|
||||
arduino-libraries/NTPClient
|
||||
JChristensen/Timezone
|
||||
paulstoffregen/Time@1.6.1
|
||||
@@ -492,6 +492,8 @@ body.tab-cli{padding-bottom:0}
|
||||
<span class="tgl"><input type="checkbox" data-k="mqtt.raw"><u></u></span></div>
|
||||
<div class="sw"><span><b>Received packets</b><i>Report packets heard over the air</i></span>
|
||||
<span class="tgl"><input type="checkbox" data-k="mqtt.rx"><u></u></span></div>
|
||||
<div class="sw"><span><b>Drop bad advert signatures</b><i>Skip received adverts that fail verification. Off keeps RF corruption visible to collectors</i></span>
|
||||
<span class="tgl"><input type="checkbox" data-k="mqtt.advert.verify"><u></u></span></div>
|
||||
<div class="sw"><span><b>Publish neighbors</b><i>Periodic neighbor table and scopes (PSRAM boards only)</i></span>
|
||||
<span class="tgl"><input type="checkbox" data-k="mqtt.neighbors"><u></u></span></div>
|
||||
<div class="row" style="margin-top:6px">
|
||||
@@ -839,6 +841,7 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
|
||||
case"mqtt.origin":return q.origin;case"mqtt.iata":return q.iata;
|
||||
case"mqtt.status":return q.status?"on":"off";case"mqtt.packets":return q.packets?"on":"off";
|
||||
case"mqtt.raw":return q.raw?"on":"off";case"mqtt.tx":return q.tx;case"mqtt.rx":return q.rx?"on":"off";
|
||||
case"mqtt.advert.verify":return q.advert_verify?"on":"off";
|
||||
case"mqtt.interval":return String(q.interval);
|
||||
case"mqtt.neighbors":return q.neighbors?"on":"off";case"mqtt.neighbors.interval":return String(q.neighbors_interval);
|
||||
case"mqtt.ntp":return q.ntp;
|
||||
@@ -1960,6 +1963,7 @@ var CLI_KEYS=[
|
||||
["mqtt.packets","Publish packet messages",0,"on|off"],
|
||||
["mqtt.raw","Also publish full raw frames",0,"on|off"],
|
||||
["mqtt.rx","Publish packets heard over the air",0,"on|off"],
|
||||
["mqtt.advert.verify","Drop received adverts with bad signatures",0,"on|off"],
|
||||
["mqtt.tx","Publish packets this node sends",0,"off|on|advert"],
|
||||
["mqtt.interval","Status publish interval {1-60 min}",0],
|
||||
["mqtt.neighbors","Publish the neighbour table (PSRAM boards)",0,"on|off"],
|
||||
|
||||
Reference in new issue
Block a user