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
@@ -577,6 +577,7 @@ jobs:
|
||||
python3 -B test/test_companion_ethernet.py -v
|
||||
python3 -B test/test_companion_discovery.py -v
|
||||
python3 -B test/test_repeater_discovery.py -v
|
||||
python3 -B test/test_repeater_discovery_settings.py -v
|
||||
python3 -B test/test_companion_discovery_correlation.py -v
|
||||
python3 -B test/test_pico_ble_queues.py -v
|
||||
python3 -B test/test_companion_preferences_transaction.py -v
|
||||
|
||||
@@ -150,6 +150,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
|
||||
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbor.remove [pubkey_prefix]`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table; omit prefix to clear all | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes |
|
||||
| Neighbors | [`get/set discovery.node.responses`; `get/set discovery.region.responses`](cli_commands.md#enable-or-disable-discovery-responses) | Repeater only; newer source, not published 1.18.0.1 binaries; saved, default on, no reboot | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No |
|
||||
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | All full-parser text CLI roles | Yes | Yes | Yes |
|
||||
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local connection | Local | Local | Local |
|
||||
@@ -325,6 +326,7 @@ should use command `0x42`. See [Companion radio binary protocol](companion_proto
|
||||
| Neighbors | [`neighbors`](cli_commands.md#list-nearby-neighbors) | Role with a neighbor table | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`neighbor.remove [pubkey_prefix]`](cli_commands.md#remove-a-neighbor) | Role with a mutable neighbor table; omit prefix to clear all | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.neighbors`](cli_commands.md#discover-zero-hop-neighbors) | Repeater; some MQTT room servers | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`get/set discovery.node.responses`; `get/set discovery.region.responses`](cli_commands.md#enable-or-disable-discovery-responses) | Repeater only; newer source, not published 1.18.0.1 binaries; saved, default on, no reboot | Yes | Yes | Yes | Yes | Yes |
|
||||
| Neighbors | [`discover.scopes`](cli_commands.md#discover-neighbor-scopes-mqtt-observer-neighbors-feature) | MQTT observer with compiled neighbor support | No | No | No | Feature | No |
|
||||
| Statistics | [`clear stats`](cli_commands.md#clear-stats) | Full parser | Yes | Yes | Yes | Yes | Yes |
|
||||
| Statistics | [`stats-core`](cli_commands.md#stats-core) | Local connection | Local | Local | Local | Local | Local |
|
||||
|
||||
@@ -636,6 +636,44 @@ require MQTT or PSRAM.
|
||||
|
||||
---
|
||||
|
||||
### Enable or disable discovery responses
|
||||
|
||||
**Availability:** Repeater firmware in newer source builds. These switches are
|
||||
not included in the existing published **1.18.0.1** binaries. They do not require
|
||||
MQTT or PSRAM and are not Companion, Room Server, or Sensor settings.
|
||||
|
||||
```text
|
||||
get discovery.node.responses
|
||||
set discovery.node.responses on
|
||||
set discovery.node.responses off
|
||||
get discovery.region.responses
|
||||
set discovery.region.responses on
|
||||
set discovery.region.responses off
|
||||
```
|
||||
|
||||
| Setting | Default | What `off` does |
|
||||
| --- | --- | --- |
|
||||
| `discovery.node.responses` | `on` | Stops replies to normal zero-hop node-discovery requests. |
|
||||
| `discovery.region.responses` | `on` | Stops replies to anonymous region/scope queries, even while adverts remain enabled. |
|
||||
|
||||
The two choices are independent. They apply immediately after a successful
|
||||
save and survive reboot; no reboot is needed. Fresh installations and upgrades
|
||||
without saved choices keep both on, preserving the previous behavior. Use a
|
||||
local console or authenticated administrator CLI to change them.
|
||||
|
||||
Enabling node responses still honors the existing hidden-node rule: when both
|
||||
`advert.interval` and `flood.advert.interval` are `0`, the repeater does not
|
||||
answer normal discovery requests. Enabling a response switch also retains the
|
||||
normal request-validation and rate limits.
|
||||
|
||||
These switches do not stop the node's own adverts, outgoing
|
||||
`discover.neighbors` or `discover.scopes` commands, or automatic
|
||||
[MQTT neighbor discovery](#view-or-change-periodic-neighbors-publishing-mqtt-observer-neighbors-feature).
|
||||
They do not restrict an authorized administrator's `region` CLI commands.
|
||||
Use `set mqtt.neighbors off` to stop the periodic outgoing MQTT discovery pass.
|
||||
|
||||
---
|
||||
|
||||
### Discover neighbor scopes (MQTT observer, neighbors feature)
|
||||
|
||||
Refreshes the zero-hop neighbor table, then queries each neighbor for its region
|
||||
@@ -649,6 +687,10 @@ scopes and publishes the assembled table to the MQTT `neighbors` topic once.
|
||||
Elsewhere it replies `Err - neighbors not enabled in this build`. If a
|
||||
`discover.neighbors` refresh is already in flight, the scope pass is queued behind it.
|
||||
|
||||
This is an outgoing query. A repeater's separate
|
||||
[discovery response switches](#enable-or-disable-discovery-responses) control
|
||||
whether that repeater answers other nodes' requests.
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
@@ -6399,6 +6441,12 @@ default-off updates keep the existing record size.
|
||||
> While enabled, `get mqtt.status` gains a trailing `nbr: <next>/<last>` field
|
||||
> (time to next publish, and how the last publish went).
|
||||
|
||||
This setting controls this node's outgoing periodic discovery and MQTT
|
||||
publication. It does not control replies to requests from other nodes. Newer
|
||||
repeater source builds have separate
|
||||
[node and region response switches](#enable-or-disable-discovery-responses),
|
||||
both defaulting to on; those switches are not in the published 1.18.0.1 images.
|
||||
|
||||
---
|
||||
|
||||
#### View or change the neighbors publish interval (MQTT observer, neighbors feature)
|
||||
|
||||
@@ -748,6 +748,7 @@ static bool hasCompleteAnonReplyPath(const uint8_t* data, size_t data_len) {
|
||||
|
||||
uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp,
|
||||
const uint8_t* data, size_t data_len) {
|
||||
if (!(_prefs.discovery_responses & mesh::discovery::REGION_RESPONSES)) return 0;
|
||||
if (!hasCompleteAnonReplyPath(data, data_len)) return 0;
|
||||
if (anon_limiter.allow(rtc_clock.getCurrentTime())) {
|
||||
// request data has: {reply-path-len}{reply-path}
|
||||
@@ -2832,6 +2833,8 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
data[len] = 0; // ensure null terminator
|
||||
if (data[4] == ANON_REQ_TYPE_REGIONS
|
||||
&& !(_prefs.discovery_responses & mesh::discovery::REGION_RESPONSES)) return;
|
||||
const bool login_request = data[4] == 0 || data[4] >= ' ';
|
||||
uint8_t reply_len;
|
||||
|
||||
@@ -3579,6 +3582,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
|
||||
void MyMesh::onControlDataRecv(mesh::Packet* packet) {
|
||||
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
|
||||
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6
|
||||
&& (_prefs.discovery_responses & mesh::discovery::NODE_RESPONSES)
|
||||
&& !isHiddenNode() // this node wishes to NOT be discoverable
|
||||
&& discover_limiter.allow(rtc_clock.getCurrentTime())
|
||||
) {
|
||||
@@ -5585,6 +5589,43 @@ void MyMesh::dumpLogFile() {
|
||||
void MyMesh::dumpLogFile() { }
|
||||
#endif
|
||||
|
||||
bool MyMesh::handleDiscoveryResponseCommand(const char* command, char* reply, size_t capacity) {
|
||||
const bool get = strncmp(command, "get ", 4) == 0;
|
||||
const bool set = strncmp(command, "set ", 4) == 0;
|
||||
if (!get && !set) return false;
|
||||
const char* name = command + 4;
|
||||
const char* setting;
|
||||
uint8_t flag;
|
||||
if (strncmp(name, "discovery.node.responses", 24) == 0) {
|
||||
setting = "discovery.node.responses";
|
||||
flag = mesh::discovery::NODE_RESPONSES;
|
||||
} else if (strncmp(name, "discovery.region.responses", 26) == 0) {
|
||||
setting = "discovery.region.responses";
|
||||
flag = mesh::discovery::REGION_RESPONSES;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const char* value = name + strlen(setting);
|
||||
if (*value && *value != ' ' && *value != '\t') return false;
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if ((get && *value) || (set && strcmp(value, "on") && strcmp(value, "off"))) {
|
||||
snprintf(reply, capacity, "Err - use get %s or set %s on|off", setting, setting);
|
||||
} else if (get) {
|
||||
snprintf(reply, capacity, "> %s", (_prefs.discovery_responses & flag) ? "on" : "off");
|
||||
} else {
|
||||
const uint8_t previous = _prefs.discovery_responses;
|
||||
if (strcmp(value, "on") == 0) _prefs.discovery_responses |= flag;
|
||||
else _prefs.discovery_responses &= ~flag;
|
||||
if (_cli.saveCommonPrefs()) {
|
||||
snprintf(reply, capacity, "OK - %s %s", setting, value);
|
||||
} else {
|
||||
_prefs.discovery_responses = previous;
|
||||
snprintf(reply, capacity, "Err - save unconfirmed; reboot may differ");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MyMesh::handleGuestCommand(const char* command, char* reply, size_t capacity) {
|
||||
const bool get = strncmp(command, "get guest", 9) == 0;
|
||||
const bool set = strncmp(command, "set guest", 9) == 0;
|
||||
@@ -12762,7 +12803,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (handleGuestCommand(command, reply, 157)
|
||||
if (handleDiscoveryResponseCommand(command, reply, 157)
|
||||
|| handleGuestCommand(command, reply, 157)
|
||||
|| mesh::cli::handleGuestsGet(acl, command, reply, 157)
|
||||
|| mesh::cli::handleLoginsGet(login_history, command, reply, 157)) return;
|
||||
_cli.handleCommand(sender_timestamp, command, reply);
|
||||
@@ -12785,7 +12827,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char *
|
||||
}
|
||||
}
|
||||
|
||||
if (handleGuestCommand(command, reply, 157)
|
||||
if (handleDiscoveryResponseCommand(command, reply, 157)
|
||||
|| handleGuestCommand(command, reply, 157)
|
||||
|| mesh::cli::handleGuestsGet(acl, command, reply, 157)
|
||||
|| mesh::cli::handleLoginsGet(login_history, command, reply, 157)) return;
|
||||
|
||||
|
||||
@@ -399,6 +399,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
mesh::GuestLoginSettings guest_login;
|
||||
mesh::LoginHistory login_history;
|
||||
bool handleGuestCommand(const char* command, char* reply, size_t capacity);
|
||||
bool handleDiscoveryResponseCommand(const char* command, char* reply, size_t capacity);
|
||||
ClientACL acl;
|
||||
CommonCLI _cli;
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
|
||||
@@ -832,6 +832,7 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
#endif
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
_prefs->trace_when_repeat_off = 0;
|
||||
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
|
||||
_prefs->ota_channel = 0;
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->rs232_heartbeat = 1;
|
||||
@@ -1105,6 +1106,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
// from a previous load of a newer image.
|
||||
_prefs->usb_debug_enabled = 0;
|
||||
_prefs->trace_when_repeat_off = 0;
|
||||
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
|
||||
_prefs->gps_sync_interval_hours = 0;
|
||||
_prefs->ota_channel = 0;
|
||||
_prefs->rs232_bridge_enabled = 0;
|
||||
@@ -1809,6 +1811,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
ensureRxPowerSavingDefaults(&_prefs->rx_ps_rx_us, &_prefs->rx_ps_sleep_us);
|
||||
recalculateRxPowerSavingFromLevel(_prefs); // retune supported level-based timings to loaded SF/BW
|
||||
|
||||
// Read reply controls independently of the current UART profile. Older
|
||||
// images have no record and retain both replies enabled.
|
||||
_prefs->discovery_responses = mesh::discovery::readPrefs(file);
|
||||
// Read the footer independently of optional legacy UART tails/profile.
|
||||
#if defined(RAK_4631)
|
||||
_prefs->peripherals_shared = mesh::peripherals::readCommonMode(file);
|
||||
@@ -1983,6 +1988,11 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
prefs->rs232_idle_seconds);
|
||||
WRITE_COMMON_PREFS_BYTES(power_tail, sizeof(power_tail));
|
||||
#endif
|
||||
if (prefs->discovery_responses != mesh::discovery::DEFAULT_RESPONSES) {
|
||||
uint8_t discovery_tail[mesh::discovery::PREFS_SIZE];
|
||||
mesh::discovery::encodePrefs(discovery_tail, prefs->discovery_responses);
|
||||
WRITE_COMMON_PREFS_BYTES(discovery_tail, sizeof(discovery_tail));
|
||||
}
|
||||
#if defined(RAK_4631)
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
|
||||
mesh::peripherals::encodeFooter(peripheral_footer, prefs->peripherals_shared);
|
||||
@@ -2078,6 +2088,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
_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;
|
||||
if (_prefs->discovery_responses > mesh::discovery::DEFAULT_RESPONSES)
|
||||
_prefs->discovery_responses = mesh::discovery::DEFAULT_RESPONSES;
|
||||
#if defined(ESP32_PLATFORM)
|
||||
if (!mesh::esp32::cpuclock::validSelection(_prefs->cpu_freq_mhz)
|
||||
|| (_prefs->cpu_freq_mhz && !_board->supportsCpuFrequencyMHz(_prefs->cpu_freq_mhz)))
|
||||
@@ -2234,6 +2246,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
_prefs->rs232_idle_seconds);
|
||||
file.write(power_tail, sizeof(power_tail));
|
||||
#endif
|
||||
if (_prefs->discovery_responses != mesh::discovery::DEFAULT_RESPONSES) {
|
||||
uint8_t discovery_tail[mesh::discovery::PREFS_SIZE];
|
||||
mesh::discovery::encodePrefs(discovery_tail, _prefs->discovery_responses);
|
||||
file.write(discovery_tail, sizeof(discovery_tail));
|
||||
}
|
||||
#if defined(RAK_4631)
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
|
||||
mesh::peripherals::encodeFooter(peripheral_footer, _prefs->peripherals_shared);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <helpers/CommonRadioPrefs.h>
|
||||
#include <helpers/DynamicConfigSerializer.h>
|
||||
#include <helpers/RepeaterRadioTiming.h>
|
||||
#include <helpers/DiscoveryResponsePrefs.h>
|
||||
#include <helpers/RadioProfileCLI.h>
|
||||
#include <helpers/MorseReplyHandoff.h>
|
||||
#include <helpers/WirelessControl.h>
|
||||
@@ -203,6 +204,9 @@ public:
|
||||
// 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;
|
||||
// Independent repeater reply gates: bit 0=node, bit 1=anonymous regions.
|
||||
// Stored in a checked extension before the platform-specific EOF footer.
|
||||
uint8_t discovery_responses = 3;
|
||||
// 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;
|
||||
@@ -472,6 +476,7 @@ protected:
|
||||
def("lat", node_lat);
|
||||
def("lon", node_lon);
|
||||
def("disc_mod", discovery_mod_timestamp);
|
||||
def("disc_responses", discovery_responses);
|
||||
def("ota_ch", ota_channel); // WiFi OTA release channel selector
|
||||
def("radio", radio);
|
||||
def("bridge", bridge);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace discovery {
|
||||
|
||||
static constexpr uint8_t NODE_RESPONSES = 1;
|
||||
static constexpr uint8_t REGION_RESPONSES = 2;
|
||||
static constexpr uint8_t DEFAULT_RESPONSES = NODE_RESPONSES | REGION_RESPONSES;
|
||||
static constexpr size_t PREFS_SIZE = 8;
|
||||
static constexpr size_t COMMON_LEGACY_SIZE = 877;
|
||||
static constexpr size_t LEGACY_RS232_TAIL_SIZE = 10;
|
||||
|
||||
inline uint16_t checksum(const uint8_t* bytes, size_t length) {
|
||||
uint16_t crc = 0xffff;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
|
||||
for (unsigned bit = 0; bit < 8; ++bit)
|
||||
crc = static_cast<uint16_t>((crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// The record is omitted when both response types are enabled. Keep it before
|
||||
// existing platform footers, whose readers locate those policies at EOF.
|
||||
inline void encodePrefs(uint8_t* out, uint8_t mask) {
|
||||
out[0] = 'D'; out[1] = 'R'; out[2] = 'P'; out[3] = 1;
|
||||
out[4] = mask <= DEFAULT_RESPONSES ? mask : DEFAULT_RESPONSES;
|
||||
out[5] = 0;
|
||||
const uint16_t crc = checksum(out, 6);
|
||||
out[6] = static_cast<uint8_t>(crc >> 8);
|
||||
out[7] = static_cast<uint8_t>(crc);
|
||||
}
|
||||
|
||||
inline bool decodePrefs(const uint8_t* in, size_t size, uint8_t& mask) {
|
||||
mask = DEFAULT_RESPONSES;
|
||||
if (!in || size != PREFS_SIZE || in[0] != 'D' || in[1] != 'R'
|
||||
|| in[2] != 'P' || in[3] != 1 || in[4] > DEFAULT_RESPONSES || in[5] != 0
|
||||
|| checksum(in, 6) != static_cast<uint16_t>((in[6] << 8) | in[7]))
|
||||
return false;
|
||||
mask = in[4];
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename File>
|
||||
uint8_t readPrefs(File& file) {
|
||||
uint8_t mask = DEFAULT_RESPONSES;
|
||||
uint8_t bytes[PREFS_SIZE] = {};
|
||||
const size_t size = file.size();
|
||||
size_t offset = COMMON_LEGACY_SIZE;
|
||||
if (size < offset + PREFS_SIZE || !file.seek(offset)
|
||||
|| file.read(bytes, sizeof(bytes)) != sizeof(bytes))
|
||||
return mask;
|
||||
// The source image decides whether a UART power tail exists. Depending on
|
||||
// this build's bridge flags would lose saved settings across role changes.
|
||||
if (bytes[0] == 'R' && bytes[1] == 'S' && bytes[2] == 'P' && bytes[3] == 1) {
|
||||
offset += LEGACY_RS232_TAIL_SIZE;
|
||||
if (size < offset + PREFS_SIZE || !file.seek(offset)
|
||||
|| file.read(bytes, sizeof(bytes)) != sizeof(bytes))
|
||||
return mask;
|
||||
}
|
||||
decodePrefs(bytes, sizeof(bytes), mask);
|
||||
return mask;
|
||||
}
|
||||
|
||||
} // namespace discovery
|
||||
} // namespace mesh
|
||||
+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