Add saved node and region discovery response switches

This commit is contained in:
mikecarper committed 2026-10-24 20:37:51 -07:00
1 parent 1ab4091a05
commit 4fd3358a45
15 files changed
+705 -5

No files matched your search

+84
View File
@@ -8,6 +8,7 @@
#include <helpers/AdvertDataHelpers.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/DatagramPayloadLimits.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RoutingPolicy.h>
#include "examples/simple_repeater/RateLimiter.h"
#include "production_constants.inc"
@@ -34,6 +35,7 @@ struct MyMesh {
char node_name[32] = "Bellevue";
char owner_info[80] = "owner";
bool disable_fwd = false;
uint8_t discovery_responses = 3;
} _prefs;
struct Clock {
uint32_t now = 123456;
@@ -164,6 +166,87 @@ static void checkAnonymousHandlers() {
}
}
static void checkDiscoveryResponseSwitches() {
const mesh::Identity sender;
const uint8_t secret[PUB_KEY_SIZE] = {};
mesh::Packet request;
request.header = (PAYLOAD_TYPE_ANON_REQ << PH_TYPE_SHIFT) | ROUTE_TYPE_DIRECT;
const uint8_t path[] = {0x42, 0x11, 0x22, 0x33, 0x44};
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
MyMesh mesh;
mesh._prefs.discovery_responses = node | (region << 1);
mesh.reply_path_len = 0x02;
memset(mesh.reply_data, 0xc7, sizeof(mesh.reply_data));
const auto limiter_before = mesh.anon_limiter;
const uint8_t length = mesh.handleAnonRegionsReq(sender, 42, path, sizeof(path));
require((length != 0) == (region != 0),
"node and region discovery choices are not independent");
if (!region) {
require(mesh.reply_path_len == 0x02, "disabled region request changed reply route");
for (uint8_t byte : mesh.reply_path)
require(byte == 0xa5, "disabled region request changed reply path bytes");
for (uint8_t byte : mesh.reply_data)
require(byte == 0xc7, "disabled region request changed reply payload");
require(mesh.region_map.exports == 0, "disabled region request exported names");
// Compare future decisions from the real limiter, instead of relying
// on the private counter representation or an access-order mock.
auto expected = limiter_before;
for (unsigned count = 0; count < 10001; ++count) {
require(mesh.anon_limiter.allow(mesh.rtc_clock.now)
== expected.allow(mesh.rtc_clock.now),
"disabled region request consumed anonymous quota");
}
} else {
require(mesh.reply_path_len == path[0] && mesh.region_map.exports == 1,
"enabled region response lost the requested path or names");
}
// Exercise the real receive dispatcher too: no allocation or RF send
// may happen for a disabled query, including an already-known caller.
MyMesh dispatched;
dispatched._prefs.discovery_responses = node | (region << 1);
dispatched.acl.present = true;
dispatched.acl.client.out_path_len = 0;
dispatched.reply_path_len = 0x02;
memset(dispatched.reply_data, 0xc7, sizeof(dispatched.reply_data));
uint8_t bytes[11] = {42, 0, 0, 0, ANON_REQ_TYPE_REGIONS,
0x42, 0x11, 0x22, 0x33, 0x44, 0};
dispatched.onAnonDataRecv(&request, secret, sender, bytes, 10);
require(dispatched.allocations == unsigned(region)
&& dispatched.direct_sends == unsigned(region)
&& dispatched.flood_sends == 0 && dispatched.logins == 0,
"disabled region query generated TX or fell through to login");
if (!region) {
require(dispatched.reply_path_len == 0x02,
"disabled dispatched region query changed pending reply route");
for (uint8_t byte : dispatched.reply_path)
require(byte == 0xa5, "disabled dispatched query changed reply path bytes");
for (uint8_t byte : dispatched.reply_data)
require(byte == 0xc7, "disabled dispatched query changed reply payload");
}
// The switches apply only to public discovery. Both anonymous metadata
// services and login remain reachable with all four combinations.
for (unsigned type : {ANON_REQ_TYPE_OWNER, ANON_REQ_TYPE_BASIC}) {
MyMesh metadata;
metadata._prefs.discovery_responses = node | (region << 1);
bytes[4] = type;
metadata.onAnonDataRecv(&request, secret, sender, bytes, 10);
require(metadata.allocations == 1 && metadata.direct_sends == 1
&& metadata.logins == 0,
"discovery switches disabled owner or clock metadata");
}
MyMesh login;
login._prefs.discovery_responses = node | (region << 1);
uint8_t credentials[] = {42, 0, 0, 0, 'x', 0};
login.onAnonDataRecv(&request, secret, sender, credentials, 5);
require(login.logins == 1 && login.allocations == 1,
"discovery switches disabled administrator login");
}
}
}
static void checkAnonymousDispatch() {
const mesh::Identity sender;
const uint8_t secret[PUB_KEY_SIZE] = {};
@@ -302,5 +385,6 @@ int main() {
checkAdverts();
checkAnonymousHandlers();
checkAnonymousDispatch();
checkDiscoveryResponseSwitches();
std::puts("PASS: production protocol parser bounds");
}
+2
View File
@@ -19,6 +19,7 @@ HARNESS = r'''
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/AtomicFileWriter.h>
#include <helpers/PrefsSaveRouting.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/GpsPowerPolicy.h>
#include <helpers/radiolib/RXPowerSaving.h>
#if defined(ESP32_PLATFORM)
@@ -76,6 +77,7 @@ struct CommonCLI {
prefs.freq=910; prefs.bw=62.5f; prefs.sf=7; prefs.cr=5;
prefs.tx_power_dbm=30; prefs.primary_radio_preamble=48;
prefs.espnow_bridge_enabled=1;
prefs.discovery_responses=3;
prefs.gps_sync_interval_hours=336;
prefs.rx_ps_rx_us=111; prefs.rx_ps_sleep_us=222;
}
+1
View File
@@ -35,6 +35,7 @@ class TestFS : public MemoryFS {
#include <helpers/HilStartupTrace.h>
#include <helpers/MQTTPrefsAtomicStore.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/RepeaterRadioTiming.h>
#include <helpers/GpsPowerPolicy.h>
#if defined(ESP32_PLATFORM)
+2
View File
@@ -248,6 +248,7 @@ inline char* utoa(unsigned value,char* out,int base){
#include <vector>
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/bridges/RS232PowerSettings.h>
#include <helpers/DiscoveryResponsePrefs.h>
@CONSTANTS@
struct NodePrefs { @FIELDS@ };
@MARKER@
@@ -269,6 +270,7 @@ void non_mqtt(File& file,NodePrefs* _prefs){ @SCALAR@ }
int main(){
using namespace mesh::esp32::cpuclock;
NodePrefs prefs{};prefs.freq=910.525f;prefs.bw=62.5f;prefs.sf=7;prefs.cr=5;
prefs.discovery_responses=3;
prefs.powersaving_enabled=1;strcpy(prefs.node_name,"unchanged prefix");
File legacy;assert(writeCommonPrefsImage(legacy,&prefs));
File nonlegacy;non_mqtt(nonlegacy,&prefs);assert(nonlegacy.bytes==legacy.bytes);
+66 -1
View File
@@ -24,8 +24,14 @@ HARNESS = r'''
#include <initializer_list>
#include "examples/simple_repeater/RateLimiter.h"
#include "src/RadioProfiles.h"
#include "src/helpers/DiscoveryResponsePrefs.h"
@CONSTANTS@
#define MESH_DEBUG_PRINTLN(...) ((void)0)
struct CountedLimiter : RateLimiter {
using RateLimiter::RateLimiter;
unsigned calls = 0;
bool allow(uint32_t now) { ++calls; return RateLimiter::allow(now); }
};
static uint32_t now_ms = 100;
bool millisHasNowPassed(uint32_t deadline) {
return static_cast<int32_t>(now_ms - deadline) > 0;
@@ -71,13 +77,14 @@ struct MyMesh {
struct {
uint16_t advert_interval = 30, flood_advert_interval = 0;
bool disable_fwd = false;
uint8_t discovery_responses = 3;
uint32_t discovery_mod_timestamp = 500;
} _prefs;
struct Clock {
uint32_t now = 1000;
uint32_t getCurrentTime() const { return now; }
} rtc_clock;
RateLimiter @LIMITER@;
CountedLimiter @LIMITER@;
mesh::Radio radio;
mesh::Radio* _radio = &radio;
mesh::Identity self_id, neighbour;
@@ -206,6 +213,53 @@ static void gates() {
unrelated.onControlDataRecv(&packet);
require(unrelated.create_attempts == 0, "unrelated control type produced a reply");
}
static void response_switches() {
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
for (unsigned flags = 0; flags < 4; ++flags) {
MyMesh receiver;
receiver._prefs.discovery_responses = node | (region << 1);
receiver.response.payload_len = 17;
receiver.response.tx_radio = mesh::RADIO_TX_BOTH;
std::memset(receiver.response.payload, 0xa5, sizeof(receiver.response.payload));
auto packet = request(flags & 1, 0, false, flags & 2);
for (unsigned count = 0; count < 8; ++count) {
receiver.onControlDataRecv(&packet);
}
if (node) {
require(receiver.sent_packets == 4 && receiver.create_attempts == 4,
"region response choice affected normal node discovery");
} else {
require(receiver.discover_limiter.calls == 0,
"disabled node discovery consumed rate-limit quota");
require(receiver.sent_packets == 0 && receiver.create_attempts == 0,
"disabled node discovery allocated or transmitted a reply");
require(receiver.response.payload_len == 17
&& receiver.response.tx_radio == mesh::RADIO_TX_BOTH,
"disabled node discovery changed pending reply metadata");
for (uint8_t byte : receiver.response.payload) {
require(byte == 0xa5, "disabled node discovery changed reply bytes");
}
// Turning the live switch back on must admit the complete original
// quota: rejected requests did not secretly exhaust the limiter.
receiver._prefs.discovery_responses |= 1;
for (unsigned count = 1; count <= 5; ++count) {
receiver.onControlDataRecv(&packet);
require(receiver.sent_packets == (count < 5 ? count : 4),
"disabled requests exhausted the later enabled quota");
}
}
// Enabling responses must not override the legacy hidden-node rule.
MyMesh hidden;
hidden._prefs.discovery_responses = node | (region << 1);
hidden._prefs.advert_interval = hidden._prefs.flood_advert_interval = 0;
hidden.onControlDataRecv(&packet);
require(hidden.create_attempts == 0 && hidden.discover_limiter.calls == 0,
"response setting bypassed hidden-node protection");
}
}
}
}
static void wire() {
for (bool prefix : {false, true}) {
MyMesh receiver;
@@ -333,7 +387,10 @@ static void radio2_disabled() {
}
}
static void received() {
for (uint8_t node : {0, 1}) {
for (uint8_t region : {0, 1}) {
MyMesh receiver;
receiver._prefs.discovery_responses = node | (region << 1);
receiver._prefs.advert_interval = 0;
receiver._prefs.flood_advert_interval = 0;
receiver.pending_discover_tag = 0x78563412;
@@ -363,11 +420,15 @@ static void received() {
receiver.onControlDataRecv(&packet);
require(receiver.neighbours == 1 && receiver.pending_discover_tag == 0,
"expired discovery response retained its pending request");
now_ms = 100;
}
}
}
int main(int argc, char** argv) {
if (argc != 2) return 2;
if (std::strcmp(argv[1], "policy") == 0) policy();
else if (std::strcmp(argv[1], "gates") == 0) gates();
else if (std::strcmp(argv[1], "response_switches") == 0) response_switches();
else if (std::strcmp(argv[1], "wire") == 0) wire();
else if (std::strcmp(argv[1], "radio2_wire") == 0) radio2_wire();
else if (std::strcmp(argv[1], "radio2_disabled") == 0) radio2_disabled();
@@ -442,6 +503,10 @@ class RepeaterDiscoveryTests(unittest.TestCase):
checked = self.run_case("wire")
self.assertEqual(checked.returncode, 0, checked.stderr)
def test_saved_response_switches_are_independent_and_reject_before_quota(self):
checked = self.run_case("response_switches")
self.assertEqual(checked.returncode, 0, checked.stderr)
def test_hidden_nodes_still_process_matching_discovery_responses(self):
checked = self.run_case("received")
self.assertEqual(checked.returncode, 0, checked.stderr)
+355
View File
@@ -0,0 +1,355 @@
"""Execute saved repeater discovery controls with real CLI and prefs images."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_repeater_trace_repeat import production_harness
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
MAIN = r'''
struct MyMesh {
CommonCLI _cli;
NodePrefs& _prefs = _cli.prefs;
bool handleDiscoveryResponseCommand(const char*, char*, size_t);
};
@HANDLER@
static void run(MyMesh& mesh, const char* command, const char* expected) {
char reply[160] = {};
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, expected) == 0);
}
static std::vector<uint8_t> image(CommonCLI& cli) {
Capture capture;
assert(writeCommonPrefsImage(capture, &cli.prefs));
return capture.bytes;
}
static void assertLoaded(const std::vector<uint8_t>& bytes, uint8_t expected) {
CommonCLI reader;
reader.prefs.discovery_responses = uint8_t(expected ^ 3);
reader.fs.files["/com_prefs"] = bytes;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.discovery_responses == expected);
assert(reader.prefs.freq == 910 && reader.prefs.primary_radio_preamble == 48);
}
static void cliChoices() {
for (uint8_t initial : {0, 1, 2, 3}) {
for (uint8_t bit : {1, 2}) {
for (bool enabled : {false, true}) {
MyMesh mesh;
mesh._prefs.discovery_responses = initial;
const char* name = bit == 1 ? "discovery.node.responses" : "discovery.region.responses";
char command[80], expected[100];
snprintf(command, sizeof(command), "get %s", name);
run(mesh, command, (initial & bit) ? "> on" : "> off");
snprintf(command, sizeof(command), "set %s %s", name, enabled ? "on" : "off");
snprintf(expected, sizeof(expected), "OK - %s %s", name, enabled ? "on" : "off");
run(mesh, command, expected);
const uint8_t mask = enabled ? uint8_t(initial | bit) : uint8_t(initial & ~bit);
assert(mesh._prefs.discovery_responses == mask);
assert((mesh._prefs.discovery_responses & (bit ^ 3)) == (initial & (bit ^ 3)));
assert(mesh._cli._common_save_succeeded);
assert(mesh._cli.fs.files["/com_prefs"] == image(mesh._cli));
assertLoaded(mesh._cli.fs.files["/com_prefs"], mask);
snprintf(command, sizeof(command), "get %s", name);
run(mesh, command, enabled ? "> on" : "> off");
}
}
}
MyMesh mesh;
assert(mesh._prefs.discovery_responses == 3);
run(mesh, "set discovery.node.responses\t \toff", "OK - discovery.node.responses off");
run(mesh, "set discovery.region.responses off", "OK - discovery.region.responses off");
assert(mesh._prefs.discovery_responses == 0);
}
static void invalidCommands() {
for (const char* name : {"discovery.node.responses", "discovery.region.responses"}) {
for (const char* value : {"", " ", "true", "false", "1", "0", "ON", "Off",
"on reboot", "off reboot", "on junk", "off ", "on\t", "on\n", "on;off"}) {
MyMesh mesh;
mesh._prefs.discovery_responses = 1;
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
const auto before = mesh._cli.fs.files["/com_prefs"];
char command[100], reply[160] = {};
snprintf(command, sizeof(command), "set %s %s", name, value);
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strncmp(reply, "Err - use ", 10) == 0);
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
snprintf(command, sizeof(command), "get %s on", name);
assert(mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strncmp(reply, "Err - use ", 10) == 0);
assert(mesh._prefs.discovery_responses == 1 && mesh._cli.fs.files["/com_prefs"] == before);
}
}
for (const char* command : {"get discovery.node.responsesx", "set discovery.region.responses_extra off",
"get discovery.node.response", "set discovery.region.responses/off", "discovery.node.responses on"}) {
MyMesh mesh;
const auto before = mesh._cli.fs.files;
char reply[160] = "unchanged";
assert(!mesh.handleDiscoveryResponseCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0 && mesh._prefs.discovery_responses == 3);
assert(mesh._cli.fs.files == before);
}
}
static void failedSaves() {
for (uint8_t initial : {0, 1, 2, 3}) {
for (uint8_t bit : {1, 2}) {
for (unsigned fault : {0, 1, 2, 3}) {
MyMesh mesh;
mesh._prefs.discovery_responses = initial;
mesh._cli.savePrefs(&mesh._cli.fs, PrefsSaveRouting::Scope::Common);
assert(mesh._cli._common_save_succeeded);
const auto before = mesh._cli.fs.files["/com_prefs"];
if (fault == 0) mesh._cli.fs.fail_write = true;
if (fault == 1) {
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
#ifdef WITH_RS232_BRIDGE
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
#endif
;
mesh._cli.fs.fail_write_after = (initial ^ bit) == 3 ? offset - 1 : offset + 4;
}
if (fault == 2) mesh._cli.fs.fail_read_open = true;
if (fault == 3) mesh._cli.fs.fail_rename = 1;
char command[100];
snprintf(command, sizeof(command), "set discovery.%s.responses %s",
bit == 1 ? "node" : "region", (initial & bit) ? "off" : "on");
run(mesh, command, "Err - save unconfirmed; reboot may differ");
assert(mesh._prefs.discovery_responses == initial);
assert(mesh._cli.fs.files["/com_prefs"] == before);
}
}
}
}
static void persistence() {
CommonCLI cli;
const auto baseline = image(cli);
const size_t offset = mesh::discovery::COMMON_LEGACY_SIZE
#ifdef WITH_RS232_BRIDGE
+ mesh::discovery::LEGACY_RS232_TAIL_SIZE
#endif
;
assert(cli.prefs.discovery_responses == 3);
for (uint8_t mask : {0, 1, 2, 3}) {
cli.prefs.discovery_responses = mask;
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto saved = image(cli);
assert(cli.fs.files["/com_prefs"] == saved);
assert(saved.size() == baseline.size() + (mask == 3 ? 0 : mesh::discovery::PREFS_SIZE));
assert(std::equal(saved.begin(), saved.begin() + offset, baseline.begin()));
assertLoaded(saved, mask);
if (mask == 3) continue;
uint8_t decoded = 3;
assert(mesh::discovery::decodePrefs(saved.data() + offset, mesh::discovery::PREFS_SIZE, decoded));
assert(decoded == mask);
// All historical prefixes and partial optional records keep replies ON,
// even when a previous load into the same object had disabled them.
for (size_t length = 0; length < offset + mesh::discovery::PREFS_SIZE; ++length) {
CommonCLI reader;
reader.prefs.discovery_responses = 0;
reader.fs.files["/com_prefs"] = {saved.begin(), saved.begin() + length};
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.discovery_responses == 3);
}
for (size_t byte = 0; byte < mesh::discovery::PREFS_SIZE; ++byte) {
auto damaged = saved;
damaged[offset + byte] ^= 0x80;
assertLoaded(damaged, 3);
}
}
CommonCLI missing;
missing.prefs.discovery_responses = 0;
missing.loadPrefsInt(&missing.fs, "/missing");
assert(missing.prefs.discovery_responses == 3);
// A complete, checksummed record with a forbidden mask must still reject.
auto invalid = baseline;
uint8_t record[mesh::discovery::PREFS_SIZE];
mesh::discovery::encodePrefs(record, 0);
record[4] = 4;
const uint16_t crc = mesh::discovery::checksum(record, 6);
record[6] = uint8_t(crc >> 8); record[7] = uint8_t(crc);
invalid.insert(invalid.begin() + offset, record, record + sizeof(record));
assertLoaded(invalid, 3);
#if defined(ESP32_PLATFORM)
// The optional discovery record must not displace the existing CPU footer.
cli.prefs.discovery_responses = 0;
cli.prefs.cpu_freq_mhz = 80;
auto combined = image(cli);
CommonCLI cpu;
cpu.fs.files["/com_prefs"] = combined;
cpu.loadPrefsInt(&cpu.fs, "/com_prefs");
assert(cpu.prefs.discovery_responses == 0 && cpu.prefs.cpu_freq_mhz == 80);
#endif
#if defined(RAK_4631)
cli.prefs.discovery_responses = 1;
cli.prefs.peripherals_shared = 1;
auto combined = image(cli);
CommonCLI peripheral;
peripheral.fs.files["/com_prefs"] = combined;
peripheral.loadPrefsInt(&peripheral.fs, "/com_prefs");
assert(peripheral.prefs.discovery_responses == 1 && peripheral.prefs.peripherals_shared == 1);
#endif
#ifndef WITH_RS232_BRIDGE
// Loading an old UART profile on a non-UART image must still find its saved
// discovery record after the source image's checked ten-byte power tail.
CommonCLI plain;
auto uart = image(plain);
uint8_t power[mesh::bridge::rs232power::PREFS_SIZE];
mesh::bridge::rs232power::encodePrefs(power, 1, 1, 300);
mesh::discovery::encodePrefs(record, 2);
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE, power, power + sizeof(power));
uart.insert(uart.begin() + mesh::discovery::COMMON_LEGACY_SIZE + sizeof(power),
record, record + sizeof(record));
assertLoaded(uart, 2);
#endif
}
int main() {
cliChoices();
invalidCommands();
failedSaves();
persistence();
}
'''
OUTBOUND = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <helpers/DiscoveryResponsePrefs.h>
#define PUB_KEY_SIZE 32
#define PAYLOAD_TYPE_ANON_REQ 7
namespace mesh {
struct Identity {
void calcSharedSecret(uint8_t* out, const Identity&) { memset(out, 0xa5, PUB_KEY_SIZE); }
};
struct Packet { uint8_t payload[16] = {}; size_t length = 0; };
}
struct MyMesh {
static constexpr unsigned NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = @MIN_FREE@;
struct { uint8_t discovery_responses = 3; } _prefs;
struct Manager { unsigned free = NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
unsigned getFreeCount() { return free; }
} manager;
Manager* _mgr = &manager;
struct Clock { uint32_t unique = 1000; uint32_t getCurrentTimeUnique() { return ++unique; } } clock;
Clock* getRTCClock() { return &clock; }
mesh::Identity self_id;
mesh::Packet packet;
bool allocation_ok = true;
unsigned allocations = 0, sends = 0;
mesh::Packet* createAnonDatagram(unsigned type, const mesh::Identity&,
const mesh::Identity&, const uint8_t*, const uint8_t* bytes, size_t length) {
++allocations;
assert(type == PAYLOAD_TYPE_ANON_REQ && length == 6);
if (!allocation_ok) return nullptr;
memcpy(packet.payload, bytes, length); packet.length = length;
return &packet;
}
void sendDirect(mesh::Packet* sent, const uint8_t* path, uint8_t length, uint32_t delay) {
assert(sent == &packet && path == nullptr && length == 0 && delay == 0); ++sends;
}
mesh::Packet* sendAnonRegionsReq(const mesh::Identity&, uint32_t&);
};
@CONSTANTS@
@METHOD@
int main() {
const mesh::Identity target;
for (uint8_t mask : {0, 1, 2, 3}) {
MyMesh mesh;
mesh._prefs.discovery_responses = mask;
uint32_t tag = 99;
assert(mesh.sendAnonRegionsReq(target, tag) == &mesh.packet);
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
uint32_t sent_tag = 0; memcpy(&sent_tag, mesh.packet.payload, sizeof(sent_tag));
assert(sent_tag == tag && mesh.packet.payload[4] == ANON_REQ_TYPE_REGIONS
&& mesh.packet.payload[5] == 0);
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS - 1;
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
assert(mesh.allocations == 1 && mesh.sends == 1 && tag == 1001);
mesh.manager.free = MyMesh::NEIGHBOR_DISCOVER_MIN_FREE_PACKETS;
mesh.allocation_ok = false;
assert(mesh.sendAnonRegionsReq(target, tag) == nullptr);
assert(mesh.allocations == 2 && mesh.sends == 1);
}
}
'''
class RepeaterDiscoverySettingsTests(unittest.TestCase):
def test_response_choices_do_not_disable_outbound_region_queries(self):
import re
source = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
header = (ROOT / 'examples/simple_repeater/MyMesh.h').read_text()
minimum = re.search(r'NEIGHBOR_DISCOVER_MIN_FREE_PACKETS\s*=\s*(\d+)', header).group(1)
constant = re.search(r'^#define ANON_REQ_TYPE_REGIONS\s+[^\n]+', source, re.M).group()
code = (OUTBOUND.replace('@MIN_FREE@', minimum).replace('@CONSTANTS@', constant)
.replace('@METHOD@', extract_braced(source, 'mesh::Packet* MyMesh::sendAnonRegionsReq(')))
with tempfile.TemporaryDirectory(prefix='repeater-discovery-outbound-') as directory:
work = Path(directory)
(work / 'test.cpp').write_text(code)
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
'-Wall', '-Wextra', '-Werror', '-I' + str(ROOT / 'src'),
str(work / 'test.cpp'), '-o', str(work / 'test')],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(work / 'test')], capture_output=True, text=True, timeout=10)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_real_commands_saved_images_failures_and_legacy_defaults(self):
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
header = (ROOT / 'src/helpers/CommonCLI.h').read_text()
repeater = (ROOT / 'examples/simple_repeater/MyMesh.cpp').read_text()
code = production_harness(source, header).split('int main() {', 1)[0]
code = code.replace(' bool trySavePrefs(', ' bool saveCommonPrefs();\n bool trySavePrefs(', 1)
code = ('#include <algorithm>\n#include <helpers/PeripheralModePrefs.h>\n'
'#include <helpers/bridges/RS232PowerSettings.h>\n' + code)
code += extract_braced(source, 'bool CommonCLI::saveCommonPrefs(') + '\n'
code += MAIN.replace('@HANDLER@', extract_braced(
repeater, 'bool MyMesh::handleDiscoveryResponseCommand('))
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 += [('NRF52_PLATFORM', 'RAK_4631'),
('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'),
('NRF52_PLATFORM', 'WITH_RS232_BRIDGE', 'WITH_RS232_BRIDGE_UART=2')]
with tempfile.TemporaryDirectory(prefix='repeater-discovery-settings-') 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):
binary = work / ('test.exe' if os.name == 'nt' else 'test')
built = subprocess.run([
os.environ.get('CXX', 'g++'), '-std=c++17',
*['-D' + macro if '=' in macro else '-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(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
if __name__ == '__main__':
unittest.main()
+7 -2
View File
@@ -22,6 +22,7 @@ HARNESS = r'''
#include <string>
#include <vector>
#include <helpers/GuestLoginSettings.h>
#include <helpers/DiscoveryResponsePrefs.h>
#include <helpers/ClientLoginPersistence.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/ClientPathPersistence.h>
@@ -119,7 +120,11 @@ struct MyMesh {
ClientACL acl;
Clock clock;
RNG rng;
struct Prefs { char password[16] = "admin"; char guest_password[16] = ""; } _prefs;
struct Prefs {
char password[16] = "admin";
char guest_password[16] = "";
uint8_t discovery_responses = 3;
} _prefs;
unsigned long dirty_contacts_expiry = 0;
uint8_t contacts_save_failures = 0, reply_data[160]{};
int matching_peer_indexes[MAX_CLIENTS]{};
@@ -573,7 +578,7 @@ class RepeaterGuestAccessTests(unittest.TestCase):
"static bool isFilterMgrAllowed(",
))
dispatch = extract_braced(source, "void MyMesh::handleCommand(uint32_t sender_timestamp,")
call = dispatch.index("if (handleGuestCommand(command, reply, 157)")
call = dispatch.index("handleGuestCommand(command, reply, 157)")
guard_start = dispatch.rindex("if (sender && !sender->isAdmin())", 0, call)
guard = extract_braced(dispatch[guard_start:], "if (sender && !sender->isAdmin())")
# Exercise the actual authorization branch and setter, excluding unrelated