diff --git a/docs/cli_commands.md b/docs/cli_commands.md index aac545383a..395deb46cd 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -712,6 +712,40 @@ 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. + +**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 +**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 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. + +--- + +#### 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` 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` + +**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 diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 24e8894927..0fb486b521 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,41 @@ 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 auto-tagging is enabled and a home region is configured + RegionEntry* home = region_map.getHomeRegion(); + 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] == '$') { + // 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; @@ -871,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/Packet.h b/src/Packet.h index 0886a06c4e..ccd2da890f 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) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 47a2592bc4..81fc5764fd 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -87,6 +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 + 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 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 // next: 291 @@ -118,6 +121,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, REGION_AUTOTAG_MAX_HOPS_LIMIT); file.close(); } @@ -178,6 +183,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 + 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.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 // next: 291 @@ -356,6 +364,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"); @@ -616,6 +628,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 <= REGION_AUTOTAG_MAX_HOPS_LIMIT) { + _prefs->region_autotag_max_hops = (uint8_t)h; + savePrefs(); + strcpy(reply, "OK"); + } else { + 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; + 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..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 @@ -60,6 +62,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 {