From 85a3b1d6dbd2996707abb801a9a41510b58d5029 Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 17 Mar 2026 15:59:52 +0000 Subject: [PATCH 1/6] feat: repeater VLAN-style region tagging for untagged flood packets Repeaters with a home region configured now automatically tag untagged flood packets (ROUTE_TYPE_FLOOD) with the home region's transport code, converting them to ROUTE_TYPE_TRANSPORT_FLOOD. Also adds TRANSPORT_CODE_ALL (0xFFFF) as a reserved code that bypasses region filtering, allowing mesh-wide flooding when explicitly requested. --- examples/simple_repeater/MyMesh.cpp | 38 ++++++++++++++++++++++++++--- src/Packet.h | 2 ++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..0c8318f717 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -416,7 +416,9 @@ bool MyMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; - if (packet->isRouteFlood() && recv_pkt_region == NULL) { + if (packet->isRouteFlood() && packet->hasTransportCodes() && packet->transport_codes[0] == TRANSPORT_CODE_ALL) { + // ALL region: always forward regardless of region config + } else if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); return false; } @@ -535,12 +537,40 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { // just try to determine region for packet (apply later in allowPacketForward()) if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) { - recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + if (pkt->transport_codes[0] == TRANSPORT_CODE_ALL) { + recv_pkt_region = ®ion_map.getWildcard(); // ALL: always allow + } else { + recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); + } } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { - if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { + // untagged packet: tag with home region if one is configured + RegionEntry* home = region_map.getHomeRegion(); + if (home && home->id != 0) { + // calculate transport code for home region and stamp onto packet + TransportKey key; + if (home->name[0] == '$') { + // private region: load key from store + if (key_store.loadKeysFor(home->id, &key, 1) < 1) { + recv_pkt_region = NULL; + return false; + } + } else if (home->name[0] == '#') { + key_store.getAutoKeyFor(home->id, home->name, key); + } else { + char tmp[sizeof(home->name) + 1]; + tmp[0] = '#'; + strcpy(&tmp[1], home->name); + key_store.getAutoKeyFor(home->id, tmp, key); + } + pkt->transport_codes[0] = key.calcTransportCode(pkt); + pkt->transport_codes[1] = 0; + pkt->header = (pkt->header & ~PH_ROUTE_MASK) | ROUTE_TYPE_TRANSPORT_FLOOD; + + recv_pkt_region = home; + } else if (region_map.getWildcard().flags & REGION_DENY_FLOOD) { recv_pkt_region = NULL; } else { - recv_pkt_region = ®ion_map.getWildcard(); + recv_pkt_region = ®ion_map.getWildcard(); } } else { recv_pkt_region = NULL; diff --git a/src/Packet.h b/src/Packet.h index 7861954618..f566e3d392 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -16,6 +16,8 @@ namespace mesh { #define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied #define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 // direct route + transport codes +#define TRANSPORT_CODE_ALL 0xFFFF // special transport code: forward to all regions + #define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) #define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) #define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) From d93746e257a9391f3239659f03006c05872d4cdb Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 14 Apr 2026 10:26:18 +0100 Subject: [PATCH 2/6] feat: make region auto-tagging opt-in with configurable hop limit Adds two new repeater prefs to give admins explicit control over VLAN-style region tagging of untagged flood packets: region.autotag on|off (default: off) region.autotag.max.hops 0-8 (default: 1) Auto-tagging now only occurs when the setting is enabled AND the received packet's path hash count is <= the configured max hops. This prevents mis-tagging distant-origin floods that arrived via untagged / older-firmware repeaters, and ensures home-region selection is a deliberate admin decision rather than an implicit side-effect of configuring a home region. --- examples/simple_repeater/MyMesh.cpp | 7 +++++-- src/helpers/CommonCLI.cpp | 27 +++++++++++++++++++++++++-- src/helpers/CommonCLI.h | 2 ++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 0c8318f717..0fb486b521 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -543,9 +543,10 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD); } } else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) { - // untagged packet: tag with home region if one is configured + // untagged packet: tag with home region if auto-tagging is enabled and a home region is configured RegionEntry* home = region_map.getHomeRegion(); - if (home && home->id != 0) { + if (_prefs.region_autotag && home && home->id != 0 + && pkt->getPathHashCount() <= _prefs.region_autotag_max_hops) { // calculate transport code for home region and stamp onto packet TransportKey key; if (home->name[0] == '$') { @@ -901,6 +902,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_interval = 1; // default to 2 minutes for NEW installs _prefs.flood_advert_interval = 12; // 12 hours _prefs.flood_max = 64; + _prefs.region_autotag = 0; // opt-in, default off + _prefs.region_autotag_max_hops = 1; // only tag zero-hop / 1-hop packets by default _prefs.interference_threshold = 0; // disabled // bridge defaults diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2f7a0fffcb..3c91af2160 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -87,7 +87,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // next: 290 + file.read((uint8_t *)&_prefs->region_autotag, sizeof(_prefs->region_autotag)); // 290 + file.read((uint8_t *)&_prefs->region_autotag_max_hops, sizeof(_prefs->region_autotag_max_hops)); // 291 + // next: 292 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -117,6 +119,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean + _prefs->region_autotag = constrain(_prefs->region_autotag, 0, 1); // boolean + _prefs->region_autotag_max_hops = constrain(_prefs->region_autotag_max_hops, 0, 8); file.close(); } @@ -177,7 +181,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - // next: 290 + file.write((uint8_t *)&_prefs->region_autotag, sizeof(_prefs->region_autotag)); // 290 + file.write((uint8_t *)&_prefs->region_autotag_max_hops, sizeof(_prefs->region_autotag_max_hops)); // 291 + // next: 292 file.close(); } @@ -349,6 +355,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch *reply = 0; // set null terminator } else if (memcmp(config, "path.hash.mode", 14) == 0) { sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); + } else if (memcmp(config, "region.autotag.max.hops", 23) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->region_autotag_max_hops); + } else if (memcmp(config, "region.autotag", 14) == 0) { + sprintf(reply, "> %s", _prefs->region_autotag ? "on" : "off"); } else if (memcmp(config, "loop.detect", 11) == 0) { if (_prefs->loop_detect == LOOP_DETECT_OFF) { strcpy(reply, "> off"); @@ -597,6 +607,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch *dp = 0; savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "region.autotag.max.hops ", 24) == 0) { + int h = atoi(&config[24]); + if (h >= 0 && h <= 8) { + _prefs->region_autotag_max_hops = (uint8_t)h; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, range is 0-8"); + } + } else if (memcmp(config, "region.autotag ", 15) == 0) { + _prefs->region_autotag = memcmp(&config[15], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); } else if (memcmp(config, "path.hash.mode ", 15) == 0) { config += 15; uint8_t mode = atoi(config); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3a4332d1f2..f0bd2aba6d 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -60,6 +60,8 @@ struct NodePrefs { // persisted to file uint8_t rx_boosted_gain; // power settings uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; + uint8_t region_autotag; // boolean: auto-tag untagged flood packets with home region's transport code + uint8_t region_autotag_max_hops; // only auto-tag packets received with pathHashCount <= this value (0 = zero-hop only) }; class CommonCLICallbacks { From 44c7023878ff4bd2e29e97fb4fbccf7479881fcf Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 14 Apr 2026 10:30:19 +0100 Subject: [PATCH 3/6] docs: document region auto-tagging controls and home-region selection Adds CLI reference entries for the new region.autotag and region.autotag.max.hops settings, and expands the region home documentation with guidance on how to choose a home region so that auto-tagged flood packets are scoped correctly. Also notes the reserved TRANSPORT_CODE_ALL (0xFFFF) bypass behaviour. --- docs/cli_commands.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index aac545383a..7869f0286d 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -712,6 +712,36 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `name`: Region name +**Note:** The home region is used by the auto-tag feature (see `region.autotag` below) to stamp a transport code onto untagged flood packets this repeater receives, converting them from `ROUTE_TYPE_FLOOD` to `ROUTE_TYPE_TRANSPORT_FLOOD`. When selecting a home region, choose the **most specific region in the local region hierarchy that includes all nodes this repeater can possibly hear** (including via hops you intend to cover — see `region.autotag.max.hops`). Choosing a home region that is too narrow will cause unscoped packets originating from neighbouring regions to be tagged incorrectly; choosing one that is too broad defeats the purpose of scoping. + +--- + +#### View or change whether this repeater auto-tags untagged flood packets +**Usage:** +- `get region.autotag` +- `set region.autotag ` + +**Parameters:** +- `state`: `on` (enable) or `off` (disable) + +**Default:** `off` + +**Note:** When enabled, the repeater stamps its home region's transport code onto untagged flood packets (`ROUTE_TYPE_FLOOD`) it receives, converting them to `ROUTE_TYPE_TRANSPORT_FLOOD` before re-broadcast. This provides VLAN-style scoping of legacy / un-scoped traffic, but requires a home region to be configured (see `region home`). Because mis-tagging is possible when the repeater can hear traffic originating outside its home region, this feature is opt-in. See also `region.autotag.max.hops` to limit how far a packet may have travelled before becoming eligible for auto-tagging. The reserved transport code `0xFFFF` (TRANSPORT_CODE_ALL) is always forwarded regardless of local region configuration, allowing explicit mesh-wide flooding when a sender requests it. + +--- + +#### View or change the max hop count for auto-tagging +**Usage:** +- `get region.autotag.max.hops` +- `set region.autotag.max.hops ` + +**Parameters:** +- `value`: Maximum path hash count (0-8). `0` means only auto-tag packets received directly (zero-hop); higher values also auto-tag packets that already traversed that many repeaters. + +**Default:** `1` + +**Note:** Only applies when `region.autotag` is `on`. Keep this small (0-2) unless you are certain no untagged / older-firmware repeaters exist within that many hops, otherwise distant-origin traffic forwarded through them may be tagged with the wrong region. + --- #### Create a new region From a06984bc81836c1b357785379f9cf137d968b999 Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 14 Apr 2026 16:36:00 +0100 Subject: [PATCH 4/6] docs: address review feedback on region auto-tag docs - Drop VLAN analogy; describe the behaviour directly so the docs stand on their own. - Clarify region.autotag.max.hops value description to refer to "packets without scope" explicitly. --- docs/cli_commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 7869f0286d..1cbbc14e75 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -726,7 +726,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** `off` -**Note:** When enabled, the repeater stamps its home region's transport code onto untagged flood packets (`ROUTE_TYPE_FLOOD`) it receives, converting them to `ROUTE_TYPE_TRANSPORT_FLOOD` before re-broadcast. This provides VLAN-style scoping of legacy / un-scoped traffic, but requires a home region to be configured (see `region home`). Because mis-tagging is possible when the repeater can hear traffic originating outside its home region, this feature is opt-in. See also `region.autotag.max.hops` to limit how far a packet may have travelled before becoming eligible for auto-tagging. The reserved transport code `0xFFFF` (TRANSPORT_CODE_ALL) is always forwarded regardless of local region configuration, allowing explicit mesh-wide flooding when a sender requests it. +**Note:** When enabled, the repeater stamps its home region's transport code onto untagged flood packets (`ROUTE_TYPE_FLOOD`) it receives, converting them to `ROUTE_TYPE_TRANSPORT_FLOOD` before re-broadcast. This scopes legacy / un-scoped traffic into the configured home region, but requires a home region to be configured (see `region home`). Because mis-tagging is possible when the repeater can hear traffic originating outside its home region, this feature is opt-in. See also `region.autotag.max.hops` to limit how far a packet may have travelled before becoming eligible for auto-tagging. The reserved transport code `0xFFFF` (TRANSPORT_CODE_ALL) is always forwarded regardless of local region configuration, allowing explicit mesh-wide flooding when a sender requests it. --- @@ -736,7 +736,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set region.autotag.max.hops ` **Parameters:** -- `value`: Maximum path hash count (0-8). `0` means only auto-tag packets received directly (zero-hop); higher values also auto-tag packets that already traversed that many repeaters. +- `value`: Maximum path hash count (0-8). `0` means only auto-tag packets without scope received directly (zero-hop); higher values also auto-tag packets without scope that already traversed that many repeaters. **Default:** `1` From e5d9eb7c67dd459424d3ac3285b907dc0a826da8 Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 14 Apr 2026 16:37:40 +0100 Subject: [PATCH 5/6] docs: explain region home behaviour when unset Clarify that with no home region configured, auto-tagging is effectively disabled regardless of the region.autotag setting, and untagged floods fall back to the wildcard region's flood permission. --- docs/cli_commands.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1cbbc14e75..116a49145a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -714,6 +714,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Note:** The home region is used by the auto-tag feature (see `region.autotag` below) to stamp a transport code onto untagged flood packets this repeater receives, converting them from `ROUTE_TYPE_FLOOD` to `ROUTE_TYPE_TRANSPORT_FLOOD`. When selecting a home region, choose the **most specific region in the local region hierarchy that includes all nodes this repeater can possibly hear** (including via hops you intend to cover — see `region.autotag.max.hops`). Choosing a home region that is too narrow will cause unscoped packets originating from neighbouring regions to be tagged incorrectly; choosing one that is too broad defeats the purpose of scoping. +**When no home region is set:** the repeater has no scope to apply, so auto-tagging is effectively disabled regardless of the `region.autotag` setting — untagged flood packets are forwarded based on the wildcard (`*`) region's flood permission only. To participate in auto-tagging, both a home region must be configured here **and** `region.autotag` must be `on`. + --- #### View or change whether this repeater auto-tags untagged flood packets From b2d80391bb74b5887acb074dd7b533b330fce71c Mon Sep 17 00:00:00 2001 From: Dale Ruane Date: Tue, 14 Apr 2026 16:56:14 +0100 Subject: [PATCH 6/6] refactor: extract region.autotag.max.hops cap into a named constant Replace the magic 8 used in both the load-time sanitize and the CLI set handler with REGION_AUTOTAG_MAX_HOPS_LIMIT, and surface the cap in the "Error, range is 0-N" reply so the CLI and the constant can never drift apart. Also expand the docs to call out the range explicitly, the clamp-on- load behaviour, and the rationale for the 8 upper bound (well below flood.max so auto-tagging can't silently scope distant traffic). --- docs/cli_commands.md | 4 +++- src/helpers/CommonCLI.cpp | 6 +++--- src/helpers/CommonCLI.h | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 116a49145a..395deb46cd 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -738,7 +738,9 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set region.autotag.max.hops ` **Parameters:** -- `value`: Maximum path hash count (0-8). `0` means only auto-tag packets without scope received directly (zero-hop); higher values also auto-tag packets without scope that already traversed that many repeaters. +- `value`: Maximum path hash count. `0` means only auto-tag packets without scope received directly (zero-hop); higher values also auto-tag packets without scope that already traversed that many repeaters. + +**Range:** `0` to `8` (inclusive). Values outside this range are rejected by `set` and clamped to this range on load. The upper bound of `8` is intentionally well below the default `flood.max` of `64`, because auto-tagging packets from far across the mesh almost always produces incorrect region assignments — the limit exists to keep admins honest about the geographic scope they can actually account for. **Default:** `1` diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2d348f8f50..81fc5764fd 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -122,7 +122,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->region_autotag = constrain(_prefs->region_autotag, 0, 1); // boolean - _prefs->region_autotag_max_hops = constrain(_prefs->region_autotag_max_hops, 0, 8); + _prefs->region_autotag_max_hops = constrain(_prefs->region_autotag_max_hops, 0, REGION_AUTOTAG_MAX_HOPS_LIMIT); file.close(); } @@ -630,12 +630,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "OK"); } else if (memcmp(config, "region.autotag.max.hops ", 24) == 0) { int h = atoi(&config[24]); - if (h >= 0 && h <= 8) { + if (h >= 0 && h <= REGION_AUTOTAG_MAX_HOPS_LIMIT) { _prefs->region_autotag_max_hops = (uint8_t)h; savePrefs(); strcpy(reply, "OK"); } else { - strcpy(reply, "Error, range is 0-8"); + sprintf(reply, "Error, range is 0-%d", REGION_AUTOTAG_MAX_HOPS_LIMIT); } } else if (memcmp(config, "region.autotag ", 15) == 0) { _prefs->region_autotag = memcmp(&config[15], "on", 2) == 0; diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f0bd2aba6d..72c200c06a 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -13,6 +13,8 @@ #define ADVERT_LOC_SHARE 1 #define ADVERT_LOC_PREFS 2 +#define REGION_AUTOTAG_MAX_HOPS_LIMIT 8 // upper bound for region.autotag.max.hops pref + #define LOOP_DETECT_OFF 0 #define LOOP_DETECT_MINIMAL 1 #define LOOP_DETECT_MODERATE 2