mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-29 07:38:43 +00:00
feat(repeater): discover and subscribe to the regions of other repeaters
Adds the client side of the existing anon-regions request, which until now only had a server side (handleAnonRegionsReq), and uses it two ways: - `discover.regions` asks each heard neighbor in turn which regions it floods, printing results to serial as they arrive. `discover.regions list` reports the last run for remote CLI users. - `regions.subscribe <pubkey>` nominates a repeater to take regions from. The node fetches its regions on change, 2 minutes after boot, and every 72 hours after that, adding any region it doesn't already have as a flood-allowed child of the wildcard. An unanswered fetch is retried after a minute for the first few attempts, then every 12 hours. Subscribing is additive: a region already on this node keeps its flags and its place in the hierarchy, so a region the operator has denied stays denied, and nothing is ever removed. Private ($) regions are skipped, as their transport keys are not derived from the name. Only one request is in flight at a time. The queried node is matched in searchPeersByHash after the ACL clients, so that querying a node which is also a client cannot swallow that client's own traffic. Also fixes two pre-existing issues this feature depends on: - handleAnonRegionsReq() could build a reply larger than createDatagram() accepts, so a node with a large region map answered nothing at all. - discovery_mod_timestamp was saved but never serialized, so it reset to zero on every reboot.
This commit is contained in:
@@ -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 <pubkey>`
|
||||
- `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 <name>`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user