room-server: drop repeater forwarding, neighbour tracking and discovery

A room server is an endpoint, not a repeater. Strip the repeater
machinery the role inherited from the RepeaterMesh clone:

- allowPacketForward() returns false unconditionally — never relays
  transit traffic. (Upstream's simple_room_server gates this on
  disable_fwd; hard-off here since the role should never repeat.)
- neighbour tracking: NeighbourInfo/neighbours[], putNeighbour,
  onAdvertRecv, removeNeighbor; formatNeighborsReply -> "not supported"
- node discovery: onControlDataRecv, sendNodeDiscoverReq, the
  discover.neighbors CLI, discover rate-limiter + pending state
- loop detection: isLooped + max_loop_* tables + loop_detect pref
- dead code: RepeaterStats struct, GET_NEIGHBOURS handler, simple_sort

-344 lines net. Builds clean (FLASH 30.7% / RAM 32.2%); login, post,
push/sync, read-only guest and admin remote-management all
hardware-verified on the GAT562 kit.
This commit is contained in:
rlwilliamson-dev
2026-06-02 18:19:22 -05:00
parent 79afecf11b
commit f3da3f7cee
2 changed files with 20 additions and 364 deletions
+12 -306
View File
@@ -39,20 +39,6 @@ static inline mesh::LoRaRadioBase& getRadioDriver(mesh::Radio* radio) {
return *static_cast<mesh::LoRaRadioBase*>(radio);
}
/* Simple sort helper since <algorithm> is not available in Zephyr minimal C++ */
template<typename T, typename Comparator>
static void simple_sort(T* arr, int count, Comparator cmp) {
for (int i = 0; i < count - 1; i++) {
for (int j = i + 1; j < count; j++) {
if (cmp(arr[j], arr[i])) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
LOG_MODULE_REGISTER(zephcore_repeater, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
#if IS_ENABLED(CONFIG_ZEPHCORE_REPEATER_UPLINK) && IS_ENABLED(CONFIG_MQTT_LIB)
@@ -72,7 +58,6 @@ static void uplink_time_sync_cb(uint32_t unix_ts)
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
#define REQ_TYPE_GET_ACCESS_LIST 0x05
#define REQ_TYPE_GET_NEIGHBOURS 0x06
#define REQ_TYPE_GET_OWNER_INFO 0x07
#define RESP_SERVER_LOGIN_OK 0
@@ -96,9 +81,9 @@ static void uplink_time_sync_cb(uint32_t unix_ts)
#define PUSH_ACK_TIMEOUT_FACTOR 2000
#define POST_SYNC_DELAY_SECS 2
/* Stats blob returned for REQ_TYPE_GET_STATUS on a room server. Mirrors the
* repeater's RepeaterStats but reports posted/pushed counts in the trailing
* two fields (matches upstream MeshCore's ServerStats wire layout). */
/* Stats blob returned for REQ_TYPE_GET_STATUS on a room server. Reports
* posted/pushed counts in the trailing two fields (matches upstream
* MeshCore's ServerStats wire layout). */
struct ServerStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
@@ -133,29 +118,6 @@ static void radio_set_tx_power(uint8_t power_dbm) {
LOG_INF("TX power %d dBm requested (configured via board defconfig)", power_dbm);
}
void RoomServerMesh::putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr) {
#if MAX_NEIGHBOURS > 0
uint32_t oldest_timestamp = 0xFFFFFFFF;
NeighbourInfo* neighbour = &neighbours[0];
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
if (id.matches(neighbours[i].id)) {
neighbour = &neighbours[i];
break;
}
if (neighbours[i].heard_timestamp < oldest_timestamp) {
neighbour = &neighbours[i];
oldest_timestamp = neighbour->heard_timestamp;
}
}
neighbour->id = id;
neighbour->advert_timestamp = timestamp;
neighbour->heard_timestamp = getRTCClock()->getCurrentTime();
neighbour->snr = (int8_t)(snr * 4);
#endif
}
int RoomServerMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len) {
memcpy(reply_data, &sender_timestamp, 4);
@@ -274,84 +236,6 @@ int RoomServerMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp,
}
}
if (payload[0] == REQ_TYPE_GET_NEIGHBOURS) {
#if MAX_NEIGHBOURS > 0
uint8_t request_version = payload[1];
if (request_version == 0) {
int reply_offset = 4;
uint8_t count = payload[2];
uint16_t offset;
memcpy(&offset, &payload[3], 2);
uint8_t order_by = payload[5];
uint8_t pubkey_prefix_length = payload[6];
if (pubkey_prefix_length > PUB_KEY_SIZE) {
pubkey_prefix_length = PUB_KEY_SIZE;
}
// Create sorted copy
int16_t neighbours_count = 0;
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
if (neighbours[i].heard_timestamp > 0) {
sorted_neighbours[neighbours_count++] = &neighbours[i];
}
}
// Sort
if (order_by == 0) {
simple_sort(sorted_neighbours, (int)neighbours_count,
[](const NeighbourInfo* a, const NeighbourInfo* b) {
return a->heard_timestamp > b->heard_timestamp;
});
} else if (order_by == 1) {
simple_sort(sorted_neighbours, (int)neighbours_count,
[](const NeighbourInfo* a, const NeighbourInfo* b) {
return a->heard_timestamp < b->heard_timestamp;
});
} else if (order_by == 2) {
simple_sort(sorted_neighbours, (int)neighbours_count,
[](const NeighbourInfo* a, const NeighbourInfo* b) {
return a->snr > b->snr;
});
} else if (order_by == 3) {
simple_sort(sorted_neighbours, (int)neighbours_count,
[](const NeighbourInfo* a, const NeighbourInfo* b) {
return a->snr < b->snr;
});
}
// Build results
int results_count = 0;
int results_offset = 0;
uint8_t results_buffer[130];
for (int index = 0; index < count && index + offset < neighbours_count; index++) {
int entry_size = pubkey_prefix_length + 4 + 1;
if (results_offset + entry_size > (int)sizeof(results_buffer)) break;
auto neighbour = sorted_neighbours[index + offset];
uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length);
results_offset += pubkey_prefix_length;
memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4);
results_offset += 4;
memcpy(&results_buffer[results_offset], &neighbour->snr, 1);
results_offset += 1;
results_count++;
}
memcpy(&reply_data[reply_offset], &neighbours_count, 2);
reply_offset += 2;
memcpy(&reply_data[reply_offset], &results_count, 2);
reply_offset += 2;
memcpy(&reply_data[reply_offset], results_buffer, results_offset);
reply_offset += results_offset;
return reply_offset;
}
#endif
}
if (payload[0] == REQ_TYPE_GET_OWNER_INFO) {
sprintf((char*)&reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info);
return 4 + strlen((char*)&reply_data[4]);
@@ -452,23 +336,6 @@ bool RoomServerMesh::saveFilter(ClientInfo* client) {
return client->isAdmin(); // only persist admins; guests/read-write re-login
}
static uint8_t max_loop_minimal[] = { 0, /* 1-byte */ 4, /* 2-byte */ 2, /* 3-byte */ 1 };
static uint8_t max_loop_moderate[] = { 0, /* 1-byte */ 2, /* 2-byte */ 1, /* 3-byte */ 1 };
static uint8_t max_loop_strict[] = { 0, /* 1-byte */ 1, /* 2-byte */ 1, /* 3-byte */ 1 };
bool RoomServerMesh::isLooped(const mesh::Packet* packet, const uint8_t max_counters[]) {
uint8_t hash_size = packet->getPathHashSize();
uint8_t hash_count = packet->getPathHashCount();
uint8_t n = 0;
const uint8_t* path = packet->path;
while (hash_count > 0) {
if (self_id.isHashMatch(path, hash_size)) n++;
hash_count--;
path += hash_size;
}
return n >= max_counters[hash_size];
}
void RoomServerMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
if (scope.isNull()) {
sendFlood(pkt, delay_millis, path_hash_size);
@@ -493,29 +360,12 @@ void RoomServerMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_mi
}
}
bool RoomServerMesh::allowPacketForward(const mesh::Packet* packet) {
if (_prefs.disable_fwd) return false;
if (packet->isRouteFlood()) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
// un-scoped floods can be clamped to a lower hop limit than scoped (transport) floods
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
}
if (packet->isRouteFlood() && recv_pkt_region == nullptr) return false;
if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) {
const uint8_t* maximums;
if (_prefs.loop_detect == LOOP_DETECT_MINIMAL) {
maximums = max_loop_minimal;
} else if (_prefs.loop_detect == LOOP_DETECT_MODERATE) {
maximums = max_loop_moderate;
} else {
maximums = max_loop_strict;
}
if (isLooped(packet, maximums)) {
MESH_DEBUG_PRINTLN("allowPacketForward: FLOOD packet loop detected!");
return false;
}
}
return true;
/* A room server is an endpoint, not a repeater: it never forwards other nodes'
* transit traffic (no flood relaying, no neighbour/loop-detect machinery). Its
* own replies still go out via sendDirect()/sendFloodReply(); this only governs
* relaying of pass-through packets. */
bool RoomServerMesh::allowPacketForward(const mesh::Packet* /*packet*/) {
return false;
}
const char* RoomServerMesh::getLogDateTime() {
@@ -725,25 +575,6 @@ void RoomServerMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
}
}
static bool isShare(const mesh::Packet* packet) {
if (packet->hasTransportCodes()) {
return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0;
}
return false;
}
void RoomServerMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp,
const uint8_t* app_data, size_t app_data_len) {
mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len);
if (packet->getPathHashCount() == 0 && !isShare(packet)) {
AdvertDataParser parser(app_data, app_data_len);
if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) {
putNeighbour(id, timestamp, packet->getSNR());
}
}
}
void RoomServerMesh::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];
@@ -908,76 +739,12 @@ bool RoomServerMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const
return false;
}
void RoomServerMesh::onControlDataRecv(mesh::Packet* packet) {
uint8_t type = packet->payload[0] & 0xF0;
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 &&
!_prefs.disable_fwd && discover_limiter.allow(getRTCClock()->getCurrentTime())) {
int i = 1;
uint8_t filter = packet->payload[i++];
uint32_t tag;
memcpy(&tag, &packet->payload[i], 4);
i += 4;
uint32_t since = 0;
if (packet->payload_len >= i + 4) {
memcpy(&since, &packet->payload[i], 4);
i += 4;
}
if ((filter & (1 << ADV_TYPE_REPEATER)) != 0 && _prefs.discovery_mod_timestamp >= since) {
bool prefix_only = packet->payload[0] & 1;
uint8_t data[6 + PUB_KEY_SIZE];
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_REPEATER;
data[1] = packet->_snr;
memcpy(&data[2], &tag, 4);
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
if (resp) {
sendZeroHop(resp, getRetransmitDelay(resp) * 4);
}
}
} else if (type == CTL_TYPE_NODE_DISCOVER_RESP && packet->payload_len >= 6) {
uint8_t node_type = packet->payload[0] & 0x0F;
if (node_type != ADV_TYPE_REPEATER) return;
if (packet->payload_len < 6 + PUB_KEY_SIZE) return;
/* Only accept responses matching our pending discover tag */
if (pending_discover_tag == 0 || millisHasNowPassed(pending_discover_until)) {
pending_discover_tag = 0;
return;
}
uint32_t tag;
memcpy(&tag, &packet->payload[2], 4);
if (tag != pending_discover_tag) return;
mesh::Identity id(&packet->payload[6]);
if (id.matches(self_id)) return;
putNeighbour(id, getRTCClock()->getCurrentTime(), packet->getSNR());
}
}
void RoomServerMesh::sendNodeDiscoverReq() {
uint8_t data[10];
data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0
data[1] = (1 << ADV_TYPE_REPEATER);
getRNG()->random(&data[2], 4); // tag
memcpy(&pending_discover_tag, &data[2], 4);
pending_discover_until = futureMillis(30000);
uint32_t since = 0;
memcpy(&data[6], &since, 4);
auto pkt = createControlData(data, sizeof(data));
if (pkt) {
sendZeroHop(pkt);
}
}
RoomServerMesh::RoomServerMesh(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 mesh::StaticPoolPacketManager(), tables),
_board(board),
_cli(board, rtc, acl, &_prefs, this),
region_map(key_store), temp_map(key_store),
discover_limiter(4, 120),
/* Failed-login rate limit: 4 wrong-password attempts per 180s. Global
* rate (not per-sender) — trade-off documented in CRYPTO_AUDIT_INDEX.md
* Phase 4 (mitigation for upstream MeshCore#2556). */
@@ -993,21 +760,12 @@ RoomServerMesh::RoomServerMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh:
region_load_active = false;
recv_pkt_region = nullptr;
memset(default_scope.key, 0, sizeof(default_scope.key));
pending_discover_tag = 0;
pending_discover_until = 0;
#if MAX_NEIGHBOURS > 0
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
neighbours[i].clear();
}
#endif
initNodePrefs(&_prefs);
strcpy(_prefs.node_name, "Room");
_prefs.advert_loc_policy = ADVERT_LOC_PREFS; // advertise prefs coordinates
_prefs.loop_detect = LOOP_DETECT_MODERATE;
_prefs.path_hash_mode = 1;
_prefs.disable_fwd = 1; // a room server does not repeat other traffic by default
_prefs.disable_fwd = 1; // a room server is an endpoint, never repeats
/* Room server: circular post buffer + round-robin push state */
next_post_idx = 0;
@@ -1226,52 +984,9 @@ void RoomServerMesh::setTxPower(int8_t power_dbm) {
radio_set_tx_power(power_dbm);
}
/* A room server keeps no neighbour table (it is not a repeater). */
void RoomServerMesh::formatNeighborsReply(char* reply) {
char* dp = reply;
#if MAX_NEIGHBOURS > 0
int16_t neighbours_count = 0;
NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS];
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
if (neighbours[i].heard_timestamp > 0) {
sorted_neighbours[neighbours_count++] = &neighbours[i];
}
}
simple_sort(sorted_neighbours, (int)neighbours_count,
[](const NeighbourInfo* a, const NeighbourInfo* b) {
return a->heard_timestamp > b->heard_timestamp;
});
for (int i = 0; i < neighbours_count && dp - reply < 134; i++) {
NeighbourInfo* neighbour = sorted_neighbours[i];
if (i > 0) *dp++ = '\n';
char hex[10];
mesh::Utils::toHex(hex, neighbour->id.pub_key, 4);
uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp;
sprintf(dp, "%s:%u:%d", hex, secs_ago, neighbour->snr);
while (*dp) dp++;
}
#endif
if (dp == reply) {
strcpy(dp, "-none-");
dp += 6;
}
*dp = 0;
}
void RoomServerMesh::removeNeighbor(const uint8_t* pubkey, int key_len) {
#if MAX_NEIGHBOURS > 0
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
if (memcmp(neighbours[i].id.pub_key, pubkey, key_len) == 0) {
neighbours[i].clear();
}
}
#endif
strcpy(reply, "not supported");
}
void RoomServerMesh::formatStatsReply(char* reply) {
@@ -1388,15 +1103,6 @@ void RoomServerMesh::handleCommand(uint32_t sender_timestamp, char* command, cha
reply[0] = 0;
} else if (memcmp(command, "region", 6) == 0) {
handleRegionCommand(command, reply);
} else if (memcmp(command, "discover.neighbors", 18) == 0) {
const char* sub = command + 18;
while (*sub == ' ') sub++;
if (*sub != 0) {
strcpy(reply, "Err - discover.neighbors has no options");
} else {
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else {
_cli.handleCommand(sender_timestamp, command, reply);
}
+8 -58
View File
@@ -1,13 +1,15 @@
/*
* SPDX-License-Identifier: Apache-2.0
* RoomServerMesh - LoRa mesh repeater implementation
* RoomServerMesh - LoRa mesh shared-room (BBS) server
*
* Extends mesh::Mesh with:
* - ACL-based client authentication
* - Region-based flood filtering
* - Neighbor tracking
* - ACL-based client authentication (admin / room / guest)
* - Region-based flood scoping
* - Shared-post buffer + round-robin push/sync to logged-in clients
* - CLI commands (via USB serial)
* - Protocol handlers (login, status, telemetry, etc.)
*
* A room server is an endpoint, not a repeater: it does not forward other
* nodes' traffic and keeps no neighbour / node-discovery state.
*/
#pragma once
@@ -37,46 +39,6 @@
#define FIRMWARE_ROLE "room_server"
#ifndef MAX_NEIGHBOURS
#ifdef CONFIG_ZEPHCORE_MAX_NEIGHBOURS
#define MAX_NEIGHBOURS CONFIG_ZEPHCORE_MAX_NEIGHBOURS
#else
#define MAX_NEIGHBOURS 16
#endif
#endif
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
uint32_t heard_timestamp;
int8_t snr; // multiplied by 4
void clear() {
id = mesh::Identity();
advert_timestamp = 0;
heard_timestamp = 0;
snr = 0;
}
};
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events;
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
uint32_t n_recv_errors;
};
#ifndef MAX_UNSYNCED_POSTS
#ifdef CONFIG_ZEPHCORE_MAX_UNSYNCED_POSTS
#define MAX_UNSYNCED_POSTS CONFIG_ZEPHCORE_MAX_UNSYNCED_POSTS
@@ -120,9 +82,7 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks {
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
TransportKey default_scope;
RateLimiter discover_limiter, login_fail_limiter;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
RateLimiter login_fail_limiter;
bool region_load_active;
unsigned long dirty_contacts_expiry;
/* Room server: circular post buffer + round-robin push state */
@@ -131,9 +91,6 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks {
int next_client_idx;
int next_post_idx;
PostInfo posts[MAX_UNSYNCED_POSTS];
#if MAX_NEIGHBOURS > 0
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
unsigned long set_radio_at, revert_radio_at;
float pending_freq;
float pending_bw;
@@ -153,7 +110,6 @@ class RoomServerMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long _uplink_next_status_at;
#endif
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
mesh::Packet* createSelfAdvert();
@@ -180,7 +136,6 @@ protected:
}
bool allowPacketForward(const mesh::Packet* packet) override;
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
const char* getLogDateTime() override;
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
@@ -208,10 +163,8 @@ protected:
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
#if IS_ENABLED(CONFIG_ZEPHCORE_REPEATER_UPLINK)
bool handleUplinkCommand(const char *command, char *reply);
@@ -235,8 +188,6 @@ public:
void begin(RepeaterDataStore* store);
void sendNodeDiscoverReq();
/* CommonCLICallbacks */
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
@@ -261,7 +212,6 @@ public:
void dumpLogFile() override;
void setTxPower(int8_t power_dbm) override;
void formatNeighborsReply(char* reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char* reply) override;
void formatRadioStatsReply(char* reply) override;
void formatPacketStatsReply(char* reply) override;