diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b618ae2b..a9392bd6 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -129,6 +129,26 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Discover the regions of zero hop neighbors + +**Usage:** +- `discover.regions` +- `discover.regions list` + +**Note:** Asks each neighbor in turn which regions it floods, one request at a time. The reply +is `OK - querying neighbors`; results arrive over the following seconds and are printed to +the serial terminal as they land. + +**Note:** `discover.regions list` reports the results of the last run, as `{pubkey-prefix}:{regions}` +per line. A `?` in place of the regions means that neighbor did not answer — it may be out of range, +running older firmware, or simply rate-limiting anonymous requests (repeaters answer at most 4 per +3 minutes). This reply is capped at 134 characters, so on a node with many neighbors it may not +show them all — the serial output always does. + +**Note:** The pubkey prefixes reported here can be passed straight to `regions.subscribe`. + +--- + ## Statistics ### Clear Stats @@ -856,6 +876,48 @@ region save --- +#### Subscribe to the regions of another repeater (Repeater Only) +**Usage:** +- `regions.subscribe` +- `regions.subscribe ` +- `regions.subscribe now` +- `regions.subscribe off` + +**Parameters:** +- `pubkey`: Public key of the repeater to subscribe to. A prefix (4 bytes or more) is accepted if + that node is a current neighbor, such as the prefixes reported by `discover.regions`; otherwise + give the full key. + +**Note:** The node asks the subscribed repeater which regions it floods, and adds any it does not +already have. This is **additive**: a region already on this node is left exactly as it is, keeping +its flags and its place in the hierarchy, so a region you have deliberately denied stays denied. +New regions are added as flood-allowed children of the wildcard `*`, and are saved automatically. + +**Note:** The subscribed node must be within direct radio range — the request and its reply are both +sent zero-hop, so a repeater that is only reachable over several hops will never answer. A node also +answers at most 4 anonymous requests every 3 minutes, so a fetch issued right after `discover.regions` +can go unanswered. Unanswered fetches are retried after 1 minute for the first 5 attempts, and every +12 hours after that until one succeeds; `regions.subscribe` reports the failure count and the next +retry. + +**Note:** The fetch repeats every 72 hours (2 minutes after boot, and immediately when the +subscription is set or `regions.subscribe now` is used), so new regions added to the subscribed node +are picked up. Nothing is ever removed by this command. `region remove` deletes a region locally, but +if the subscribed node still floods it, the next fetch adds it back as flood-allowed — to drop it for +good, either `regions.subscribe off` first, or keep the region and `region denyf` it, which a fetch +will not override. + +**Note:** A reply carries at most ~158 characters of region names. A subscribed node with a larger +map has its list truncated at a name boundary, and the names past that point are not inherited. + +**Note:** Private (`$`) regions are skipped, since their transport keys are not derived from the name. + +**Note:** Bare `regions.subscribe` reports the subscription and the result of the last fetch, for +example `OK - subscribed to a1b2c3d4 (2 added, 5 known, 43 secs ago)`. `regions.subscribe off` +clears it and replies `OK - unsubscribed`. + +--- + #### Remove a region **Usage:** - `region remove ` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 09f74cbe..73492af4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -52,6 +52,10 @@ #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ +// createDatagram() rejects a reply longer than this, once the MAC and the cipher block +// padding are added. The exporters below MUST keep within it, or no reply is sent at all. +#define MAX_ANON_REPLY_LEN (MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1)) + #define ANON_REQ_TYPE_REGIONS 0x01 #define ANON_REQ_TYPE_OWNER 0x02 #define ANON_REQ_TYPE_BASIC 0x03 // just remote clock @@ -60,6 +64,20 @@ #define LAZY_CONTACTS_WRITE_DELAY 5000 +#define REGION_QUERY_NONE 0 +#define REGION_QUERY_DISCOVER 1 // 'discover.regions' pass over the neighbours table +#define REGION_QUERY_SUBSCRIBE 2 // scheduled fetch from the subscribed node + +#define REGION_QUERY_TIMEOUT 20000 // millis to wait for one regions reply +#define REGION_FETCH_START_DELAY 120000 // first fetch after boot +#define REGION_FETCH_INTERVAL (72*3600000) // between successful fetches +#define REGION_FETCH_RETRY 60000 // after an unanswered fetch +#define REGION_FETCH_MAX_TRIES 5 // ..then back off to: +#define REGION_FETCH_BACKOFF (12*3600000) // ..until one succeeds + +// peer index (see searchPeersByHash) for the node we are querying for regions +#define REGION_QUERY_PEER_IDX 1000 + void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled // find existing neighbour, else use least recently updated @@ -158,7 +176,9 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send uint32_t now = getRTCClock()->getCurrentTime(); memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness) - return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); // reply length + // NOTE: a region map that doesn't fit is truncated at a name boundary. Asking for more + // than one packet's worth would need a paged request, which this reply has no room for. + return 8 + region_map.exportNamesTo((char *) &reply_data[8], MAX_ANON_REPLY_LEN - 8, REGION_DENY_FLOOD); // reply length } return 0; } @@ -607,17 +627,26 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m int MyMesh::searchPeersByHash(const uint8_t *hash) { int n = 0; - for (int i = 0; i < acl.getNumClients(); i++) { + for (int i = 0; i < acl.getNumClients() && n < MAX_CLIENTS; i++) { if (acl.getClientByIdx(i)->id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } } + // the node we are querying for regions is not (necessarily) a client, so match it + // separately, to be able to decode its reply. MUST come after the clients: the caller + // stops at the first match that decrypts, and a client that IS the queried node has the + // same shared secret, so matching it here first would swallow its normal traffic. + if (region_query_mode != REGION_QUERY_NONE && region_query_id.isHashMatch(hash) && n < MAX_CLIENTS) { + matching_peer_indexes[n++] = REGION_QUERY_PEER_IDX; + } return n; } void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < acl.getNumClients()) { + if (i == REGION_QUERY_PEER_IDX) { + self_id.calcSharedSecret(dest_secret, region_query_id); // not a client, so calculate it now + } else if (i >= 0 && i < acl.getNumClients()) { // lookup pre-calculated shared_secret memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE); } else { @@ -648,12 +677,23 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { int i = matching_peer_indexes[sender_idx]; + if (i == REGION_QUERY_PEER_IDX) { // a reply to our regions query (not a client packet) + if (type == PAYLOAD_TYPE_RESPONSE) handleRegionsResponse(data, len); + return; + } if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); return; } ClientInfo* client = acl.getClientByIdx(i); + // a node we are querying for regions can ALSO be a client, and then resolves to a client + // index (see searchPeersByHash), so its reply lands here instead + if (type == PAYLOAD_TYPE_RESPONSE && region_query_mode != REGION_QUERY_NONE + && client->id.matches(region_query_id) && handleRegionsResponse(data, len)) { + return; + } + if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) uint32_t timestamp; memcpy(×tamp, data, 4); @@ -843,6 +883,214 @@ void MyMesh::sendNodeDiscoverReq() { } } +// Expand a pubkey prefix to the full key of a heard neighbour, so that operators can +// use the (short) keys that 'discover.regions' reports. +bool MyMesh::resolveNeighbourPubKey(uint8_t *pubkey, int prefix_len) { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + auto neighbour = &neighbours[i]; + if (neighbour->heard_timestamp > 0 && neighbour->id.isHashMatch(pubkey, prefix_len)) { + memcpy(pubkey, neighbour->id.pub_key, PUB_KEY_SIZE); + return true; + } + } +#endif + return false; // not found +} + +void MyMesh::formatRegionSubscribeReply(char *reply) { + if (!isRegionSrcSet()) { + strcpy(reply, "OK - not subscribed"); + return; + } + + char hex[10]; + mesh::Utils::toHex(hex, _prefs.region_src_pubkey, 4); + if (region_query_mode == REGION_QUERY_SUBSCRIBE) { + sprintf(reply, "OK - subscribed to %s (querying now)", hex); + } else if (region_fetch_fails > 0) { + long remaining = (long)(next_region_fetch - futureMillis(0)); // can be due already + sprintf(reply, "OK - subscribed to %s (%d failed, retry in %d secs)", hex, + (int) region_fetch_fails, remaining > 0 ? (uint32_t) remaining / 1000 : 0); + } else if (region_fetch_at == 0) { + sprintf(reply, "OK - subscribed to %s (no reply yet)", hex); + } else { + uint32_t secs_ago = getRTCClock()->getCurrentTime() - region_fetch_at; + sprintf(reply, "OK - subscribed to %s (%d added, %d known, %d secs ago)", hex, + (int) region_fetch_added, (int) region_fetch_known, secs_ago); + } +} + +bool MyMesh::isRegionSrcSet() const { + for (int i = 0; i < PUB_KEY_SIZE; i++) { + if (_prefs.region_src_pubkey[i] != 0) return true; + } + return false; // all zeroes means 'not configured' +} + +// Client side of the anon-regions request (handleAnonRegionsReq is the server side). +// Asks 'target' which regions it floods. Only ONE of these is ever in flight. +bool MyMesh::sendRegionsReq(const mesh::Identity &target) { + uint8_t secret[PUB_KEY_SIZE]; + self_id.calcSharedSecret(secret, target); + + region_query_tag = getRTCClock()->getCurrentTimeUnique(); + + uint8_t inner[6]; + memcpy(inner, ®ion_query_tag, 4); // tag, so we can match up the reply + inner[4] = ANON_REQ_TYPE_REGIONS; + inner[5] = 0; // reply-path len, zero == reply direct to us (we are a neighbour) + + auto pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, target, secret, inner, sizeof(inner)); + if (pkt == NULL) return false; + + region_query_id = target; + region_query_until = futureMillis(REGION_QUERY_TIMEOUT); + sendDirect(pkt, NULL, 0, 0); + return true; +} + +// The reply payload is: {tag}{their-clock}{comma separated region names} +bool MyMesh::handleRegionsResponse(const uint8_t *data, size_t len) { + if (region_query_mode == REGION_QUERY_NONE || len < 8) return false; + + uint32_t tag; + memcpy(&tag, data, 4); + if (tag != region_query_tag) return false; // not the reply we are waiting for + + if (region_query_mode == REGION_QUERY_SUBSCRIBE) { + mergeSubscribedRegions(&data[8], len - 8); + region_query_mode = REGION_QUERY_NONE; + next_region_fetch = futureMillis(REGION_FETCH_INTERVAL); + } else { + advanceRegionDiscover(&data[8], len - 8); + } + return true; +} + +// Merge the regions of the subscribed node into our region map. The merge itself is +// additive (see RegionMap::importNamesFrom), so the operator's own flags and hierarchy +// always win. +void MyMesh::mergeSubscribedRegions(const uint8_t *names, size_t len) { + int known = 0; + int added = region_map.importNamesFrom((const char *) names, (int) len, &known); + + region_fetch_at = getRTCClock()->getCurrentTime(); + region_fetch_added = added; + region_fetch_known = known; + region_fetch_fails = 0; + + if (added > 0) { // only touch the filesystem when the map actually changed + _prefs.discovery_mod_timestamp = region_fetch_at; + savePrefs(); + saveRegions(); + } + MESH_DEBUG_PRINTLN("mergeSubscribedRegions: %d added, %d already known", added, known); +} + +void MyMesh::startRegionDiscover(char *reply) { + if (region_query_mode != REGION_QUERY_NONE) { + strcpy(reply, "Err - busy, try again shortly"); + return; + } +#if MAX_NEIGHBOURS + region_query_mode = REGION_QUERY_DISCOVER; + region_discover_next = 0; + region_discover_reply[0] = 0; + advanceRegionDiscover(NULL, 0); // query the first neighbour + + if (region_query_mode == REGION_QUERY_NONE) { + strcpy(reply, "Err - no neighbors heard yet"); + } else { + strcpy(reply, "OK - querying neighbors"); + } +#else + strcpy(reply, "Err - neighbors not enabled"); +#endif +} + +// Record the result for the neighbour we just queried ('names' is NULL if it didn't +// answer), then move on to the next one. Queries are sent one at a time. +void MyMesh::advanceRegionDiscover(const uint8_t *names, size_t len) { +#if MAX_NEIGHBOURS + if (region_discover_next > 0) { // ..we have a result to record + char entry[64]; + char hex[10]; + mesh::Utils::toHex(hex, region_query_id.pub_key, 4); + + char *dp = entry + sprintf(entry, "%s:", hex); + if (names == NULL) { + *dp++ = '?'; // no reply + } else { + // stop at the cipher block padding, else it is counted as part of the entry + for (size_t i = 0; i < len && names[i] != 0 && dp - entry < (int)sizeof(entry) - 1; i++) { + *dp++ = names[i]; + } + } + *dp = 0; + Serial.printf("regions: %s\n", entry); // ..for the serial CLI, as they arrive + + int used = strlen(region_discover_reply); + if (used + 1 + (dp - entry) < (int) sizeof(region_discover_reply)) { // else drop, reply is full + if (used > 0) { region_discover_reply[used++] = '\n'; } + strcpy(®ion_discover_reply[used], entry); + } + } + + while (region_discover_next < MAX_NEIGHBOURS) { + auto neighbour = &neighbours[region_discover_next++]; + if (neighbour->heard_timestamp > 0 && sendRegionsReq(neighbour->id)) return; + } +#endif + region_query_mode = REGION_QUERY_NONE; // no more neighbours to query +} + +// Forget the configured source: any in-flight request for it is abandoned, and the +// last result no longer describes what we have. +void MyMesh::cancelRegionSubscribe() { + if (region_query_mode == REGION_QUERY_SUBSCRIBE) { + region_query_mode = REGION_QUERY_NONE; + } + next_region_fetch = 0; + region_fetch_at = 0; + region_fetch_fails = 0; +} + +// A request can go unanswered because it was lost, or because the other node is +// rate-limiting anon requests (it allows only a few per minute). Retry soon for the first +// few attempts, then keep retrying on the backoff (which is shorter than the interval +// between successful fetches, so a node that was down is picked up again sooner). +void MyMesh::scheduleRegionFetchRetry() { + if (region_fetch_fails < 0xFF) region_fetch_fails++; + next_region_fetch = futureMillis( + region_fetch_fails < REGION_FETCH_MAX_TRIES ? REGION_FETCH_RETRY : REGION_FETCH_BACKOFF); + MESH_DEBUG_PRINTLN("regions fetch failed (attempt %d)", (uint32_t) region_fetch_fails); +} + +void MyMesh::startRegionFetch() { + mesh::Identity source(_prefs.region_src_pubkey); + if (sendRegionsReq(source)) { + region_query_mode = REGION_QUERY_SUBSCRIBE; + } else { + scheduleRegionFetchRetry(); + } +} + +void MyMesh::loopRegionQuery() { + if (region_query_mode != REGION_QUERY_NONE) { + if (!millisHasNowPassed(region_query_until)) return; // still waiting for the reply + + if (region_query_mode == REGION_QUERY_SUBSCRIBE) { + region_query_mode = REGION_QUERY_NONE; + scheduleRegionFetchRetry(); + } else { + advanceRegionDiscover(NULL, 0); + } + } else if (next_region_fetch && millisHasNowPassed(next_region_fetch) && isRegionSrcSet()) { + startRegionFetch(); + } +} + MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng, mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), @@ -866,6 +1114,13 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _logging = false; region_load_active = false; recv_pkt_region = NULL; + region_query_mode = REGION_QUERY_NONE; + region_query_until = 0; + region_discover_next = 0; + region_discover_reply[0] = 0; + next_region_fetch = 0; + region_fetch_at = 0; + region_fetch_added = region_fetch_known = region_fetch_fails = 0; #if MAX_NEIGHBOURS memset(neighbours, 0, sizeof(neighbours)); @@ -970,6 +1225,10 @@ void MyMesh::begin(FILESYSTEM *fs) { updateAdvertTimer(); updateFloodAdvertTimer(); + if (isRegionSrcSet()) { // give the mesh time to settle before the first fetch + next_region_fetch = futureMillis(REGION_FETCH_START_DELAY); + } + board.setAdcMultiplier(_prefs.adc_multiplier); #if ENV_INCLUDE_GPS == 1 @@ -1256,6 +1515,52 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply sendNodeDiscoverReq(); strcpy(reply, "OK - Discover sent"); } + } else if (memcmp(command, "discover.regions", 16) == 0) { // format: discover.regions [list] + const char* sub = command + 16; + while (*sub == ' ') sub++; + if (*sub == 0) { + startRegionDiscover(reply); + } else if (strcmp(sub, "list") == 0) { + strcpy(reply, region_discover_reply[0] ? region_discover_reply : "-none-"); + } else { + strcpy(reply, "Err - unknown option"); + } + } else if (memcmp(command, "regions.subscribe", 17) == 0) { // format: regions.subscribe [{pubkey-hex}|off|now] + char* sub = command + 17; + while (*sub == ' ') sub++; + + if (*sub == 0) { + formatRegionSubscribeReply(reply); + } else if (strcmp(sub, "off") == 0) { + memset(_prefs.region_src_pubkey, 0, PUB_KEY_SIZE); + cancelRegionSubscribe(); + savePrefs(); + strcpy(reply, "OK - unsubscribed"); + } else if (strcmp(sub, "now") == 0) { + if (isRegionSrcSet()) { + next_region_fetch = futureMillis(1); + strcpy(reply, "OK - fetching"); + } else { + strcpy(reply, "Err - not subscribed"); + } + } else { + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = strlen(sub); + if (hex_len < 8 || hex_len > PUB_KEY_SIZE*2 || (hex_len & 1) + || !mesh::Utils::fromHex(pubkey, hex_len / 2, sub)) { + strcpy(reply, "Err - bad pubkey"); + } else if (hex_len < PUB_KEY_SIZE*2 && !resolveNeighbourPubKey(pubkey, hex_len / 2)) { + strcpy(reply, "Err - not a known neighbor, need full pubkey"); + } else if (self_id.matches(pubkey)) { + strcpy(reply, "Err - that is this node"); + } else { + cancelRegionSubscribe(); + memcpy(_prefs.region_src_pubkey, pubkey, PUB_KEY_SIZE); + next_region_fetch = futureMillis(1); // fetch from the new source now + savePrefs(); + formatRegionSubscribeReply(reply); + } + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } @@ -1300,6 +1605,8 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } + loopRegionQuery(); + // update uptime uint32_t now = millis(); uptime_millis += now - last_millis; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index aa7d30b0..dd1e533a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -114,12 +114,35 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t pending_sf; uint8_t pending_cr; int matching_peer_indexes[MAX_CLIENTS]; + mesh::Identity region_query_id; // node we are currently asking for regions + uint32_t region_query_tag; + unsigned long region_query_until; + uint8_t region_query_mode; // one of SCOPE_QUERY_* + uint8_t region_discover_next; // index into neighbours[], for the 'discover.regions' pass + char region_discover_reply[134]; // results of last pass, for 'discover.regions list' + unsigned long next_region_fetch; // when to re-fetch from the subscribed node (0 = never) + uint32_t region_fetch_at; // when the last fetch completed (0 = not yet) + uint8_t region_fetch_added, region_fetch_known; + uint8_t region_fetch_fails; // consecutive unanswered fetches #if defined(WITH_RS232_BRIDGE) RS232Bridge bridge; #elif defined(WITH_ESPNOW_BRIDGE) ESPNowBridge bridge; #endif + bool isRegionSrcSet() const; + bool resolveNeighbourPubKey(uint8_t* pubkey, int prefix_len); + void formatRegionSubscribeReply(char* reply); + bool sendRegionsReq(const mesh::Identity& target); + bool handleRegionsResponse(const uint8_t* data, size_t len); + void mergeSubscribedRegions(const uint8_t* names, size_t len); + void startRegionDiscover(char* reply); + void advanceRegionDiscover(const uint8_t* names, size_t len); + void cancelRegionSubscribe(); + void scheduleRegionFetchRetry(); + void startRegionFetch(); + void loopRegionQuery(); + void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 69fe0315..407eed1c 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -68,6 +68,7 @@ public: uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) + uint8_t region_src_pubkey[PUB_KEY_SIZE]; // node to subscribe to regions from (all zeroes = none) private: class RadioPrefs : public ConfigSerializer { @@ -146,6 +147,7 @@ private: def("f_max_uns", _parent->flood_max_unscoped); def("f_max_adv", _parent->flood_max_advert); def("loop", _parent->loop_detect); + def("region_src", _parent->region_src_pubkey, sizeof(_parent->region_src_pubkey)); } public: RepeatPrefs(NodePrefs* parent) : _parent(parent) { } @@ -171,6 +173,7 @@ protected: def("owner", owner_info, sizeof(owner_info)); def("adv_int", advert_interval); def("f_adv_int", flood_advert_interval); + def("disc_mod", discovery_mod_timestamp); // else 'since' filtered DISCOVERs miss us after a reboot def("lat", node_lat); def("lon", node_lon); def("radio", radio); @@ -188,6 +191,7 @@ public: guest_password[0] = 0; bridge_secret[0] = 0; owner_info[0] = 0; + memset(region_src_pubkey, 0, sizeof(region_src_pubkey)); } }; diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 4667e003..29ee4b18 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -346,3 +346,50 @@ int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) *dp = 0; // set null terminator return dp - dest; // return length } + +// Take the next region name from 'src', starting at *cursor. Returns false at the end. +static bool next_name(const char *src, int len, int *cursor, char dest[], int dest_size) { + while (*cursor < len) { + int n = 0; + bool valid = true; + while (*cursor < len && src[*cursor] != ',') { + uint8_t c = src[(*cursor)++]; + if (!RegionMap::is_name_char(c)) { valid = false; continue; } // don't trust the sender + if (n + 1 < dest_size) { dest[n++] = c; } else { valid = false; } // too long for a name + } + (*cursor)++; // skip the separator + + dest[n] = 0; + if (valid && n > 0) return true; + } + return false; // no more names +} + +int RegionMap::importNamesFrom(const char *src, int len, int* num_known) { + char name[sizeof(RegionEntry::name)]; + int cursor = 0, added = 0, known = 0; + + // a decrypted payload is padded out to the cipher block size, so the list can be + // followed by null bytes. Without this, the last name runs into the padding. + for (int i = 0; i < len; i++) { + if (src[i] == 0) { len = i; break; } + } + + while (next_name(src, len, &cursor, name, sizeof(name))) { + // the wildcard is not a Region, and private ('$') Regions have keys we cannot derive + if (name[0] == '*' || name[0] == '$') continue; + + if (findByName(name)) { + known++; // already in the map, leave it exactly as it is + continue; + } + + auto region = putRegion(name, 0); // add as a child of the wildcard + if (region == NULL) break; // full! + region->flags = 0; // allow flood + added++; + } + + if (num_known) { *num_known = known; } + return added; // return number of new Regions +} diff --git a/src/helpers/RegionMap.h b/src/helpers/RegionMap.h index 5eb14429..038f11d5 100644 --- a/src/helpers/RegionMap.h +++ b/src/helpers/RegionMap.h @@ -54,6 +54,16 @@ public: const RegionEntry* getByIdx(int i) const { return ®ions[i]; } const RegionEntry* getRoot() const { return &wildcard; } int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false); + + /** + * \brief Add Regions from a comma separated list of names, as written by exportNamesTo(). + * This is additive: a name already in the map is left exactly as it is, keeping its + * flags and its parent. New names are added as flood-allowed children of the wildcard. + * The wildcard and private ('$') Regions in the list are skipped. + * \param num_known OUT - optional, how many names were already in the map + * \returns the number of Regions added + */ + int importNamesFrom(const char *src, int len, int* num_known = NULL); int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num); void exportTo(Stream& out) const;