diff --git a/build.sh b/build.sh index 0370063a..1697e0c4 100755 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 48b76245..1971709a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -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 ` + +**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.` +- `get flood.channel.block ` +- `set flood.channel.block ` +- `set flood.channel.block. ` +- `set flood.channel.block #channel` +- `set flood.channel.block. #channel` +- `del flood.channel.block.` +- `del flood.channel.block ` + +**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 diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 30e437b6..e3054035 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index c521c80c..83485b25 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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; } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index dbb7eefa..b97228f3 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -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 diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 59c9aa09..99e10bc8 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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') diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 9c312deb..c43b2948 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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] |#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 "); + } } else { strcpy(reply, "unknown del: "); StrHelper::strncpy(&reply[13], config, 160 - 14); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 38019a8f..7cfef4d4 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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