diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1971709a..da39fca2 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -781,9 +781,11 @@ send text.flood checking ridge link **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. +This is checked before `flood.channel.block` and applies to flood `GRP_DATA` +packets regardless of channel key, subject to `flood.channel.block.hops`. +Flood group text (`GRP_TXT`) is unaffected by this setting. + +`get flood.channel.data` includes the active hop gate as `h=all` or `h>N`. --- @@ -792,10 +794,12 @@ unaffected. - `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` +- `get flood.channel.block.hops` +- `set flood.channel.block [h=]` +- `set flood.channel.block. [h=]` +- `set flood.channel.block #channel [h=]` +- `set flood.channel.block. #channel [h=]` +- `set flood.channel.block.hops ` - `del flood.channel.block.` - `del flood.channel.block ` @@ -803,8 +807,11 @@ unaffected. - `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. +- `name`: Local label for hex-key rows. Not needed for `#channel`; extra text after `#channel` is ignored unless it is a hop setting. - `8_hex_prefix`: First 4 bytes of the derived channel hash, shown by single-entry `get`. +- `all`: Block matching flood channel packets at any received flood hop count. +- `1-7`: Maximum received flood path hash count to repeat. Matching packets over this hop count are blocked. +- `default`: Row inherits the global `flood.channel.block.hops` setting. **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 @@ -815,7 +822,26 @@ writes that slot. 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. +packets over the hop gate are blocked before this per-channel check runs. + +**Hop gate:** `flood.channel.block.hops` defaults to `all`, which preserves the +original behavior. When set to `N` from `1` to `7`, both `flood.channel.data off` +and block rows that inherit the global setting only block packets whose received +flood path hash count is greater than `N`; packets at `N` hops or lower can +still repeat. For example, `set flood.channel.block.hops 1` repeats zero-hop and +one-hop matches but blocks two-hop and longer matches. + +Each block row can override the global hop gate with `h=`. +For example, `set flood.channel.block #wardriving h=3` blocks `#wardriving` +matches above three hops, while `set flood.channel.block #bot h=7` blocks +`#bot` matches above seven hops. Use `h=default` to make the row inherit the +global setting again. + +`get flood.channel.block` includes the global default first, then adds per-row +overrides as `/h>N` or `/h=all`; inherited rows do not show a suffix. Single-row +`get` replies include that row's stored hop mode as `h=def`, `h=all`, or `h>N`. +List replies truncate displayed row names only when the full list would exceed +the remote-management response limit. **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 @@ -829,7 +855,11 @@ MAC/decrypt. ``` set flood.channel.block #test set flood.channel.block.2 9cd8fcf22a47333b591d96a2b848b73f #test +set flood.channel.block.hops 3 +set flood.channel.block #wardriving h=3 +set flood.channel.block #bot h=7 get flood.channel.block +get flood.channel.block.hops get flood.channel.block #test del flood.channel.block.2 ``` diff --git a/docs/halo_keymind_settings.md b/docs/halo_keymind_settings.md index 826753a2..1e1b0d04 100644 --- a/docs/halo_keymind_settings.md +++ b/docs/halo_keymind_settings.md @@ -77,7 +77,11 @@ set flood.retry.ignore none | `battery.alert.low` | Warning threshold percentage. Must be greater than `battery.alert.critical`. | `get battery.alert.low`, `set battery.alert.low <1-100>` | `set battery.alert.low 20` | | `battery.alert.critical` | Critical threshold percentage. Critical warnings repeat more often. | `get battery.alert.critical`, `set battery.alert.critical <0-99>` | `set battery.alert.critical 10` | | `recent.repeater` | Shows, seeds, or clears the recent repeater prefix/SNR table used by direct retry and bridge freshness checks. | `get recent.repeater`, `get recent.repeater `, `set recent.repeater `, `clear recent.repeater` | `set recent.repeater A1B2C3 -8.5` | +| `flood.channel.data` | Turns forwarding of flood `GRP_DATA` channel packets on or off, subject to the channel block hop gate. | `get flood.channel.data`, `set flood.channel.data on/off` | `set flood.channel.data off` | +| `flood.channel.block` | Blocks selected flood `GRP_TXT`/`GRP_DATA` channels when the key validates the packet. Add `h=` for a per-channel hop override. | `get flood.channel.block`, `set flood.channel.block[.n] [name] [h=...]`, `del flood.channel.block[.n]` | `set flood.channel.block #wardriving h=3` | +| `flood.channel.block.hops` | Limits channel/data forwarding to short flood paths. `all` blocks matching packets at any hop count; `1`-`7` repeats packets at that hop count or lower and blocks longer matches. | `get flood.channel.block.hops`, `set flood.channel.block.hops ` | `set flood.channel.block.hops 3` | | `outpath` | Overrides the primary direct route used for replies to the current remote client. | `get outpath`, `set outpath `, `set outpath direct`, `set outpath clear`, `set outpath flood` | `set outpath A1B2C3,D4E5F6` | +| `altpath` | Adds a secondary direct route for repeater replies to the current remote client. | `get altpath`, `set altpath `, `set altpath direct`, `set altpath clear`, `set altpath flood` | `set altpath 71CE82,BA09F0` | ## Other Keymind Commands @@ -146,8 +150,8 @@ Serial CLI pages contain up to `128` rows. Remote LoRa CLI pages contain up to ## Direct Path Overrides -`outpath` applies to the current remote client ACL entry. It needs remote -client context, so it is not useful from the local serial CLI. +`outpath` and `altpath` apply to the current remote client ACL entry. They need +remote client context, so they are not useful from the local serial CLI. Set paths with comma-separated hop hashes. Each hop must be `2`, `4`, or `6` hex characters, and all hops in one path must use the same width. @@ -158,6 +162,9 @@ set outpath A1B2C3,D4E5F6 set outpath direct set outpath clear set outpath flood +get altpath +set altpath 71CE82,BA09F0 +set altpath clear ``` `set outpath direct` sets a zero-hop direct route for a client reachable without @@ -165,6 +172,14 @@ repeaters. `set outpath clear` forgets the override and lets normal path discovery fill it again. `set outpath flood` forces replies to use flood packets until the client logs in again. +When `outpath` is a valid direct path and `altpath` is also a valid, different +direct path, repeater DM replies send two packets: one on `outpath` and one on +`altpath`. The secondary `altpath` copy does not create its own direct-retry +state, so retry tracking stays attached to the primary `outpath` packet. +`altpath clear` disables the secondary direct reply. `altpath flood` is accepted +for command symmetry, but it does not create a second flood reply; only a valid +direct `altpath` sends the second packet. + ## Direct Retry Settings Direct retry applies to direct-routed packets. A queued resend is canceled when the next-hop echo is heard. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index e3054035..165aee10 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -624,10 +624,64 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui } } +static bool directPathsEqual(const uint8_t* a_path, uint8_t a_len, const uint8_t* b_path, uint8_t b_len) { + if (!mesh::Packet::isValidPathLen(a_len) || !mesh::Packet::isValidPathLen(b_len) || a_len != b_len) { + return false; + } + uint8_t hash_count = a_len & 63; + uint8_t hash_size = (a_len >> 6) + 1; + uint8_t byte_len = hash_count * hash_size; + return byte_len == 0 || memcmp(a_path, b_path, byte_len) == 0; +} + +void MyMesh::sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) { + if (packet == NULL) { + return; + } + if (client == NULL || !mesh::Packet::isValidPathLen(client->out_path_len)) { + sendFloodReply(packet, delay_millis, path_hash_size); + return; + } + + mesh::Packet* alt = NULL; + if (mesh::Packet::isValidPathLen(client->alt_path_len) + && !directPathsEqual(client->out_path, client->out_path_len, client->alt_path, client->alt_path_len)) { + alt = obtainNewPacket(); + if (alt != NULL) { + *alt = *packet; + } else { + MESH_DEBUG_PRINTLN("sendClientReply: altpath packet pool empty"); + } + } + + sendDirect(packet, client->out_path, client->out_path_len, delay_millis); + if (alt != NULL) { + uint8_t direct_retry_enabled = _prefs.direct_retry_enabled; + _prefs.direct_retry_enabled = 0; + sendDirect(alt, client->alt_path, client->alt_path_len, delay_millis); + _prefs.direct_retry_enabled = direct_retry_enabled; + } +} + +uint8_t MyMesh::resolveFloodChannelBlockHops(uint8_t max_hops) const { + return max_hops == FLOOD_CHANNEL_BLOCK_HOPS_INHERIT ? _prefs.flood_channel_block_max_hops : max_hops; +} + +bool MyMesh::floodChannelBlockHopApplies(const mesh::Packet* packet, uint8_t max_hops) const { + if (packet == NULL) { + return false; + } + max_hops = resolveFloodChannelBlockHops(max_hops); + return max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL || packet->getPathHashCount() > max_hops; +} + bool MyMesh::floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const { if (!entry.active || packet == NULL || !packet->isRouteFlood()) { return false; } + if (!floodChannelBlockHopApplies(packet, entry.max_hops)) { + return false; + } uint8_t type = packet->getPayloadType(); if (type != PAYLOAD_TYPE_GRP_TXT && type != PAYLOAD_TYPE_GRP_DATA) { return false; @@ -645,8 +699,8 @@ bool MyMesh::floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const 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); + MESH_DEBUG_PRINTLN("allowPacketForward: flood.channel.block matched slot=%d name=%s hops=%d", + i + 1, flood_channel_blocks[i].name, packet->getPathHashCount()); return true; } } @@ -659,8 +713,11 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) { 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"); + if (!_prefs.flood_channel_data_enabled + && packet->getPayloadType() == PAYLOAD_TYPE_GRP_DATA + && floodChannelBlockHopApplies(packet, _prefs.flood_channel_block_max_hops)) { + MESH_DEBUG_PRINTLN("allowPacketForward: flood.channel.data off, blocking GRP_DATA hops=%d", + packet->getPathHashCount()); return false; } if (shouldBlockFloodChannelForward(packet)) return false; @@ -1919,13 +1976,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); - if (reply) { - if (mesh::Packet::isValidPathLen(client->out_path_len)) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); - } - } + sendClientReply(client, reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); @@ -1952,13 +2003,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, PUB_KEY_SIZE); mesh::Packet *ack = createAck(ack_hash); - if (ack) { - if (mesh::Packet::isValidPathLen(client->out_path_len)) { - sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); - } else { - sendFloodReply(ack, TXT_ACK_DELAY, packet->getPathHashSize()); - } - } + sendClientReply(client, ack, TXT_ACK_DELAY, packet->getPathHashSize()); } uint8_t temp[166]; @@ -1980,13 +2025,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); - if (reply) { - if (mesh::Packet::isValidPathLen(client->out_path_len)) { - sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); - } else { - sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); - } - } + sendClientReply(client, reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); @@ -2172,6 +2211,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.flood_retry_bridge_enabled = 0; _prefs.flood_retry_advert_enabled = FLOOD_RETRY_ADVERT_DEFAULT; _prefs.flood_channel_data_enabled = 1; + _prefs.flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; _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; @@ -3011,18 +3051,20 @@ void MyMesh::loadFloodChannelBlocks() { uint8_t magic[4]; uint8_t count = 0; bool success = file.read(magic, sizeof(magic)) == sizeof(magic) - && memcmp(magic, "FCB1", sizeof(magic)) == 0 + && memcmp(magic, "FCB2", 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 max_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; 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(&max_hops, sizeof(max_hops)) == sizeof(max_hops); 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); @@ -3035,6 +3077,9 @@ void MyMesh::loadFloodChannelBlocks() { auto& entry = flood_channel_blocks[i]; entry.active = true; entry.key_len = key_len; + entry.max_hops = (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL + || max_hops == FLOOD_CHANNEL_BLOCK_HOPS_INHERIT + || (max_hops >= 1 && max_hops <= 7)) ? max_hops : FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; 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); @@ -3057,7 +3102,7 @@ bool MyMesh::saveFloodChannelBlocks() { return false; } - const uint8_t magic[4] = {'F', 'C', 'B', '1'}; + const uint8_t magic[4] = {'F', 'C', 'B', '2'}; 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); @@ -3065,8 +3110,10 @@ bool MyMesh::saveFloodChannelBlocks() { 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; + uint8_t max_hops = entry.active ? entry.max_hops : FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; 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(&max_hops, sizeof(max_hops)) == sizeof(max_hops); 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); @@ -3100,6 +3147,16 @@ static bool parseFloodChannelBlockPrefixSelector(const char* selector, return mesh::Utils::fromHex(prefix, FLOOD_CHANNEL_BLOCK_PREFIX_LEN, text); } +static void formatFloodChannelBlockHops(char* dest, uint8_t max_hops) { + if (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL) { + strcpy(dest, "h=all"); + } else if (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_INHERIT) { + strcpy(dest, "h=def"); + } else { + sprintf(dest, "h>%u", (unsigned int)max_hops); + } +} + int MyMesh::findFloodChannelBlockBySelector(const char* selector) const { uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]; if (parseFloodChannelBlockPrefixSelector(selector, prefix)) { @@ -3154,17 +3211,19 @@ void MyMesh::formatFloodChannelBlockDetail(char* reply, int idx) const { const auto& entry = flood_channel_blocks[idx]; if (!entry.active) { - snprintf(reply, 160, "> %d empty", idx + 1); + snprintf(reply, 150, "> %d empty", idx + 1); return; } char prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN * 2 + 1]; + char hops[8]; 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); + formatFloodChannelBlockHops(hops, entry.max_hops); + snprintf(reply, 150, "> %d %s %u %s %s", idx + 1, prefix, (unsigned int)entry.key_len * 8, hops, entry.name); } void MyMesh::setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len, - const char* name, char* reply) { + const char* name, uint8_t max_hops, 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; @@ -3173,6 +3232,12 @@ void MyMesh::setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_ snprintf(reply, 160, "Err - index 1-%d", FLOOD_CHANNEL_BLOCK_SLOTS); return; } + if (max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL + && max_hops != FLOOD_CHANNEL_BLOCK_HOPS_INHERIT + && (max_hops < 1 || max_hops > 7)) { + strcpy(reply, "Err - bad hops"); + return; + } uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]; deriveFloodChannelBlockPrefix(secret, key_len, prefix); @@ -3186,6 +3251,7 @@ void MyMesh::setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_ clearFloodChannelBlockEntry(entry); entry.active = true; entry.key_len = key_len; + entry.max_hops = max_hops; 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); @@ -3212,8 +3278,24 @@ void MyMesh::formatFloodChannelBlocks(const char* selector, char* reply) { } char* out = reply; - size_t remaining = 160; - int written = snprintf(out, remaining, ">"); + const size_t reply_limit = 150; + size_t remaining = reply_limit; + char hops[8]; + formatFloodChannelBlockHops(hops, _prefs.flood_channel_block_max_hops); + size_t full_len = 2 + strlen(hops); + for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS; i++) { + const auto& entry = flood_channel_blocks[i]; + size_t display_len = entry.active ? strlen(entry.name) : 1; + full_len += 1 + (i + 1 >= 10 ? 2 : 1) + 1 + display_len; + if (entry.active && entry.max_hops != FLOOD_CHANNEL_BLOCK_HOPS_INHERIT) { + char row_hops[8]; + formatFloodChannelBlockHops(row_hops, entry.max_hops); + full_len += 1 + strlen(row_hops); + } + } + bool trim_names = full_len >= reply_limit; + + int written = snprintf(out, remaining, "> %s", hops); if (written < 0 || (size_t)written >= remaining) { reply[0] = 0; return; @@ -3222,18 +3304,30 @@ void MyMesh::formatFloodChannelBlocks(const char* selector, char* reply) { remaining -= written; for (int i = 0; i < FLOOD_CHANNEL_BLOCK_SLOTS && remaining > 1; i++) { - char display[8]; + const char* display = "-"; + char short_display[6]; 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; + if (entry.active) { + char row_hops[8]; + row_hops[0] = 0; + if (entry.max_hops != FLOOD_CHANNEL_BLOCK_HOPS_INHERIT) { + formatFloodChannelBlockHops(row_hops, entry.max_hops); } + if (trim_names) { + StrHelper::strncpy(short_display, entry.name, sizeof(short_display)); + if (strlen(entry.name) >= sizeof(short_display)) { + short_display[sizeof(short_display) - 2] = '~'; + short_display[sizeof(short_display) - 1] = 0; + } + display = short_display; + } else { + display = entry.name; + } + written = snprintf(out, remaining, " %d:%s%s%s", i + 1, display, + row_hops[0] ? "/" : "", row_hops); + } else { + written = snprintf(out, remaining, " %d:%s", i + 1, display); } - written = snprintf(out, remaining, " %d:%s", i + 1, display); if (written < 0 || (size_t)written >= remaining) { out[remaining - 1] = 0; break; @@ -3478,14 +3572,23 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * reply[0] = 0; } else if (strcmp(command, "get outpath") == 0 || strcmp(command, "set outpath") == 0 - || strncmp(command, "set outpath ", 12) == 0) { + || strncmp(command, "set outpath ", 12) == 0 + || strcmp(command, "get altpath") == 0 + || strcmp(command, "set altpath") == 0 + || strncmp(command, "set altpath ", 12) == 0) { bool is_get = strncmp(command, "get ", 4) == 0; + bool is_alt = strncmp(command + 4, "altpath", 7) == 0; if (sender == NULL) { strcpy(reply, "Err - command needs remote client context"); - } else if (is_get) { - formatPathReply(sender->out_path, sender->out_path_len, reply, 160); } else { - char* spec = command + 11; // length of "set outpath" + uint8_t* stored_path = is_alt ? sender->alt_path : sender->out_path; + uint8_t* stored_path_len = is_alt ? &sender->alt_path_len : &sender->out_path_len; + if (is_get) { + formatPathReply(stored_path, *stored_path_len, reply, 160); + return; + } + + char* spec = command + 11; // length of "set outpath" or "set altpath" if (*spec == ' ') spec++; uint8_t path[MAX_PATH_SIZE]; @@ -3495,13 +3598,13 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char * strcpy(reply, err ? err : "Err - invalid path"); } else { if (path_len == OUT_PATH_UNKNOWN || path_len == OUT_PATH_FORCE_FLOOD) { - memset(sender->out_path, 0, sizeof(sender->out_path)); - sender->out_path_len = path_len; + memset(stored_path, 0, MAX_PATH_SIZE); + *stored_path_len = path_len; } else { - sender->out_path_len = mesh::Packet::copyPath(sender->out_path, path, path_len); + *stored_path_len = mesh::Packet::copyPath(stored_path, path, path_len); } dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - formatPathReply(sender->out_path, sender->out_path_len, reply, 160); + formatPathReply(stored_path, *stored_path_len, reply, 160); } } } else if (strncmp(command, "send text.flood ", 16) == 0) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 83485b25..536fc951 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -135,6 +135,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { struct FloodChannelBlockEntry { bool active; uint8_t key_len; + uint8_t max_hops; uint8_t hash_prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]; uint8_t secret[PUB_KEY_SIZE]; char name[FLOOD_CHANNEL_BLOCK_NAME_LEN]; @@ -206,6 +207,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void clearFloodChannelBlockEntry(FloodChannelBlockEntry& entry); void deriveFloodChannelBlockPrefix(const uint8_t* secret, uint8_t key_len, uint8_t prefix[FLOOD_CHANNEL_BLOCK_PREFIX_LEN]) const; + uint8_t resolveFloodChannelBlockHops(uint8_t max_hops) const; + bool floodChannelBlockHopApplies(const mesh::Packet* packet, uint8_t max_hops) const; bool floodChannelBlockMatches(const FloodChannelBlockEntry& entry, const mesh::Packet* packet) const; bool shouldBlockFloodChannelForward(const mesh::Packet* packet) const; int findFloodChannelBlockBySelector(const char* selector) const; @@ -291,6 +294,7 @@ protected: void onControlDataRecv(mesh::Packet* packet) override; void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); + void sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); @@ -342,7 +346,7 @@ public: 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; + const char* name, uint8_t max_hops, char* reply) override; void formatFloodChannelBlocks(const char* selector, char* reply) override; void deleteFloodChannelBlock(const char* selector, char* reply) override; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c43b2948..3127a0ba 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -325,6 +325,98 @@ static void formatFloodRetryPathGate(char* dest, uint8_t path_gate) { } } +static bool parseFloodChannelBlockHops(const char* value, uint8_t& max_hops) { + if (value == NULL) { + return false; + } + value = skipSpacesConst(value); + if (strcmp(value, "all") == 0) { + max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; + return true; + } + return parseUint8Strict(value, 1, 7, max_hops); +} + +static bool parseFloodChannelBlockRowHops(const char* value, uint8_t& max_hops) { + if (value == NULL) { + return false; + } + value = skipSpacesConst(value); + if (strcmp(value, "default") == 0 || strcmp(value, "def") == 0 || strcmp(value, "inherit") == 0) { + max_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; + return true; + } + return parseFloodChannelBlockHops(value, max_hops); +} + +static bool parseFloodChannelBlockHopAssignment(const char* text, bool allow_bare, uint8_t& max_hops) { + char token[16]; + text = skipSpacesConst(text); + if (text == NULL || *text == 0) { + return false; + } + + size_t len = 0; + while (text[len] && text[len] != ' ' && len + 1 < sizeof(token)) { + token[len] = text[len]; + len++; + } + token[len] = 0; + + const char* value = NULL; + if (strncmp(token, "h=", 2) == 0) { + value = token + 2; + } else if (strncmp(token, "hops=", 5) == 0) { + value = token + 5; + } else if (allow_bare) { + value = token; + } else { + return false; + } + return parseFloodChannelBlockRowHops(value, max_hops); +} + +static bool looksFloodChannelBlockHopAssignment(const char* text) { + text = skipSpacesConst(text); + if (text == NULL || *text == 0) { + return false; + } + return (*text >= '0' && *text <= '9') + || strncmp(text, "h=", 2) == 0 + || strncmp(text, "hops=", 5) == 0 + || strncmp(text, "all", 3) == 0 + || strncmp(text, "def", 3) == 0 + || strncmp(text, "default", 7) == 0 + || strncmp(text, "inherit", 7) == 0; +} + +static bool trimFloodChannelBlockHopSuffix(char* name, uint8_t& max_hops) { + size_t len = strlen(name); + while (len > 0 && name[len - 1] == ' ') { + name[--len] = 0; + } + char* token = strrchr(name, ' '); + if (token == NULL) { + return true; + } + if (strncmp(token + 1, "h=", 2) != 0 && strncmp(token + 1, "hops=", 5) != 0) { + return true; + } + if (!parseFloodChannelBlockHopAssignment(token + 1, false, max_hops)) { + return false; + } + *token = 0; + return strlen(name) > 0; +} + +static void formatFloodChannelBlockHops(char* dest, uint8_t max_hops) { + if (max_hops == FLOOD_CHANNEL_BLOCK_HOPS_ALL) { + strcpy(dest, "h=all"); + } else { + sprintf(dest, "h>%u", (unsigned int)max_hops); + } +} + static void formatFloodRetryPrefixList(char* dest, const uint8_t prefixes[][FLOOD_RETRY_PREFIX_LEN], uint8_t max_prefixes) { char* out = dest; @@ -611,6 +703,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _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; + _prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; 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 @@ -645,8 +738,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { 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)); } + if (file.available() >= (int)sizeof(_prefs->flood_channel_block_max_hops)) { + file.read((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops)); + } } - // next: 673 + // next: 674 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -707,6 +803,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _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->flood_channel_block_max_hops != FLOOD_CHANNEL_BLOCK_HOPS_ALL + && (_prefs->flood_channel_block_max_hops < 1 || _prefs->flood_channel_block_max_hops > 7)) { + _prefs->flood_channel_block_max_hops = FLOOD_CHANNEL_BLOCK_HOPS_ALL; + } if (_prefs->battery_alert_low_percent < 1 || _prefs->battery_alert_low_percent > 100 || _prefs->battery_alert_critical_percent >= _prefs->battery_alert_low_percent) { @@ -805,7 +905,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { 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)); file.write((uint8_t *)&_prefs->flood_channel_data_enabled, sizeof(_prefs->flood_channel_data_enabled)); - // next: 673 + file.write((uint8_t *)&_prefs->flood_channel_block_max_hops, sizeof(_prefs->flood_channel_block_max_hops)); + // next: 674 file.close(); } @@ -1461,6 +1562,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, must be on or off"); } + } else if (memcmp(config, "flood.channel.block.hops ", 25) == 0) { + uint8_t max_hops; + if (parseFloodChannelBlockHops(&config[25], max_hops)) { + _prefs->flood_channel_block_max_hops = max_hops; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be all or 1-7"); + } } else if (memcmp(config, "flood.channel.block", 19) == 0 && (config[19] == ' ' || config[19] == '.')) { const char* cursor = &config[19]; @@ -1483,10 +1593,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep key_text[key_len_text] = 0; char name[FLOOD_CHANNEL_BLOCK_NAME_LEN]; + uint8_t block_hops = FLOOD_CHANNEL_BLOCK_HOPS_INHERIT; if (key_text[0] == '#') { StrHelper::strncpy(name, key_text, sizeof(name)); + const char* extra = skipSpacesConst(cursor); + if (*extra && looksFloodChannelBlockHopAssignment(extra) + && !parseFloodChannelBlockHopAssignment(extra, true, block_hops)) { + strcpy(reply, "Error, hops must be all, default, or 1-7"); + return; + } } else { copyTrimmedFloodChannelBlockName(name, sizeof(name), cursor); + if (!trimFloodChannelBlockHopSuffix(name, block_hops)) { + strcpy(reply, "Error, bad name or hops"); + return; + } } uint8_t secret[PUB_KEY_SIZE]; uint8_t decoded_key_len = 0; @@ -1495,7 +1616,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else if (name[0] == 0 || !isValidName(name)) { strcpy(reply, "Error, bad name"); } else { - _callbacks->setFloodChannelBlock(index, secret, decoded_key_len, name, reply); + _callbacks->setFloodChannelBlock(index, secret, decoded_key_len, name, block_hops, reply); } } else if (memcmp(config, "flood.retry.count ", 18) == 0) { int attempts = looksUnsignedInteger(&config[18]) ? _atoi(&config[18]) : -1; @@ -1820,7 +1941,13 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } 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"); + char hops[8]; + formatFloodChannelBlockHops(hops, _prefs->flood_channel_block_max_hops); + sprintf(reply, "> %s %s", _prefs->flood_channel_data_enabled ? "on" : "off", hops); + } else if (memcmp(config, "flood.channel.block.hops", 24) == 0) { + char hops[8]; + formatFloodChannelBlockHops(hops, _prefs->flood_channel_block_max_hops); + sprintf(reply, "> %s", hops); } else if (memcmp(config, "flood.channel.block", 19) == 0 && (config[19] == 0 || config[19] == ' ' || config[19] == '.')) { const char* cursor = &config[19]; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 7cfef4d4..b9943481 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -87,6 +87,8 @@ #ifndef FLOOD_CHANNEL_BLOCK_PREFIX_LEN #define FLOOD_CHANNEL_BLOCK_PREFIX_LEN 4 #endif +#define FLOOD_CHANNEL_BLOCK_HOPS_ALL 0xFF +#define FLOOD_CHANNEL_BLOCK_HOPS_INHERIT 0xFE #define DIRECT_RETRY_CR4_MIN_SNR_X4_DEFAULT 40 #define DIRECT_RETRY_CR5_MIN_SNR_X4_DEFAULT 30 @@ -167,6 +169,7 @@ struct NodePrefs { // persisted to file uint8_t battery_alert_critical_percent; uint8_t direct_retry_recent_enabled; uint8_t flood_channel_data_enabled; + uint8_t flood_channel_block_max_hops; }; class CommonCLICallbacks { @@ -228,11 +231,12 @@ public: strcpy(reply, "Error: unsupported"); } virtual void setFloodChannelBlock(int index, const uint8_t* secret, uint8_t key_len, - const char* name, char* reply) { + const char* name, uint8_t max_hops, char* reply) { (void)index; (void)secret; (void)key_len; (void)name; + (void)max_hops; strcpy(reply, "Error: unsupported"); } virtual void formatFloodChannelBlocks(const char* selector, char* reply) {