Add saved node and region discovery response switches
This commit is contained in:
1 parent
1ab4091a05
commit
4fd3358a45
15 files changed
+705
-5
No files matched your search
+84
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user