Add flood channel controls and cascade prompt details
This commit is contained in:
1 parent
84c166d5cc
commit
f747f96739
8 files changed
+611
-3
No files matched your search
@@ -527,7 +527,7 @@ prompt_for_radio_build_settings() {
|
||||
prompt_for_firmware_profile_settings() {
|
||||
local -a options=(
|
||||
"Keep target defaults"
|
||||
"Cascade: path.hash.mode=2 / loop.detect=minimal / rxdelay=2 / agc.reset.interval=8 / advert.interval=0 / flood.advert.interval=83 / multi.acks=1"
|
||||
"Cascade: path.hash.mode=2 / loop.detect=minimal / rxdelay=2 / agc.reset.interval=8 / advert.interval=0 / flood.advert.interval=83 / multi.acks=1 / companion.manual.add=1 / companion.autoadd=0"
|
||||
)
|
||||
|
||||
clear_firmware_profile_overrides
|
||||
|
||||
@@ -768,6 +768,74 @@ send text.flood checking ridge link
|
||||
|
||||
---
|
||||
|
||||
#### Forward flood group data packets on repeaters
|
||||
**Usage:**
|
||||
- `get flood.channel.data`
|
||||
- `set flood.channel.data <on|off>`
|
||||
|
||||
**Parameters:**
|
||||
- `on`: Retransmit received flood `GRP_DATA` channel packets.
|
||||
- `off`: Do not retransmit received flood `GRP_DATA` channel packets.
|
||||
|
||||
**Default:** `on`
|
||||
|
||||
**Forwarding behavior:** Repeater firmware only. The repeater still receives and
|
||||
logs the packet when logging is enabled; this only blocks retransmission.
|
||||
This is checked before `flood.channel.block` and applies to every flood
|
||||
`GRP_DATA` packet regardless of channel key. Flood group text (`GRP_TXT`) is
|
||||
unaffected.
|
||||
|
||||
---
|
||||
|
||||
#### Block selected flood channel packets on repeaters
|
||||
**Usage:**
|
||||
- `get flood.channel.block`
|
||||
- `get flood.channel.block.<n>`
|
||||
- `get flood.channel.block <name|8_hex_prefix>`
|
||||
- `set flood.channel.block <key> <name>`
|
||||
- `set flood.channel.block.<n> <key> <name>`
|
||||
- `set flood.channel.block #channel`
|
||||
- `set flood.channel.block.<n> #channel`
|
||||
- `del flood.channel.block.<n>`
|
||||
- `del flood.channel.block <name|8_hex_prefix>`
|
||||
|
||||
**Parameters:**
|
||||
- `n`: Slot number from `1` to `15`.
|
||||
- `key`: 128-bit or 256-bit channel key as hex.
|
||||
- `#channel`: Public hashtag channel name; derives the 128-bit channel key from the hashtag and is stored as the row name.
|
||||
- `name`: Local label for hex-key rows. Not needed for `#channel`; any extra text after `#channel` is ignored.
|
||||
- `8_hex_prefix`: First 4 bytes of the derived channel hash, shown by single-entry `get`.
|
||||
|
||||
**Slot behavior:** Without `.n`, `set flood.channel.block` updates an existing
|
||||
row with the same derived channel prefix or name, otherwise it uses the next
|
||||
empty slot. If all 15 slots are full, the command fails. With `.n`, the command
|
||||
writes that slot.
|
||||
|
||||
**Forwarding behavior:** Repeater firmware only. This only affects received
|
||||
flood `GRP_TXT` and `GRP_DATA` channel packets. The repeater still receives and
|
||||
logs the packet, but it does not retransmit it when a configured block entry can
|
||||
validate/decode it. If `flood.channel.data` is `off`, all flood `GRP_DATA`
|
||||
packets are blocked before this per-channel check runs.
|
||||
|
||||
**Matching behavior:** Each block entry stores the first 4 bytes of the derived
|
||||
channel hash for display and lookup. Current group packets carry only the first
|
||||
channel-hash byte, so that byte is used as a cheap prefilter. Only entries whose
|
||||
first hash byte matches the packet try MAC/decrypt with their stored key. If
|
||||
multiple blocked channels share the same first byte, the repeater tries each
|
||||
matching key until one validates; the packet is blocked only after a successful
|
||||
MAC/decrypt.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
set flood.channel.block #test
|
||||
set flood.channel.block.2 9cd8fcf22a47333b591d96a2b848b73f #test
|
||||
get flood.channel.block
|
||||
get flood.channel.block #test
|
||||
del flood.channel.block.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ACL
|
||||
|
||||
#### Add, update or remove permissions for a companion
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
|
||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
||||
|
||||
#define FLOOD_CHANNEL_BLOCK_FILE "/flood_ch_block"
|
||||
|
||||
#ifndef REPEATERS_CHANNEL_KEY_HEX
|
||||
#define REPEATERS_CHANNEL_KEY_HEX "89db441e2814dccf0dbd2e8cc5f501a3"
|
||||
#endif
|
||||
@@ -194,6 +196,25 @@ static bool buildRepeatersChannel(mesh::GroupChannel& channel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static File openFloodChannelBlockRead(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "r");
|
||||
#else
|
||||
return fs->open(filename);
|
||||
#endif
|
||||
}
|
||||
|
||||
static File openFloodChannelBlockWrite(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
fs->remove(filename);
|
||||
return fs->open(filename, FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "w");
|
||||
#else
|
||||
return fs->open(filename, "w", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint8_t batteryPercentFromMilliVolts(uint16_t batt_mv) {
|
||||
const int min_mv = BATT_MIN_MILLIVOLTS;
|
||||
const int max_mv = BATT_MAX_MILLIVOLTS;
|
||||
@@ -603,12 +624,46 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const {
|
||||
if (!entry.active || packet == NULL || !packet->isRouteFlood()) {
|
||||
return false;
|
||||
}
|
||||
uint8_t type = packet->getPayloadType();
|
||||
if (type != PAYLOAD_TYPE_GRP_TXT && type != PAYLOAD_TYPE_GRP_DATA) {
|
||||
return false;
|
||||
}
|
||||
if (packet->payload_len <= PATH_HASH_SIZE + CIPHER_MAC_SIZE || packet->payload[0] != entry.hash_prefix[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t data[MAX_PACKET_PAYLOAD];
|
||||
int len = mesh::Utils::MACThenDecrypt(entry.secret, data, &packet->payload[PATH_HASH_SIZE],
|
||||
packet->payload_len - PATH_HASH_SIZE);
|
||||
return len > 0;
|
||||
}
|
||||
|
||||
bool MyMesh::shouldBlockFloodChannelForward(const mesh::Packet* packet) const {
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
if (floodChannelBlockMatches(flood_channel_blocks[i], packet)) {
|
||||
MESH_DEBUG_PRINTLN("allowPacketForward: flood.channel.block matched slot=%d name=%s",
|
||||
i + 1, flood_channel_blocks[i].name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
|
||||
if (_prefs.disable_fwd) return false;
|
||||
if (packet->isRouteFlood()) {
|
||||
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
|
||||
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
|
||||
if (!_prefs.flood_channel_data_enabled && packet->getPayloadType() == PAYLOAD_TYPE_GRP_DATA) {
|
||||
MESH_DEBUG_PRINTLN("allowPacketForward: flood.channel.data off, blocking GRP_DATA");
|
||||
return false;
|
||||
}
|
||||
if (shouldBlockFloodChannelForward(packet)) return false;
|
||||
}
|
||||
if (packet->isRouteFlood() && recv_pkt_region == NULL) {
|
||||
MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet");
|
||||
@@ -2066,6 +2121,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
region_load_active = false;
|
||||
memset(flood_retry_bridge_states, 0, sizeof(flood_retry_bridge_states));
|
||||
recv_pkt_region = NULL;
|
||||
memset(flood_channel_blocks, 0, sizeof(flood_channel_blocks));
|
||||
|
||||
#if MAX_NEIGHBOURS
|
||||
memset(neighbours, 0, sizeof(neighbours));
|
||||
@@ -2115,6 +2171,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
_prefs.flood_retry_max_path = FLOOD_RETRY_ROOFTOP_MAX_PATH;
|
||||
_prefs.flood_retry_bridge_enabled = 0;
|
||||
_prefs.flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT;
|
||||
_prefs.flood_channel_data_enabled = 1;
|
||||
_prefs.battery_alert_enabled = 0;
|
||||
_prefs.battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
||||
_prefs.battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
||||
@@ -2158,6 +2215,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
acl.load(_fs, self_id);
|
||||
// TODO: key_store.begin();
|
||||
region_map.load(_fs);
|
||||
loadFloodChannelBlocks();
|
||||
|
||||
// establish default-scope
|
||||
{
|
||||
@@ -2930,6 +2988,276 @@ void MyMesh::onDefaultRegionChanged(const RegionEntry* r) {
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::clearFloodChannelBlockEntry(FloodChannelBlockEntry& entry) {
|
||||
memset(&entry, 0, sizeof(entry));
|
||||
}
|
||||
|
||||
void MyMesh::deriveFloodChannelBlockPrefix(const uint8_t* secret, uint8_t key_len,
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) const {
|
||||
mesh::Utils::sha256(prefix, FLOOD_CHANNEL_BLOCK_PREFIX_LEN, secret, key_len);
|
||||
}
|
||||
|
||||
void MyMesh::loadFloodChannelBlocks() {
|
||||
memset(flood_channel_blocks, 0, sizeof(flood_channel_blocks));
|
||||
if (_fs == NULL || !_fs->exists(FLOOD_CHANNEL_BLOCK_FILE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file = openFloodChannelBlockRead(_fs, FLOOD_CHANNEL_BLOCK_FILE);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t magic[4];
|
||||
uint8_t count = 0;
|
||||
bool success = file.read(magic, sizeof(magic)) == sizeof(magic)
|
||||
&& memcmp(magic, "FCB1", sizeof(magic)) == 0
|
||||
&& file.read(&count, sizeof(count)) == sizeof(count);
|
||||
|
||||
for (int i = 0; success && i < count && i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
uint8_t active = 0;
|
||||
uint8_t key_len = 0;
|
||||
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
|
||||
success = file.read(&active, sizeof(active)) == sizeof(active);
|
||||
success = success && file.read(&key_len, sizeof(key_len)) == sizeof(key_len);
|
||||
success = success && file.read(hash_prefix, sizeof(hash_prefix)) == sizeof(hash_prefix);
|
||||
success = success && file.read(secret, sizeof(secret)) == sizeof(secret);
|
||||
success = success && file.read((uint8_t*)name, sizeof(name)) == sizeof(name);
|
||||
if (!success) {
|
||||
break;
|
||||
}
|
||||
|
||||
name[sizeof(name) - 1] = 0;
|
||||
if (active && (key_len == CIPHER_KEY_SIZE || key_len == PUB_KEY_SIZE) && name[0] != 0) {
|
||||
auto& entry = flood_channel_blocks[i];
|
||||
entry.active = true;
|
||||
entry.key_len = key_len;
|
||||
memcpy(entry.secret, secret, sizeof(entry.secret));
|
||||
if (entry.key_len == CIPHER_KEY_SIZE) {
|
||||
memset(&entry.secret[CIPHER_KEY_SIZE], 0, PUB_KEY_SIZE - CIPHER_KEY_SIZE);
|
||||
}
|
||||
deriveFloodChannelBlockPrefix(entry.secret, entry.key_len, entry.hash_prefix);
|
||||
StrHelper::strncpy(entry.name, name, sizeof(entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
bool MyMesh::saveFloodChannelBlocks() {
|
||||
if (_fs == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = openFloodChannelBlockWrite(_fs, FLOOD_CHANNEL_BLOCK_FILE);
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t magic[4] = {'F', 'C', 'B', '1'};
|
||||
uint8_t count = FLOOD_CHANNEL_BLOCK_SLOTS;
|
||||
bool success = file.write(magic, sizeof(magic)) == sizeof(magic);
|
||||
success = success && file.write(&count, sizeof(count)) == sizeof(count);
|
||||
|
||||
for (int i = 0; success && i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
const auto& entry = flood_channel_blocks[i];
|
||||
uint8_t active = entry.active ? 1 : 0;
|
||||
success = file.write(&active, sizeof(active)) == sizeof(active);
|
||||
success = success && file.write(&entry.key_len, sizeof(entry.key_len)) == sizeof(entry.key_len);
|
||||
success = success && file.write(entry.hash_prefix, sizeof(entry.hash_prefix)) == sizeof(entry.hash_prefix);
|
||||
success = success && file.write(entry.secret, sizeof(entry.secret)) == sizeof(entry.secret);
|
||||
success = success && file.write((const uint8_t*)entry.name, sizeof(entry.name)) == sizeof(entry.name);
|
||||
}
|
||||
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
static void trimFloodChannelBlockSelector(const char* selector, char* dest, size_t dest_len) {
|
||||
selector = skipLocalSpaces(selector);
|
||||
StrHelper::strncpy(dest, selector == NULL ? "" : selector, dest_len);
|
||||
size_t len = strlen(dest);
|
||||
while (len > 0 && dest[len - 1] == ' ') {
|
||||
dest[--len] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockPrefixSelector(const char* selector,
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) {
|
||||
char text[16];
|
||||
trimFloodChannelBlockSelector(selector, text, sizeof(text));
|
||||
if (strlen(text) != FLOOD_CHANNEL_BLOCK_PREFIX_LEN * 2) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_PREFIX_LEN * 2; i++) {
|
||||
if (!mesh::Utils::isHexChar(text[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return mesh::Utils::fromHex(prefix, FLOOD_CHANNEL_BLOCK_PREFIX_LEN, text);
|
||||
}
|
||||
|
||||
int MyMesh::findFloodChannelBlockBySelector(const char* selector) const {
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
if (parseFloodChannelBlockPrefixSelector(selector, prefix)) {
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
const auto& entry = flood_channel_blocks[i];
|
||||
if (entry.active && memcmp(entry.hash_prefix, prefix, sizeof(entry.hash_prefix)) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
if (parsePositiveSelector(selector, index)) {
|
||||
return (index >= 1 && index <= FLOOD_CHANNEL_BLOCK_SLOTS) ? index - 1 : -1;
|
||||
}
|
||||
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
trimFloodChannelBlockSelector(selector, name, sizeof(name));
|
||||
if (name[0] == 0) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
const auto& entry = flood_channel_blocks[i];
|
||||
if (entry.active && strcmp(entry.name, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int MyMesh::findFloodChannelBlockSlot(const uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN], const char* name) const {
|
||||
int free_slot = -1;
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) {
|
||||
const auto& entry = flood_channel_blocks[i];
|
||||
if (entry.active) {
|
||||
if (memcmp(entry.hash_prefix, prefix, FLOOD_CHANNEL_BLOCK_PREFIX_LEN) == 0 || strcmp(entry.name, name) == 0) {
|
||||
return i;
|
||||
}
|
||||
} else if (free_slot < 0) {
|
||||
free_slot = i;
|
||||
}
|
||||
}
|
||||
return free_slot;
|
||||
}
|
||||
|
||||
void MyMesh::formatFloodChannelBlockDetail(char* reply, int idx) const {
|
||||
if (idx < 0 || idx >= FLOOD_CHANNEL_BLOCK_SLOTS) {
|
||||
strcpy(reply, "Err - not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& entry = flood_channel_blocks[idx];
|
||||
if (!entry.active) {
|
||||
snprintf(reply, 160, "> %d empty", idx + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
char prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN * 2 + 1];
|
||||
mesh::Utils::toHex(prefix, entry.hash_prefix, FLOOD_CHANNEL_BLOCK_PREFIX_LEN);
|
||||
snprintf(reply, 160, "> %d %s %u %s", idx + 1, prefix, (unsigned int)entry.key_len * 8, entry.name);
|
||||
}
|
||||
|
||||
void MyMesh::setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
|
||||
const char* name, char* reply) {
|
||||
if ((key_len != CIPHER_KEY_SIZE && key_len != PUB_KEY_SIZE) || secret == NULL || name == NULL || name[0] == 0) {
|
||||
strcpy(reply, "Err - bad params");
|
||||
return;
|
||||
}
|
||||
if (index < 0 || index > FLOOD_CHANNEL_BLOCK_SLOTS) {
|
||||
snprintf(reply, 160, "Err - index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
deriveFloodChannelBlockPrefix(secret, key_len, prefix);
|
||||
int slot = index > 0 ? index - 1 : findFloodChannelBlockSlot(prefix, name);
|
||||
if (slot < 0 || slot >= FLOOD_CHANNEL_BLOCK_SLOTS) {
|
||||
strcpy(reply, "Err - block list full");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& entry = flood_channel_blocks[slot];
|
||||
clearFloodChannelBlockEntry(entry);
|
||||
entry.active = true;
|
||||
entry.key_len = key_len;
|
||||
memcpy(entry.secret, secret, PUB_KEY_SIZE);
|
||||
if (entry.key_len == CIPHER_KEY_SIZE) {
|
||||
memset(&entry.secret[CIPHER_KEY_SIZE], 0, PUB_KEY_SIZE - CIPHER_KEY_SIZE);
|
||||
}
|
||||
deriveFloodChannelBlockPrefix(entry.secret, entry.key_len, entry.hash_prefix);
|
||||
StrHelper::strncpy(entry.name, name, sizeof(entry.name));
|
||||
|
||||
if (!saveFloodChannelBlocks()) {
|
||||
strcpy(reply, "Err - save failed");
|
||||
return;
|
||||
}
|
||||
formatFloodChannelBlockDetail(reply, slot);
|
||||
}
|
||||
|
||||
void MyMesh::formatFloodChannelBlocks(const char* selector, char* reply) {
|
||||
if (!selectorIsEmpty(selector)) {
|
||||
int idx = findFloodChannelBlockBySelector(selector);
|
||||
if (idx < 0) {
|
||||
strcpy(reply, "Err - not found");
|
||||
} else {
|
||||
formatFloodChannelBlockDetail(reply, idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
char* out = reply;
|
||||
size_t remaining = 160;
|
||||
int written = snprintf(out, remaining, ">");
|
||||
if (written < 0 || (size_t)written >= remaining) {
|
||||
reply[0] = 0;
|
||||
return;
|
||||
}
|
||||
out += written;
|
||||
remaining -= written;
|
||||
|
||||
for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS && remaining > 1; i++) {
|
||||
char display[8];
|
||||
const auto& entry = flood_channel_blocks[i];
|
||||
if (!entry.active) {
|
||||
strcpy(display, "-");
|
||||
} else {
|
||||
StrHelper::strncpy(display, entry.name, sizeof(display));
|
||||
if (strlen(entry.name) >= sizeof(display)) {
|
||||
display[sizeof(display) - 2] = '~';
|
||||
display[sizeof(display) - 1] = 0;
|
||||
}
|
||||
}
|
||||
written = snprintf(out, remaining, " %d:%s", i + 1, display);
|
||||
if (written < 0 || (size_t)written >= remaining) {
|
||||
out[remaining - 1] = 0;
|
||||
break;
|
||||
}
|
||||
out += written;
|
||||
remaining -= written;
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::deleteFloodChannelBlock(const char* selector, char* reply) {
|
||||
int idx = findFloodChannelBlockBySelector(selector);
|
||||
if (idx < 0 || idx >= FLOOD_CHANNEL_BLOCK_SLOTS || !flood_channel_blocks[idx].active) {
|
||||
strcpy(reply, "Err - not found");
|
||||
return;
|
||||
}
|
||||
|
||||
clearFloodChannelBlockEntry(flood_channel_blocks[idx]);
|
||||
if (!saveFloodChannelBlocks()) {
|
||||
strcpy(reply, "Err - save failed");
|
||||
return;
|
||||
}
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
|
||||
void MyMesh::formatStatsReply(char *reply) {
|
||||
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
uint8_t progress_marker;
|
||||
bool active;
|
||||
};
|
||||
struct FloodChannelBlockEntry {
|
||||
bool active;
|
||||
uint8_t key_len;
|
||||
uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
};
|
||||
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
|
||||
FloodChannelBlockEntry flood_channel_blocks[FLOOD_CHANNEL_BLOCK_SLOTS];
|
||||
uint32_t pending_discover_tag;
|
||||
unsigned long pending_discover_until;
|
||||
bool region_load_active;
|
||||
@@ -193,6 +201,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
void applySavedRadioParams();
|
||||
void processScheduledRadioSettings();
|
||||
bool isMillisTimerDue(unsigned long timestamp) const;
|
||||
void loadFloodChannelBlocks();
|
||||
bool saveFloodChannelBlocks();
|
||||
void clearFloodChannelBlockEntry(FloodChannelBlockEntry& entry);
|
||||
void deriveFloodChannelBlockPrefix(const uint8_t* secret, uint8_t key_len,
|
||||
uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) const;
|
||||
bool floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const;
|
||||
bool shouldBlockFloodChannelForward(const mesh::Packet* packet) const;
|
||||
int findFloodChannelBlockBySelector(const char* selector) const;
|
||||
int findFloodChannelBlockSlot(const uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN], const char* name) const;
|
||||
void formatFloodChannelBlockDetail(char* reply, int idx) const;
|
||||
bool hasScheduledRadioWorkDue() const;
|
||||
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_secs) const;
|
||||
uint32_t limitSleepToRtcTime(uint32_t timestamp, uint32_t sleep_secs) const;
|
||||
@@ -323,6 +341,10 @@ public:
|
||||
void startRegionsLoad() override;
|
||||
bool saveRegions() override;
|
||||
void onDefaultRegionChanged(const RegionEntry* r) override;
|
||||
void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
|
||||
const char* name, char* reply) override;
|
||||
void formatFloodChannelBlocks(const char* selector, char* reply) override;
|
||||
void deleteFloodChannelBlock(const char* selector, char* reply) override;
|
||||
|
||||
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
||||
|
||||
|
||||
@@ -648,6 +648,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.flood_max_unscoped = 64;
|
||||
_prefs.flood_max_advert = 8;
|
||||
_prefs.flood_channel_data_enabled = 1;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
#ifdef ROOM_PASSWORD
|
||||
|
||||
@@ -728,6 +728,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
_prefs.flood_advert_interval = 0; // disabled
|
||||
_prefs.disable_fwd = true;
|
||||
_prefs.flood_max = 64;
|
||||
_prefs.flood_channel_data_enabled = 1;
|
||||
_prefs.interference_threshold = 0; // disabled
|
||||
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
||||
|
||||
|
||||
+163
-2
@@ -380,6 +380,69 @@ static bool parseFloodRetryPrefixList(uint8_t dest[][FLOOD_RETRY_PREFIX_LEN], ui
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockKey(const char* text, uint8_t secret[PUB_KEY_SIZE], uint8_t& key_len) {
|
||||
if (text == NULL || text[0] == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(secret, 0, PUB_KEY_SIZE);
|
||||
if (text[0] == '#') {
|
||||
if (!isValidName(text)) {
|
||||
return false;
|
||||
}
|
||||
mesh::Utils::sha256(secret, CIPHER_KEY_SIZE, (const uint8_t*)text, strlen(text));
|
||||
key_len = CIPHER_KEY_SIZE;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t hex_len = strlen(text);
|
||||
if (!(hex_len == CIPHER_KEY_SIZE * 2 || hex_len == PUB_KEY_SIZE * 2)) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < hex_len; i++) {
|
||||
if (!mesh::Utils::isHexChar(text[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
key_len = (uint8_t)(hex_len / 2);
|
||||
return mesh::Utils::fromHex(secret, key_len, text);
|
||||
}
|
||||
|
||||
static bool parseFloodChannelBlockDotIndex(const char*& cursor, int& index) {
|
||||
if (*cursor != '.') {
|
||||
index = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor++;
|
||||
if (*cursor < '0' || *cursor > '9') {
|
||||
return false;
|
||||
}
|
||||
int value = 0;
|
||||
while (*cursor >= '0' && *cursor <= '9') {
|
||||
value = (value * 10) + (*cursor - '0');
|
||||
if (value > FLOOD_CHANNEL_BLOCK_SLOTS) {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
if (value < 1) {
|
||||
return false;
|
||||
}
|
||||
index = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void copyTrimmedFloodChannelBlockName(char* dest, size_t dest_len, const char* src) {
|
||||
src = skipSpacesConst(src);
|
||||
StrHelper::strncpy(dest, src, dest_len);
|
||||
size_t len = strlen(dest);
|
||||
while (len > 0 && dest[len - 1] == ' ') {
|
||||
dest[--len] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void applyDirectRetryPreset(NodePrefs* prefs, uint8_t preset) {
|
||||
prefs->retry_preset = preset;
|
||||
if (preset == RETRY_PRESET_INFRA) {
|
||||
@@ -547,6 +610,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->battery_alert_low_percent = BATTERY_ALERT_LOW_PERCENT_DEFAULT;
|
||||
_prefs->battery_alert_critical_percent = BATTERY_ALERT_CRITICAL_PERCENT_DEFAULT;
|
||||
_prefs->direct_retry_recent_enabled = DIRECT_RETRY_RECENT_DEFAULT;
|
||||
_prefs->flood_channel_data_enabled = 1;
|
||||
bool has_flood_retry_prefs = file.available() >= 2;
|
||||
if (has_flood_retry_prefs) {
|
||||
file.read((uint8_t *)&_prefs->flood_retry_attempts, sizeof(_prefs->flood_retry_attempts)); // 311
|
||||
@@ -578,8 +642,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->direct_retry_recent_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
||||
}
|
||||
if (file.available() >= (int)sizeof(_prefs->flood_channel_data_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
||||
}
|
||||
}
|
||||
// next: 672
|
||||
// next: 673
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
|
||||
@@ -639,6 +706,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->flood_retry_advert_enabled = constrain(_prefs->flood_retry_advert_enabled, 0, 1);
|
||||
_prefs->battery_alert_enabled = constrain(_prefs->battery_alert_enabled, 0, 1);
|
||||
_prefs->direct_retry_recent_enabled = constrain(_prefs->direct_retry_recent_enabled, 0, 1);
|
||||
_prefs->flood_channel_data_enabled = constrain(_prefs->flood_channel_data_enabled, 0, 1);
|
||||
if (_prefs->battery_alert_low_percent < 1
|
||||
|| _prefs->battery_alert_low_percent > 100
|
||||
|| _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) {
|
||||
@@ -736,7 +804,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
file.write((uint8_t *)&_prefs->battery_alert_low_percent, sizeof(_prefs->battery_alert_low_percent));
|
||||
file.write((uint8_t *)&_prefs->battery_alert_critical_percent, sizeof(_prefs->battery_alert_critical_percent));
|
||||
file.write((uint8_t *)&_prefs->direct_retry_recent_enabled, sizeof(_prefs->direct_retry_recent_enabled));
|
||||
// next: 672
|
||||
file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled));
|
||||
// next: 673
|
||||
|
||||
file.close();
|
||||
}
|
||||
@@ -1380,6 +1449,54 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
strcpy(reply, "Error, must be 0-5000 ms");
|
||||
}
|
||||
} else if (memcmp(config, "flood.channel.data ", 19) == 0) {
|
||||
if (strcmp(&config[19], "on") == 0) {
|
||||
_prefs->flood_channel_data_enabled = 1;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (strcmp(&config[19], "off") == 0) {
|
||||
_prefs->flood_channel_data_enabled = 0;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error, must be on or off");
|
||||
}
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
|
||||
const char* key_start = cursor;
|
||||
while (*cursor && *cursor != ' ') cursor++;
|
||||
size_t key_len_text = cursor - key_start;
|
||||
char key_text[PUB_KEY_SIZE * 2 + 1];
|
||||
if (key_len_text == 0 || key_len_text >= sizeof(key_text)) {
|
||||
strcpy(reply, "Error, use: set flood.channel.block[.n] <hex-key> <name>|#channel");
|
||||
return;
|
||||
}
|
||||
memcpy(key_text, key_start, key_len_text);
|
||||
key_text[key_len_text] = 0;
|
||||
|
||||
char name[FLOOD_CHANNEL_BLOCK_NAME_LEN];
|
||||
if (key_text[0] == '#') {
|
||||
StrHelper::strncpy(name, key_text, sizeof(name));
|
||||
} else {
|
||||
copyTrimmedFloodChannelBlockName(name, sizeof(name), cursor);
|
||||
}
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
uint8_t decoded_key_len = 0;
|
||||
if (!parseFloodChannelBlockKey(key_text, secret, decoded_key_len)) {
|
||||
strcpy(reply, "Error, key must be 128/256-bit hex or #channel");
|
||||
} else if (name[0] == 0 || !isValidName(name)) {
|
||||
strcpy(reply, "Error, bad name");
|
||||
} else {
|
||||
_callbacks->setFloodChannelBlock(index, secret, decoded_key_len, name, reply);
|
||||
}
|
||||
} else if (memcmp(config, "flood.retry.count ", 18) == 0) {
|
||||
int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1;
|
||||
if (attempts >= 0 && attempts <= 15) {
|
||||
@@ -1702,6 +1819,28 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->rx_delay_base));
|
||||
} else if (memcmp(config, "txdelay", 7) == 0) {
|
||||
sprintf(reply, "> %s", StrHelper::ftoa(_prefs->tx_delay_factor));
|
||||
} else if (memcmp(config, "flood.channel.data", 18) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->flood_channel_data_enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == 0 || config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
char selector[8];
|
||||
if (index > 0 && *cursor != 0) {
|
||||
strcpy(reply, "Error, use index or selector");
|
||||
return;
|
||||
}
|
||||
if (index > 0) {
|
||||
snprintf(selector, sizeof(selector), "%d", index);
|
||||
_callbacks->formatFloodChannelBlocks(selector, reply);
|
||||
} else {
|
||||
_callbacks->formatFloodChannelBlocks(cursor, reply);
|
||||
}
|
||||
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->flood_max_advert);
|
||||
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
|
||||
@@ -1880,6 +2019,28 @@ void CommonCLI::handleDelCmd(char* command, char* reply) {
|
||||
_callbacks->deleteScheduledRadioParams(true, skipSpacesConst(&config[11]), reply);
|
||||
} else if (memcmp(config, "radioat", 7) == 0 && (config[7] == 0 || config[7] == ' ')) {
|
||||
_callbacks->deleteScheduledRadioParams(false, skipSpacesConst(&config[7]), reply);
|
||||
} else if (memcmp(config, "flood.channel.block", 19) == 0
|
||||
&& (config[19] == ' ' || config[19] == '.')) {
|
||||
const char* cursor = &config[19];
|
||||
int index = 0;
|
||||
if (!parseFloodChannelBlockDotIndex(cursor, index)) {
|
||||
sprintf(reply, "Error, index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS);
|
||||
return;
|
||||
}
|
||||
cursor = skipSpacesConst(cursor);
|
||||
char selector[8];
|
||||
if (index > 0 && *cursor != 0) {
|
||||
strcpy(reply, "Error, use index or selector");
|
||||
return;
|
||||
}
|
||||
if (index > 0) {
|
||||
snprintf(selector, sizeof(selector), "%d", index);
|
||||
_callbacks->deleteFloodChannelBlock(selector, reply);
|
||||
} else if (*cursor != 0) {
|
||||
_callbacks->deleteFloodChannelBlock(cursor, reply);
|
||||
} else {
|
||||
strcpy(reply, "Error, use: del flood.channel.block <index|name|prefix>");
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "unknown del: ");
|
||||
StrHelper::strncpy(&reply[13], config, 160 - 14);
|
||||
|
||||
@@ -78,6 +78,16 @@
|
||||
#define COMMON_CLI_TMP_LEN ((FLOOD_RETRY_LIST_TEXT_MAX > (PRV_KEY_SIZE * 2 + 4)) ? FLOOD_RETRY_LIST_TEXT_MAX : (PRV_KEY_SIZE * 2 + 4))
|
||||
#endif
|
||||
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_SLOTS
|
||||
#define FLOOD_CHANNEL_BLOCK_SLOTS 15
|
||||
#endif
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_NAME_LEN
|
||||
#define FLOOD_CHANNEL_BLOCK_NAME_LEN 32
|
||||
#endif
|
||||
#ifndef FLOOD_CHANNEL_BLOCK_PREFIX_LEN
|
||||
#define FLOOD_CHANNEL_BLOCK_PREFIX_LEN 4
|
||||
#endif
|
||||
|
||||
#define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40
|
||||
#define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30
|
||||
#define DIRECT_RETRY_CR7_MIN_SNR_X4_DEFAULT 10
|
||||
@@ -156,6 +166,7 @@ struct NodePrefs { // persisted to file
|
||||
uint8_t battery_alert_low_percent;
|
||||
uint8_t battery_alert_critical_percent;
|
||||
uint8_t direct_retry_recent_enabled;
|
||||
uint8_t flood_channel_data_enabled;
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
@@ -216,6 +227,22 @@ public:
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len,
|
||||
const char* name, char* reply) {
|
||||
(void)index;
|
||||
(void)secret;
|
||||
(void)key_len;
|
||||
(void)name;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void formatFloodChannelBlocks(const char* selector, char* reply) {
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
virtual void deleteFloodChannelBlock(const char* selector, char* reply) {
|
||||
(void)selector;
|
||||
strcpy(reply, "Error: unsupported");
|
||||
}
|
||||
|
||||
virtual void startRegionsLoad() {
|
||||
// no op by default
|
||||
|
||||
Reference in new issue
Block a user