Add saved repeater trace exception while repeating is disabled
This commit is contained in:
1 parent
4bec31b4e1
commit
1ddca12a41
14 files changed
+761
-22
No files matched your search
@@ -96,6 +96,11 @@ jobs:
|
||||
- name: Verify bridge filter wiring and shorthand permissions
|
||||
run: python3 -B test/test_transport_filter_wiring.py -v
|
||||
|
||||
- name: Verify saved repeat-off trace exception and relay guards
|
||||
run: |
|
||||
python3 -B test/test_repeater_trace_repeat.py -v
|
||||
python3 -B test/test_repeater_trace_forwarding.py -v
|
||||
|
||||
- name: Verify MQTT start results and connection-attempt accounting
|
||||
run: python3 -B test/test_mqtt_transport_results.py -v
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
|
||||
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes |
|
||||
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat.trace`](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only) | Repeater only; saved repeat-off trace exception, default off | Yes | Yes | Yes |
|
||||
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Role that supports path-hash selection | Yes | Yes | Yes |
|
||||
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater | Yes | Yes | Yes |
|
||||
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes |
|
||||
@@ -338,6 +339,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
|
||||
| System | [`get/set reboot.interval`](#reboot-interval) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Clock sync | [`get/set clock.sync.*`; `clock.sync.mesh now`](cli_commands.md#estimate-and-correct-infrastructure-node-time-after-startup) | Repeater, sensor, and room server; `clock.sync.internet` needs MQTT repeater | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat`](cli_commands.md#view-or-change-this-nodes-repeat-flag) | Forwarding-capable role | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set repeat.trace`](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only) | Repeater only; saved repeat-off trace exception, default off | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set path.hash.mode`](cli_commands.md#view-or-change-this-nodes-advert-path-hash-size) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set loop.detect`](cli_commands.md#view-or-change-this-nodes-loop-detection) | Repeater, full common parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Routing | [`get/set txdelay`](cli_commands.md#view-or-change-the-retransmit-delay-factor-for-flood-traffic) | All text CLI roles, including bridges | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
+41
-1
@@ -2382,7 +2382,47 @@ The pin number is the Arduino pin number used by that target (the normal GPIO nu
|
||||
**Parameters:**
|
||||
- `state`: `on`|`off`
|
||||
|
||||
**Default:** `flood.channel.data on`; `flood.channel.data.hops h=all`
|
||||
**Default:** `repeat on`.
|
||||
|
||||
`repeat off` disables relay forwarding, not locally generated traffic or
|
||||
configuration replies. Repeaters can separately opt in to the routed-trace
|
||||
exception below.
|
||||
|
||||
#### Allow routed traces while repeating is disabled (repeater only)
|
||||
|
||||
```text
|
||||
get repeat.trace
|
||||
set repeat.trace on
|
||||
set repeat.trace off
|
||||
```
|
||||
|
||||
Default: `off`, including upgrades from older preference files. The setting is
|
||||
saved and takes effect without reboot. Failed saves leave the active choice
|
||||
unchanged. Room Server, Sensor, and Companion do not gain this exception.
|
||||
|
||||
To keep ordinary forwarding disabled while allowing routed trace diagnostics:
|
||||
|
||||
```text
|
||||
set repeat off
|
||||
set repeat.trace on
|
||||
```
|
||||
|
||||
Only direct TRACE packets with a matching next hop may be relayed. Ordinary
|
||||
messages, adverts, group data, ACKs, and control packets remain subject to
|
||||
`repeat off`; flood traces remain invalid. Existing trace length, path-capacity,
|
||||
duplicate, radio-profile, and channel/duty-cycle checks still apply. This is
|
||||
a diagnostics opt-in, not authenticated-admin-only forwarding: trace packets
|
||||
retain the protocol's existing authentication behavior.
|
||||
|
||||
`get repeat` and the disabled status bit continue to report repeating off.
|
||||
With `repeat on`, traces use the ordinary forwarding policy regardless of
|
||||
`repeat.trace`. Changing the exception to off while repeating is off blocks
|
||||
subsequent relayed trace transmissions, including queued copies and retries;
|
||||
a trace already on air is allowed to finish. Locally initiated traffic and
|
||||
management replies are not cancelled.
|
||||
|
||||
The dynamic configuration field is `repeat.trace_off` (`0` or `1`). It is
|
||||
appended to `/com_prefs`; all previously saved field offsets stay unchanged.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -260,6 +260,9 @@ settings override these defaults. To toggle only MQTT while keeping
|
||||
USB logging unchanged, use `set mqtt.enabled off` / `on`. Neither setting
|
||||
turns LoRa repeating off. Repeater forwarding uses `set repeat off` / `on`
|
||||
and `get repeat` separately.
|
||||
Repeaters can opt in to routed trace diagnostics with `set repeat.trace on`
|
||||
while `repeat` stays off; the saved exception defaults off and does not relay
|
||||
other payload types. See [repeat-off traces](cli_commands.md#allow-routed-traces-while-repeating-is-disabled-repeater-only).
|
||||
The selector changes USB master output, not saved `usb.debug` intent. Direct
|
||||
WiFi MQTT is independent of that USB verbosity preference.
|
||||
|
||||
|
||||
@@ -1022,7 +1022,8 @@ bool MyMesh::floodChannelDataHopApplies(const mesh::Packet* packet) const {
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (_prefs.disable_fwd && !(_prefs.trace_when_repeat_off == 1 && packet
|
||||
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE)) return false;
|
||||
if (packet->isRouteFlood()) {
|
||||
if (mesh::isFloodHopLimitExceeded(packet, _prefs.flood_max,
|
||||
_prefs.flood_max_unscoped,
|
||||
@@ -1086,6 +1087,16 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketTransmit(const mesh::Packet* packet) const {
|
||||
// Forwarded traces have already appended this node's SNR. A locally
|
||||
// originated trace starts with an empty SNR path and is not a relay.
|
||||
// Recheck at egress so a queued relay/retry cannot outlive this opt-in.
|
||||
if (_prefs.disable_fwd && _prefs.trace_when_repeat_off != 1 && packet
|
||||
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE
|
||||
&& packet->path_len > 0) return false;
|
||||
return mesh::Mesh::allowPacketTransmit(packet);
|
||||
}
|
||||
|
||||
const char *MyMesh::getLogDateTime() {
|
||||
static char tmp[32];
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
@@ -1462,7 +1473,9 @@ uint32_t MyMesh::getDirectRetryAttemptStepMillis() const {
|
||||
}
|
||||
|
||||
bool MyMesh::allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const {
|
||||
(void)packet;
|
||||
if (_prefs.disable_fwd && _prefs.trace_when_repeat_off != 1 && packet
|
||||
&& packet->isRouteDirect() && packet->getPayloadType() == PAYLOAD_TYPE_TRACE
|
||||
&& packet->path_len > 0) return false;
|
||||
if (!_prefs.direct_retry_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -917,6 +917,7 @@ protected:
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
bool allowPacketTransmit(const mesh::Packet* packet) const override;
|
||||
const char* getLogDateTime() override;
|
||||
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
|
||||
|
||||
|
||||
@@ -764,6 +764,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
||||
_prefs->usb_logging_enabled = 1;
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
_prefs->trace_when_repeat_off = 0;
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
#else
|
||||
@@ -935,9 +936,10 @@ void CommonCLI::syncOtaConfigFromPrefs() {
|
||||
#endif
|
||||
|
||||
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
// Older or truncated preference images must never inherit debug opt-in
|
||||
// Older or truncated preference images must never inherit these opt-ins
|
||||
// from a previous load of a newer image.
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
_prefs->trace_when_repeat_off = 0;
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = fs->open(filename, "r");
|
||||
#else
|
||||
@@ -1330,6 +1332,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->usb_debug_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->usb_debug_enabled,
|
||||
sizeof(_prefs->usb_debug_enabled));
|
||||
if (file.available() >= (int)sizeof(_prefs->trace_when_repeat_off)) {
|
||||
file.read((uint8_t *)&_prefs->trace_when_repeat_off,
|
||||
sizeof(_prefs->trace_when_repeat_off));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1445,6 +1451,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->usb_logging_enabled = constrain(_prefs->usb_logging_enabled, 0, 1); // boolean
|
||||
// Only a deliberately saved 1 enables diagnostics; corrupted bytes are OFF.
|
||||
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
||||
// Corrupt or unknown values must not enable a repeat-off exception.
|
||||
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
|
||||
_prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean
|
||||
if (!directRetryPrefsValid(_prefs)) {
|
||||
setDefaultDirectRetryPrefs(_prefs);
|
||||
@@ -1658,6 +1666,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->primary_radio_preamble); // appended primary tuple field
|
||||
WRITE_COMMON_PREFS(&prefs->espnow_bridge_enabled); // appended Full ESP-NOW intent
|
||||
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
|
||||
WRITE_COMMON_PREFS(&prefs->trace_when_repeat_off); // appended repeater trace exception
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -1671,6 +1680,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
_common_save_result_known = true;
|
||||
_common_save_succeeded = false;
|
||||
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
|
||||
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
|
||||
}
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// Observer builds use a verified temp/backup transaction for common prefs.
|
||||
@@ -1817,6 +1827,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)&_prefs->primary_radio_preamble, sizeof(_prefs->primary_radio_preamble)); // appended
|
||||
file.write((uint8_t *)&_prefs->espnow_bridge_enabled, sizeof(_prefs->espnow_bridge_enabled)); // appended
|
||||
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
|
||||
file.write((uint8_t *)&_prefs->trace_when_repeat_off, sizeof(_prefs->trace_when_repeat_off)); // appended
|
||||
|
||||
_common_save_succeeded = file.commit();
|
||||
if (!_common_save_succeeded) {
|
||||
@@ -3349,6 +3360,29 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
||||
|
||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
if (strncmp(config, "repeat.trace", 12) == 0
|
||||
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
||||
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
||||
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
|
||||
return;
|
||||
}
|
||||
const char* value = config + 12;
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
strcpy(reply, "Error: usage set repeat.trace on|off");
|
||||
return;
|
||||
}
|
||||
const uint8_t previous = _prefs->trace_when_repeat_off;
|
||||
_prefs->trace_when_repeat_off = strcmp(value, "on") == 0 ? 1 : 0;
|
||||
if (!trySavePrefs()) {
|
||||
_prefs->trace_when_repeat_off = previous;
|
||||
strcpy(reply, "Error: Repeat trace not saved; unchanged");
|
||||
return;
|
||||
}
|
||||
_callbacks->onRetryConfigChanged();
|
||||
snprintf(reply, 160, "OK - repeat.trace %s (saved)", value);
|
||||
return;
|
||||
}
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
|
||||
#endif
|
||||
@@ -4695,6 +4729,17 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
|
||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
if (strncmp(config, "repeat.trace", 12) == 0
|
||||
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
||||
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
||||
strcpy(reply, "Error: repeat.trace is only supported by repeaters");
|
||||
} else if (strcmp(config, "repeat.trace") != 0) {
|
||||
strcpy(reply, "Error: usage get repeat.trace");
|
||||
} else {
|
||||
snprintf(reply, 160, "> %s", _prefs->trace_when_repeat_off == 1 ? "on" : "off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
if (mesh::power::handleVoltagePolicyCommand(command, reply, 160)) return;
|
||||
#endif
|
||||
|
||||
@@ -183,8 +183,11 @@ public:
|
||||
// logging artifact's enabled-at-first-boot behavior.
|
||||
uint8_t usb_logging_enabled = 1;
|
||||
// USB diagnostics are opt-in independently of the packet output gate.
|
||||
// Persisted at the very end of /com_prefs, not beside usb_log.
|
||||
// Appended after ESP-NOW intent in /com_prefs, not beside usb_log.
|
||||
uint8_t usb_debug_enabled = 0;
|
||||
// Repeater-only opt-in for routed trace diagnostics while repeat is off.
|
||||
// Appended after usb_debug in /com_prefs; existing images stay disabled.
|
||||
uint8_t trace_when_repeat_off = 0;
|
||||
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
|
||||
// /com_prefs offset 862; single-UART builds keep their compiled port here.
|
||||
uint8_t bridge_uart = 0;
|
||||
@@ -377,6 +380,7 @@ private:
|
||||
protected:
|
||||
void structure() override {
|
||||
def("disable", _parent->disable_fwd);
|
||||
def("trace_off", _parent->trace_when_repeat_off);
|
||||
def("f_max", _parent->flood_max);
|
||||
def("f_max_uns", _parent->flood_max_unscoped);
|
||||
def("f_max_adv", _parent->flood_max_advert);
|
||||
|
||||
@@ -573,15 +573,19 @@ bool isUsbLoggingWatchdogUpdateActive() { return false; }
|
||||
bool handleUsbLoggingWatchdogCommand(const char* command, char* reply,
|
||||
size_t capacity) {
|
||||
if (!command || !reply || !capacity) return false;
|
||||
static const char* const keys[] = {"get usb.watchdog.last", "get usb.watchdog", "set usb.watchdog"};
|
||||
for (const char* key : keys) {
|
||||
const size_t length = strlen(key);
|
||||
if (strncmp(command, key, length)
|
||||
|| (command[length] && command[length] != ' ' && command[length] != '\t')) continue;
|
||||
snprintf(reply, capacity, "Error: unsupported");
|
||||
return true;
|
||||
const char* suffix;
|
||||
constexpr size_t prefix_size = sizeof("get usb.watchdog") - 1;
|
||||
if (strncmp(command, "get usb.watchdog", prefix_size) == 0) {
|
||||
suffix = command + prefix_size;
|
||||
if (strncmp(suffix, ".last", 5) == 0) suffix += 5;
|
||||
} else if (strncmp(command, "set usb.watchdog", prefix_size) == 0) {
|
||||
suffix = command + prefix_size;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (*suffix && *suffix != ' ' && *suffix != '\t') return false;
|
||||
snprintf(reply, capacity, "Error: unsupported");
|
||||
return true;
|
||||
}
|
||||
} // namespace mesh
|
||||
#endif // MESH_USB_CONSOLE_COOPERATIVE
|
||||
|
||||
@@ -82,8 +82,9 @@ int main() {
|
||||
CommonCLI cli;
|
||||
assert(cli.saveCommonPrefs());
|
||||
auto original=cli.fs.files["/com_prefs"];
|
||||
const size_t preamble_offset=original.size()-4;
|
||||
assert(preamble_offset>=864);
|
||||
// Appending preferences must not move the established preamble bytes.
|
||||
const size_t preamble_offset=866;
|
||||
assert(original.size() >= preamble_offset+2);
|
||||
assert(original[preamble_offset]==48 && original[preamble_offset+1]==0);
|
||||
assert(original[preamble_offset+2]==1);
|
||||
Capture capture; assert(writeCommonPrefsImage(capture,&cli.prefs));
|
||||
|
||||
@@ -157,11 +157,11 @@ int main() {
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto baseline = cli.fs.files["/com_prefs"];
|
||||
assert(baseline.size() == 870);
|
||||
const size_t debug_offset = baseline.size() - 1;
|
||||
const size_t logging_offset = baseline.size() - 7;
|
||||
const size_t preamble_offset = baseline.size() - 4;
|
||||
const size_t espnow_offset = baseline.size() - 2;
|
||||
assert(baseline.size() == 871);
|
||||
const size_t debug_offset = baseline.size() - 2;
|
||||
const size_t logging_offset = baseline.size() - 8;
|
||||
const size_t preamble_offset = baseline.size() - 5;
|
||||
const size_t espnow_offset = baseline.size() - 3;
|
||||
assert(baseline[debug_offset] == 0 && baseline[logging_offset] == 1);
|
||||
assert(baseline[preamble_offset] == 48 && baseline[espnow_offset] == 1);
|
||||
Capture capture;
|
||||
@@ -249,10 +249,11 @@ int main() {
|
||||
assert(cli.prefs.usb_debug_enabled == 0 && cli.fs.files["/com_prefs"][debug_offset] == 0);
|
||||
}
|
||||
|
||||
// Every older/torn length inherits debug OFF, even after loading debug ON.
|
||||
// Every image without the complete debug byte inherits debug OFF, even
|
||||
// after loading debug ON; a missing later trace byte cannot erase debug.
|
||||
auto image = baseline;
|
||||
image[logging_offset] = 0; image[debug_offset] = 1;
|
||||
for (size_t size = 0; size < image.size(); ++size) {
|
||||
for (size_t size = 0; size <= debug_offset; ++size) {
|
||||
CommonCLI reader;
|
||||
reader.prefs.usb_debug_enabled = 1;
|
||||
reader.fs.files["/com_prefs"] = {image.begin(), image.begin() + size};
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Execute actual repeater trace gates with the production Packet definition."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <Packet.h>
|
||||
#include <helpers/RoutingPolicy.h>
|
||||
#define LOOP_DETECT_OFF 0
|
||||
#define LOOP_DETECT_MINIMAL 1
|
||||
#define LOOP_DETECT_MODERATE 2
|
||||
#define LOOP_DETECT_STRICT 3
|
||||
#define FLOOD_CHANNEL_HOPS_ALL 255
|
||||
const uint8_t max_loop_minimal[] = {1}, max_loop_moderate[] = {2}, max_loop_strict[] = {3};
|
||||
namespace mesh {
|
||||
@CONSTRUCTOR@
|
||||
// The boundary records delegation to the existing OTA/profile transmit veto.
|
||||
class Mesh {
|
||||
public:
|
||||
mutable unsigned base_calls = 0;
|
||||
bool base_allowed = true;
|
||||
virtual ~Mesh() = default;
|
||||
virtual bool allowPacketTransmit(const Packet*) const {
|
||||
++base_calls;
|
||||
return base_allowed;
|
||||
}
|
||||
};
|
||||
}
|
||||
struct SimpleMeshTables {
|
||||
struct RecentRepeaterInfo { int8_t snr_x4 = 12; } recent;
|
||||
bool known = false;
|
||||
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t*, uint8_t) const {
|
||||
return known ? &recent : nullptr;
|
||||
}
|
||||
};
|
||||
struct MyMesh : mesh::Mesh {
|
||||
struct Prefs {
|
||||
uint8_t disable_fwd = 1, trace_when_repeat_off = 0;
|
||||
uint8_t flood_max = 64, flood_max_unscoped = 64, flood_max_advert = 64;
|
||||
uint8_t flood_channel_data_enabled = 1, loop_detect = LOOP_DETECT_OFF;
|
||||
uint8_t direct_retry_enabled = 1, direct_retry_recent_enabled = 0;
|
||||
int8_t direct_retry_snr_margin_x4 = 0;
|
||||
} _prefs;
|
||||
bool recv_pkt_channel_scope_rejected = false, recv_pkt_regionless_scope_set = false;
|
||||
bool recv_pkt_channel_scope_bypass = false, looped = false;
|
||||
const void* recv_pkt_region = this;
|
||||
unsigned recv_pkt_filter_match_mask = 0;
|
||||
bool filter_block = false, moderation_block = false, clock_sync_mesh_edge_enabled = false;
|
||||
unsigned commits = 0, clock_samples = 0;
|
||||
SimpleMeshTables tables;
|
||||
bool floodChannelDataHopApplies(const mesh::Packet*) const { return true; }
|
||||
bool isLooped(const mesh::Packet*, const uint8_t*) { return looped; }
|
||||
bool shouldBlockFloodPacketForward(const mesh::Packet*, unsigned) { return filter_block; }
|
||||
bool shouldBlockFloodGroupTextForward(const mesh::Packet*) { return moderation_block; }
|
||||
void commitFloodPacketFilterRates(const mesh::Packet*, unsigned) { ++commits; }
|
||||
void recordAcceptedFloodClockSample(const mesh::Packet*) { ++clock_samples; }
|
||||
const SimpleMeshTables* getTables() const { return &tables; }
|
||||
int8_t getDirectRetryMinSNRX4() const { return 12; }
|
||||
bool allowPacketForward(const mesh::Packet*);
|
||||
bool allowPacketTransmit(const mesh::Packet*) const override;
|
||||
bool allowDirectRetry(const mesh::Packet*, const uint8_t*, uint8_t) const;
|
||||
};
|
||||
@METHODS@
|
||||
|
||||
int main() {
|
||||
MyMesh node;
|
||||
mesh::Packet p;
|
||||
// Admission is an exact opt-in, not a general forwarding bypass. Test every
|
||||
// route/type and invalid stored values with the actual header accessors.
|
||||
for (uint8_t pref : {uint8_t(0), uint8_t(1), uint8_t(2), uint8_t(255)}) {
|
||||
node._prefs.trace_when_repeat_off = pref;
|
||||
for (uint8_t route = 0; route < 4; ++route) {
|
||||
for (uint8_t type = 0; type < 16; ++type) {
|
||||
p.header = route | (type << PH_TYPE_SHIFT);
|
||||
p.path_len = 0;
|
||||
assert(node.allowPacketForward(&p) ==
|
||||
(pref == 1 && p.isRouteDirect() && type == PAYLOAD_TYPE_TRACE));
|
||||
for (uint16_t path : {uint16_t(0), uint16_t(1), uint16_t(MAX_PATH_SIZE)}) {
|
||||
p.path_len = path;
|
||||
const bool blocked = pref != 1 && p.isRouteDirect()
|
||||
&& type == PAYLOAD_TYPE_TRACE && path > 0;
|
||||
const unsigned before = node.base_calls;
|
||||
assert(node.allowPacketTransmit(&p) == !blocked);
|
||||
assert(node.base_calls == before + (blocked ? 0 : 1));
|
||||
assert(node.allowDirectRetry(&p, nullptr, 0) == !blocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(!node.allowPacketForward(nullptr));
|
||||
assert(node.allowPacketTransmit(nullptr));
|
||||
assert(node.allowDirectRetry(nullptr, nullptr, 0));
|
||||
|
||||
// repeat on restores the original gate; switching it off retires a relay,
|
||||
// but never a locally generated trace whose SNR path starts empty.
|
||||
p.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
|
||||
p.path_len = 1;
|
||||
node._prefs.trace_when_repeat_off = 0;
|
||||
node._prefs.disable_fwd = 0;
|
||||
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
|
||||
node._prefs.disable_fwd = 1;
|
||||
assert(!node.allowPacketForward(&p) && !node.allowPacketTransmit(&p));
|
||||
node._prefs.trace_when_repeat_off = 1;
|
||||
assert(node.allowPacketForward(&p) && node.allowPacketTransmit(&p));
|
||||
node._prefs.trace_when_repeat_off = 0;
|
||||
assert(!node.allowPacketTransmit(&p) && !node.allowDirectRetry(&p, nullptr, 0));
|
||||
p.path_len = 0;
|
||||
assert(node.allowPacketTransmit(&p) && node.allowDirectRetry(&p, nullptr, 0));
|
||||
|
||||
// An enabled opt-in still delegates the base hard-drain/profile safety veto.
|
||||
p.path_len = 1;
|
||||
node._prefs.trace_when_repeat_off = 1;
|
||||
node.base_allowed = false;
|
||||
assert(!node.allowPacketTransmit(&p));
|
||||
node.base_allowed = true;
|
||||
node._prefs.direct_retry_enabled = 0;
|
||||
assert(!node.allowDirectRetry(&p, nullptr, 0));
|
||||
node._prefs.direct_retry_enabled = 1;
|
||||
node._prefs.direct_retry_recent_enabled = 1;
|
||||
const uint8_t hop[] = {0x42};
|
||||
assert(node.allowDirectRetry(&p, hop, 1)); // unknown remains eligible
|
||||
node.tables.known = true;
|
||||
node.tables.recent.snr_x4 = 11;
|
||||
assert(!node.allowDirectRetry(&p, hop, 1));
|
||||
node.tables.recent.snr_x4 = 12;
|
||||
assert(node.allowDirectRetry(&p, hop, 1));
|
||||
node._prefs.direct_retry_snr_margin_x4 = 1;
|
||||
assert(!node.allowDirectRetry(&p, hop, 1));
|
||||
|
||||
// The opt-in never overrides existing flood-hop/region/scope/loop rules,
|
||||
// or spends moderation quota for a packet rejected earlier in the chain.
|
||||
node._prefs.disable_fwd = 0;
|
||||
p.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
|
||||
p.setPathHashSizeAndCount(1, 1);
|
||||
assert(node.allowPacketForward(&p));
|
||||
const unsigned committed = node.commits;
|
||||
node._prefs.flood_max = 0;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node._prefs.flood_max = 64;
|
||||
node.recv_pkt_channel_scope_rejected = true;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node.recv_pkt_channel_scope_rejected = false;
|
||||
node.recv_pkt_region = nullptr;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node.recv_pkt_region = &node;
|
||||
node._prefs.loop_detect = LOOP_DETECT_MINIMAL;
|
||||
node.looped = true;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node.looped = false;
|
||||
#if !defined(PORTABLE_MQTT_OBSERVER)
|
||||
node.filter_block = true;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node.filter_block = false;
|
||||
node.moderation_block = true;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
node.moderation_block = false;
|
||||
assert(node.commits == committed);
|
||||
#else
|
||||
(void)committed;
|
||||
#endif
|
||||
assert(node.allowPacketForward(&p));
|
||||
node._prefs.disable_fwd = 1;
|
||||
assert(!node.allowPacketForward(&p));
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RepeaterTraceForwardingTest(unittest.TestCase):
|
||||
def test_actual_repeater_methods(self):
|
||||
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
||||
if not compiler:
|
||||
self.skipTest('a host C++17 compiler is required')
|
||||
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text(encoding='utf-8')
|
||||
methods = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'bool MyMesh::allowPacketForward(',
|
||||
'bool MyMesh::allowPacketTransmit(',
|
||||
'bool MyMesh::allowDirectRetry('))
|
||||
packet_source = (ROOT / 'src/Packet.cpp').read_text(encoding='utf-8')
|
||||
code = HARNESS.replace('@METHODS@', methods).replace(
|
||||
'@CONSTRUCTOR@', extract_braced(packet_source, 'Packet::Packet()'))
|
||||
flags = ['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []
|
||||
with tempfile.TemporaryDirectory(prefix='repeater-trace-forwarding-') as directory:
|
||||
work = Path(directory)
|
||||
cpp = work / 'test.cpp'
|
||||
cpp.write_text(code, encoding='utf-8')
|
||||
for portable in (False, True):
|
||||
with self.subTest(portable=portable):
|
||||
binary = work / 'test'
|
||||
built = subprocess.run([
|
||||
compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-Wno-unused-parameter',
|
||||
'-DMESH_ENABLE_FLOOD_GROUP_MODERATION=1', '-DMESH_ENABLE_CLOCK_SYNC=1',
|
||||
*(['-DPORTABLE_MQTT_OBSERVER=1'] if portable else []), *flags,
|
||||
'-I' + str(ROOT / 'src'), str(cpp), '-o', str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
tested = subprocess.run([str(binary)], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
|
||||
def test_repeater_only_override(self):
|
||||
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text(encoding='utf-8')
|
||||
self.assertIn('bool allowPacketTransmit(const mesh::Packet* packet) const override;', header)
|
||||
for role in ('simple_room_server', 'simple_sensor', 'companion_radio'):
|
||||
for path in (ROOT / 'examples' / role).glob('MyMesh.*'):
|
||||
self.assertNotIn('trace_when_repeat_off', path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Execute the real repeater trace CLI and appended preference persistence."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_common_usb_debug import HARNESS as COMMON_HARNESS
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
TRACE_MAIN = r'''
|
||||
int main() {
|
||||
CommonCLI cli;
|
||||
char reply[160] = {};
|
||||
assert(cli.prefs.trace_when_repeat_off == 0);
|
||||
cli.prefs.usb_debug_enabled = 1;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto baseline = cli.fs.files["/com_prefs"];
|
||||
assert(baseline.size() == 871);
|
||||
const size_t trace_offset = baseline.size() - 1;
|
||||
const size_t debug_offset = trace_offset - 1;
|
||||
assert(baseline[trace_offset] == 0 && baseline[debug_offset] == 1);
|
||||
Capture capture;
|
||||
assert(writeCommonPrefsImage(capture, &cli.prefs));
|
||||
assert(capture.bytes == baseline);
|
||||
cli.get("repeat.trace", reply);
|
||||
assert(!strcmp(reply, "> off"));
|
||||
cli.set("repeat.trace on", reply);
|
||||
assert(!strcmp(reply, "OK - repeat.trace on (saved)"));
|
||||
assert(cli.prefs.trace_when_repeat_off == 1);
|
||||
const auto enabled = cli.fs.files["/com_prefs"];
|
||||
assert(enabled.size() == baseline.size() && enabled[trace_offset] == 1);
|
||||
assert(std::equal(baseline.begin(), baseline.begin() + trace_offset, enabled.begin()));
|
||||
Capture enabled_capture;
|
||||
assert(writeCommonPrefsImage(enabled_capture, &cli.prefs));
|
||||
assert(enabled_capture.bytes == enabled);
|
||||
cli.get("repeat.trace", reply);
|
||||
assert(!strcmp(reply, "> on"));
|
||||
|
||||
for (const char* command : {"repeat.trace", "repeat.trace ", "repeat.trace\t",
|
||||
"repeat.trace true", "repeat.trace 1", "repeat.trace ON", "repeat.trace Off",
|
||||
"repeat.trace off junk", "repeat.trace off ", "repeat.trace off\t",
|
||||
"repeat.trace on reboot", "repeat.trace off reboot", "repeat.trace on\n"}) {
|
||||
const auto before = cli.fs.files["/com_prefs"];
|
||||
cli.set(command, reply);
|
||||
assert(!strcmp(reply, "Error: usage set repeat.trace on|off"));
|
||||
assert(cli.prefs.trace_when_repeat_off == 1 && cli.fs.files["/com_prefs"] == before);
|
||||
}
|
||||
for (const char* query : {"repeat.trace ", "repeat.trace\t", "repeat.trace on"}) {
|
||||
cli.get(query, reply);
|
||||
assert(!strcmp(reply, "Error: usage get repeat.trace"));
|
||||
}
|
||||
for (const char* collision : {"repeat.traces", "repeat.trace_off",
|
||||
"repeat.tracex on", "repeat.traceback", "repeat", "repeat.trace/on"}) {
|
||||
cli.set(collision, reply);
|
||||
assert(!strcmp(reply, "unknown"));
|
||||
cli.get(collision, reply);
|
||||
assert(!strcmp(reply, "unknown"));
|
||||
assert(cli.prefs.trace_when_repeat_off == 1 && cli.fs.files["/com_prefs"] == enabled);
|
||||
}
|
||||
cli.set("repeat.trace\t \toff", reply);
|
||||
assert(!strcmp(reply, "OK - repeat.trace off (saved)"));
|
||||
assert(cli.prefs.trace_when_repeat_off == 0);
|
||||
|
||||
// Both enable and disable must preserve the saved and requested state when
|
||||
// staging, the new final-byte write, readback or publication fails.
|
||||
for (uint8_t desired : {1, 0}) {
|
||||
cli.set(desired ? "repeat.trace off" : "repeat.trace on", reply);
|
||||
assert(cli.prefs.trace_when_repeat_off == uint8_t(!desired));
|
||||
const auto committed = cli.fs.files["/com_prefs"];
|
||||
for (int fault : {0, 1, 2, 3}) {
|
||||
const unsigned callbacks_before = cli.callbacks.retry_updates;
|
||||
if (fault == 0) cli.fs.fail_write = true;
|
||||
if (fault == 1) cli.fs.fail_write_after = trace_offset;
|
||||
if (fault == 2) cli.fs.fail_read_open = true;
|
||||
if (fault == 3) cli.fs.fail_rename = 1;
|
||||
cli.set(desired ? "repeat.trace on" : "repeat.trace off", reply);
|
||||
assert(!strcmp(reply, "Error: Repeat trace not saved; unchanged"));
|
||||
assert(cli.prefs.trace_when_repeat_off == uint8_t(!desired));
|
||||
assert(cli.fs.files["/com_prefs"] == committed);
|
||||
assert(cli.callbacks.retry_updates == callbacks_before);
|
||||
cli.fs.fail_write = false; cli.fs.fail_write_after = -1;
|
||||
cli.fs.fail_read_open = false; cli.fs.fail_rename = 0;
|
||||
}
|
||||
cli.set(desired ? "repeat.trace on" : "repeat.trace off", reply);
|
||||
assert(cli.prefs.trace_when_repeat_off == desired);
|
||||
Capture saved;
|
||||
assert(writeCommonPrefsImage(saved, &cli.prefs));
|
||||
assert(saved.bytes == cli.fs.files["/com_prefs"]);
|
||||
}
|
||||
|
||||
// Sharing CommonCLI must not expose the setting on other firmware roles.
|
||||
for (const char* role : {"room-server", "sensor", "companion", "observer", "Repeater", ""}) {
|
||||
cli.callbacks.role = role;
|
||||
const auto before = cli.fs.files["/com_prefs"];
|
||||
const uint8_t previous = cli.prefs.trace_when_repeat_off;
|
||||
cli.get("repeat.trace", reply);
|
||||
assert(!strcmp(reply, "Error: repeat.trace is only supported by repeaters"));
|
||||
for (const char* command : {"repeat.trace on", "repeat.trace off"}) {
|
||||
cli.set(command, reply);
|
||||
assert(!strcmp(reply, "Error: repeat.trace is only supported by repeaters"));
|
||||
assert(cli.prefs.trace_when_repeat_off == previous && cli.fs.files["/com_prefs"] == before);
|
||||
}
|
||||
}
|
||||
cli.callbacks.role = "repeater";
|
||||
|
||||
// Execute the production RepeatPrefs dynamic schema and committed writers.
|
||||
RepeatPrefs dynamic(&cli.prefs);
|
||||
for (uint8_t requested : {1, 0, 2, 255}) {
|
||||
assert(dynamic.import("trace_off", requested));
|
||||
assert(cli.prefs.trace_when_repeat_off == requested);
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
assert(cli.prefs.trace_when_repeat_off == (requested == 1 ? 1 : 0));
|
||||
assert(cli.fs.files["/com_prefs"][trace_offset] == (requested == 1 ? 1 : 0));
|
||||
Capture saved;
|
||||
assert(writeCommonPrefsImage(saved, &cli.prefs));
|
||||
assert(saved.bytes == cli.fs.files["/com_prefs"]);
|
||||
}
|
||||
|
||||
// Every old or torn image lacks the complete appended preference and must
|
||||
// reset prior RAM intent to OFF. The old debug byte retains its old offset.
|
||||
auto image = enabled;
|
||||
for (size_t size = 0; size < image.size(); ++size) {
|
||||
CommonCLI reader;
|
||||
reader.prefs.trace_when_repeat_off = 1;
|
||||
reader.fs.files["/com_prefs"] = {image.begin(), image.begin() + size};
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.trace_when_repeat_off == 0);
|
||||
if (size > debug_offset) assert(reader.prefs.usb_debug_enabled == 1);
|
||||
}
|
||||
for (uint8_t saved : {0, 1, 2, 255}) {
|
||||
image[trace_offset] = saved;
|
||||
CommonCLI reader;
|
||||
reader.prefs.trace_when_repeat_off = 1;
|
||||
reader.fs.files["/com_prefs"] = image;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.trace_when_repeat_off == (saved == 1 ? 1 : 0));
|
||||
assert(reader.prefs.usb_debug_enabled == 1);
|
||||
assert(reader.prefs.freq == 910 && reader.prefs.primary_radio_preamble == 48);
|
||||
}
|
||||
cli.prefs.trace_when_repeat_off = 1;
|
||||
cli.loadPrefsInt(&cli.fs, "/missing");
|
||||
assert(cli.prefs.trace_when_repeat_off == 0);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def production_harness(source, header):
|
||||
"""Reuse the common USB fixture while executing real trace/store methods."""
|
||||
fields = header.split('class NodePrefs :', 1)[1].split('private:', 1)[0]
|
||||
fields = '\n'.join(re.findall(
|
||||
r'^\s*(?:float|double|char|u?int(?:8|16|32)_t)\s+[^;]+;', fields, re.M))
|
||||
constants = '\n'.join(re.findall(
|
||||
r'^\s*#define\s+\w+\s+(?:0x[\dA-Fa-f]+|\d+)\s*$', header, re.M))
|
||||
constants += '\n' + '\n'.join(re.findall(
|
||||
r'^static const size_t LEGACY_\w+\s*=[^;]+;', source, re.M))
|
||||
constants += '\n' + re.search(
|
||||
r'^static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY\s*=[^;]+;',
|
||||
header, re.M).group()
|
||||
helpers = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'static bool bwMatches(', 'static bool isValidLoRaBandwidth(',
|
||||
'static float defaultLoRaBandwidth(', 'static void markDirectRetryPrefsValid(',
|
||||
'static void applyFloodRetryPreset(', 'static void applyDirectRetryPreset(',
|
||||
'static void setDefaultDirectRetryPrefs(', 'static bool directRetryPrefsValid('))
|
||||
methods = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'static const char* commonPrefsSaveResultName(',
|
||||
'bool CommonCLI::recoverCommonPrefsFiles(',
|
||||
'bool CommonCLI::saveCommonPrefsImageAtomically(',
|
||||
'void CommonCLI::savePrefs(FILESYSTEM*',
|
||||
'void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope)',
|
||||
'bool CommonCLI::trySavePrefs(', 'void CommonCLI::loadPrefsInt('))
|
||||
setter = extract_braced(source, 'void CommonCLI::handleSetCmd(')
|
||||
getter = extract_braced(source, 'void CommonCLI::handleGetCmd(')
|
||||
branch = 'if (strncmp(config, "repeat.trace", 12)'
|
||||
code = COMMON_HARNESS.split('int main() {', 1)[0].replace(
|
||||
'void updateAdvertTimer() {}',
|
||||
'void updateAdvertTimer() {}\n'
|
||||
' const char* role = "repeater";\n'
|
||||
' const char* getRole() const { return role; }\n'
|
||||
' unsigned retry_updates = 0;\n'
|
||||
' void onRetryConfigChanged() { ++retry_updates; }')
|
||||
replacements = {
|
||||
'FIELDS': fields, 'CONSTANTS': constants, 'HELPERS': helpers,
|
||||
'LEGACY': extract_braced(header, 'struct LegacyObserverTail') + ';',
|
||||
'SERIALIZER': 'template<typename Writer>\n' + extract_braced(
|
||||
source, 'static bool writeCommonPrefsImage('),
|
||||
'STORE': extract_braced(source, 'class CommonPrefsFileStore'),
|
||||
'METHODS': methods,
|
||||
'DYNAMIC': extract_braced(header, 'class RepeatPrefs :'),
|
||||
'SET_DEBUG': extract_braced(setter, branch), 'SET_MASTER': '',
|
||||
'GET_DEBUG': extract_braced(getter, branch), 'GET_MASTER': '',
|
||||
}
|
||||
for key, value in replacements.items():
|
||||
code = code.replace('@' + key + '@', value)
|
||||
return code + TRACE_MAIN
|
||||
|
||||
|
||||
class RepeaterTraceRepeatTest(unittest.TestCase):
|
||||
def test_actual_cli_schema_and_old_new_persistence(self):
|
||||
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
|
||||
header = (ROOT / 'src/helpers/CommonCLI.h').read_text()
|
||||
code = production_harness(source, header)
|
||||
sanitizer_flags = [] if os.name == 'nt' else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie']
|
||||
variants = [('ENABLE_OTA', platform) for platform in (
|
||||
'NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM')]
|
||||
variants += [('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'),
|
||||
('NRF52_PLATFORM',), ('ESP32_PLATFORM', 'WITH_MQTT_BRIDGE')]
|
||||
with tempfile.TemporaryDirectory(prefix='repeater-trace-') as directory:
|
||||
work = Path(directory)
|
||||
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
|
||||
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
|
||||
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
|
||||
(work / 'test.cpp').write_text(code)
|
||||
for macros in variants:
|
||||
with self.subTest(macros=macros):
|
||||
exe = work / ('test.exe' if os.name == 'nt' else 'test')
|
||||
built = subprocess.run([
|
||||
os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
*['-D' + macro + '=1' for macro in macros], *sanitizer_flags,
|
||||
'-I' + str(work), '-I' + str(ROOT / 'test/fixtures/radio_profiles/mocks'),
|
||||
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'src/helpers'),
|
||||
str(work / 'test.cpp'), '-o', str(exe)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
tested = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(tested.returncode, 0, tested.stderr)
|
||||
|
||||
def test_outer_load_defaults_before_loading_saved_image(self):
|
||||
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
|
||||
load = extract_braced(source, 'void CommonCLI::loadPrefs(FILESYSTEM*')
|
||||
self.assertIn('_prefs->trace_when_repeat_off = 0;', load)
|
||||
self.assertLess(load.index('_prefs->trace_when_repeat_off = 0;'),
|
||||
load.index('loadPrefsInt('))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -836,6 +836,160 @@ TEST(MeshReceiveHooks, FloodTraceAndControlAreNeverForwarded) {
|
||||
}
|
||||
}
|
||||
|
||||
// Admission/egress boundary controls deliberately do not reproduce the role
|
||||
// predicate. The Python extraction suite executes the actual repeater methods;
|
||||
// these tests exercise the real Mesh/Dispatcher validation and queue ownership.
|
||||
class TraceForwardingPolicyMesh : public TraceTestMesh {
|
||||
public:
|
||||
using TraceTestMesh::TraceTestMesh;
|
||||
const mesh::Packet* denied = nullptr;
|
||||
unsigned forwarding_checks = 0;
|
||||
bool allowPacketForward(const mesh::Packet*) override {
|
||||
++forwarding_checks;
|
||||
return forwardFloods;
|
||||
}
|
||||
bool allowPacketTransmit(const mesh::Packet* packet) const override {
|
||||
return packet != denied && mesh::Mesh::allowPacketTransmit(packet);
|
||||
}
|
||||
};
|
||||
|
||||
static mesh::Packet makeIncomingTrace(const TraceTestMesh& node, uint8_t route,
|
||||
uint8_t flags = 0, uint8_t hash_size = 1) {
|
||||
mesh::Packet packet;
|
||||
packet.header = route | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
|
||||
packet.path_len = 0;
|
||||
packet.payload_len = 9 + 2 * hash_size;
|
||||
memset(packet.payload, 0, packet.payload_len);
|
||||
packet.payload[8] = flags;
|
||||
memcpy(packet.payload + 9, node.self_id.pub_key, hash_size);
|
||||
memset(packet.payload + 9 + hash_size, 0x98, hash_size);
|
||||
packet._snr = 17;
|
||||
return packet;
|
||||
}
|
||||
|
||||
TEST(TraceForwardingSafety, MatchingDirectHashesAppendOneSnrAndDuplicateIsRejected) {
|
||||
for (uint8_t route : {ROUTE_TYPE_DIRECT, ROUTE_TYPE_TRANSPORT_DIRECT}) {
|
||||
for (uint8_t code = 0; code < 4; ++code) {
|
||||
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
|
||||
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
|
||||
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
node.forwardFloods = true;
|
||||
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
|
||||
mesh::Packet packet = makeIncomingTrace(node, route, code, 1U << code);
|
||||
mesh::Packet duplicate = packet;
|
||||
EXPECT_NE(ACTION_RELEASE, node.receivePacket(&packet));
|
||||
EXPECT_EQ(1, packet.path_len);
|
||||
EXPECT_EQ(17, packet.path[0]);
|
||||
EXPECT_EQ(1, tables.mark_seen_calls);
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&duplicate));
|
||||
EXPECT_EQ(0, duplicate.path_len);
|
||||
EXPECT_EQ(1, tables.mark_seen_calls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(TraceForwardingSafety, MalformedWrongHashAndFullSnrPathNeverReachForwardingGate) {
|
||||
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
|
||||
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
|
||||
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
node.forwardFloods = true;
|
||||
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
|
||||
const mesh::Packet valid = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
|
||||
for (uint8_t length = 0; length < 9; ++length) {
|
||||
mesh::Packet packet = valid;
|
||||
packet.payload_len = length;
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet));
|
||||
}
|
||||
mesh::Packet wrong = valid;
|
||||
wrong.payload[9] ^= 0xff;
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&wrong));
|
||||
mesh::Packet truncated = valid;
|
||||
truncated.payload[8] = 3;
|
||||
truncated.payload_len = 10; // neither an eight-byte nor four-byte hash fits
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&truncated));
|
||||
mesh::Packet full = valid;
|
||||
full.path_len = MAX_PATH_SIZE;
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&full));
|
||||
mesh::Packet long_offset = valid;
|
||||
long_offset.path_len = 33;
|
||||
long_offset.payload[8] = 3;
|
||||
long_offset.payload_len = 9 + 160;
|
||||
memset(long_offset.payload + 9, 0x42, 160);
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&long_offset)); // 33*8 must not wrap
|
||||
EXPECT_EQ(0u, node.forwarding_checks);
|
||||
EXPECT_EQ(0, tables.mark_seen_calls);
|
||||
}
|
||||
|
||||
TEST(TraceForwardingSafety, FinalSnrSlotIsSafeAndDeniedAdmissionDoesNotMarkSeen) {
|
||||
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
|
||||
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
|
||||
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
|
||||
mesh::Packet blocked = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&blocked));
|
||||
EXPECT_EQ(0, blocked.path_len);
|
||||
EXPECT_EQ(0, tables.mark_seen_calls);
|
||||
node.forwardFloods = true;
|
||||
mesh::Packet last = blocked;
|
||||
last.path_len = MAX_PATH_SIZE - 1;
|
||||
last.payload_len = 9 + MAX_PATH_SIZE;
|
||||
memset(last.payload + 9, 0x42, MAX_PATH_SIZE);
|
||||
EXPECT_NE(ACTION_RELEASE, node.receivePacket(&last));
|
||||
EXPECT_EQ(MAX_PATH_SIZE, last.path_len);
|
||||
EXPECT_EQ(17, last.path[MAX_PATH_SIZE - 1]);
|
||||
EXPECT_EQ(1, tables.mark_seen_calls);
|
||||
}
|
||||
|
||||
TEST(TraceForwardingSafety, EgressVetoRetiresQueuedRelayAndItsRetryOwner) {
|
||||
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
|
||||
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
|
||||
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
node.begin();
|
||||
node.forwardFloods = true;
|
||||
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
|
||||
auto* relay = manager.allocNew(); ASSERT_NE(nullptr, relay);
|
||||
*relay = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
|
||||
ASSERT_NE(ACTION_RELEASE, node.receivePacket(relay));
|
||||
ASSERT_TRUE(node.sendPacket(relay, 1));
|
||||
node.denied = relay;
|
||||
clock.now = 1000; node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
node.denied = nullptr;
|
||||
clock.now = 10000; node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
EXPECT_EQ(12, manager.getFreeCount());
|
||||
}
|
||||
|
||||
TEST(TraceForwardingSafety, AirtimeAlreadyStartedFinishesButQueuedRetryIsRetired) {
|
||||
TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio;
|
||||
ForwardingTestTables tables; StaticPoolPacketManager manager(12);
|
||||
TraceForwardingPolicyMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
node.begin();
|
||||
node.forwardFloods = true;
|
||||
memset(node.self_id.pub_key, 0x42, PUB_KEY_SIZE);
|
||||
auto* relay = manager.allocNew(); ASSERT_NE(nullptr, relay);
|
||||
*relay = makeIncomingTrace(node, ROUTE_TYPE_DIRECT);
|
||||
ASSERT_NE(ACTION_RELEASE, node.receivePacket(relay));
|
||||
ASSERT_TRUE(node.sendPacket(relay, 1));
|
||||
clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending);
|
||||
node.denied = relay;
|
||||
clock.now = 2; node.loop(); EXPECT_TRUE(radio.sending);
|
||||
radio.complete = true;
|
||||
clock.now = 3; node.loop(); EXPECT_FALSE(radio.sending);
|
||||
ASSERT_EQ(1, manager.getOutboundTotal());
|
||||
node.denied = manager.getOutboundByIdx(0);
|
||||
clock.now = 1000; node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
node.denied = nullptr;
|
||||
clock.now = 10000; node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
EXPECT_EQ(12, manager.getFreeCount());
|
||||
}
|
||||
|
||||
static mesh::Packet* makeTrace(TraceTestMesh& node, uint32_t tag, uint32_t auth,
|
||||
const uint8_t* route, uint8_t route_len) {
|
||||
mesh::Packet* packet = node.createTrace(tag, auth, 0);
|
||||
|
||||
Reference in new issue
Block a user