Let room users manage their own return routes

This commit is contained in:
mikecarper committed 2026-10-10 11:27:39 -07:00
1 parent 5cb8367d12
commit 0eb1d03a16
15 files changed
+1098 -54

No files matched your search

+186 -37
View File
@@ -25,6 +25,8 @@
#include <helpers/RoomTopicStore.h>
#include <helpers/RoomBoardProtocol.h>
#include <helpers/ClientPathPersistence.h>
#include <helpers/ClientPathObservation.h>
#include <helpers/RoomClientPathCommand.h>
#include <helpers/LazyPersistence.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
@@ -452,6 +454,134 @@ void MyMesh::processRoomRequest(const uint8_t* token, uint32_t sequence, char* r
}
#endif
bool MyMesh::sendClientReply(ClientInfo* client, mesh::Packet* packet,
unsigned long delay_millis, uint8_t path_hash_size) {
if (packet == NULL) return false;
if (client == NULL || !mesh::Packet::isValidPathLen(client->out_path_len)) {
return sendFloodReply(packet, delay_millis, path_hash_size);
}
mesh::Packet* alternate = NULL;
if (mesh::Packet::isValidPathLen(client->alt_path_len)
&& (client->alt_path_len != client->out_path_len
|| memcmp(client->alt_path, client->out_path,
mesh::encodedClientPathByteLength(client->out_path_len)) != 0)) {
alternate = obtainNewPacket();
if (alternate != NULL) *alternate = *packet;
}
const bool sent = sendDirect(packet, client->out_path, client->out_path_len,
delay_millis);
if (alternate != NULL) {
if (sent) {
// The primary owns retry and delivery state; the extra route is one copy.
const uint8_t retries = _prefs.direct_retry_enabled;
_prefs.direct_retry_enabled = 0;
sendDirect(alternate, client->alt_path, client->alt_path_len, delay_millis);
_prefs.direct_retry_enabled = retries;
} else {
releasePacket(alternate);
}
}
return sent;
}
bool MyMesh::handleClientPathCommand(ClientInfo* client, char* command, char* reply) {
char* body = mesh::normalizeRoomClientPathCommand(command, reply);
const mesh::RoomClientPathCommand operation = mesh::classifyRoomClientPathCommand(body);
if (operation == mesh::RoomClientPathCommand::None) return false;
if (strlen(command) > 4 && command[2] == '|') reply += 3;
// Leading whitespace can precede the same single Companion prefix.
else {
const char* start = command;
while (*start == ' ' || *start == '\t') start++;
if (strlen(start) > 4 && start[2] == '|') reply += 3;
}
if (client == NULL) {
strcpy(reply, "Err - command needs remote client context");
return true;
}
const uint8_t role = client->permissions & PERM_ACL_ROLE_MASK;
if (!(role == PERM_ACL_READ_ONLY || role == PERM_ACL_READ_WRITE
|| role == PERM_ACL_ADMIN
|| (role == PERM_ACL_GUEST && client->last_activity != 0))) {
strcpy(reply, "Err - room user permission required");
return true;
}
const bool alternate = operation == mesh::RoomClientPathCommand::GetAlt
|| operation == mesh::RoomClientPathCommand::SetAlt;
const bool observed = operation == mesh::RoomClientPathCommand::GetObserved
|| operation == mesh::RoomClientPathCommand::SetObserved;
const bool setting = operation == mesh::RoomClientPathCommand::SetOut
|| operation == mesh::RoomClientPathCommand::SetObserved
|| operation == mesh::RoomClientPathCommand::SetAlt;
uint8_t* selected = alternate ? client->alt_path : client->out_path;
uint8_t* selected_length = alternate ? &client->alt_path_len : &client->out_path_len;
if (observed && mesh::isObservedClientPathPending(*client, futureMillis(0))) {
strcpy(reply, setting ? "Err - path pending" : "> path pending");
return true;
}
if (!setting) {
mesh::formatRoomClientPathReply(observed ? client->observed_path : selected,
observed ? client->observed_path_len : *selected_length, reply, 157);
return true;
}
uint8_t path[MAX_PATH_SIZE] = {};
uint8_t length = OUT_PATH_UNKNOWN;
if (observed) {
length = client->observed_path_len;
if (!mesh::isValidObservedClientPathLength(length, sizeof(path))) {
strcpy(reply, "Err - no path received");
return true;
}
memcpy(path, client->observed_path, mesh::observedClientPathByteLength(length));
} else {
const char* spec = mesh::cli::skipRecentRepeaterSpaces(body + 11);
if (mesh::cli::terminalPathKeywordMatches(spec, "flood")) length = OUT_PATH_FORCE_FLOOD;
else if (mesh::cli::terminalPathKeywordMatches(spec, "none")
|| mesh::cli::terminalPathKeywordMatches(spec, "-")) length = OUT_PATH_UNKNOWN;
else {
mesh::cli::TerminalPath parsed;
if (mesh::cli::parseTerminalPath(spec, path, sizeof(path), 63, parsed)
!= mesh::cli::TerminalPathParseResult::Valid) {
strcpy(reply, "Err - invalid path");
return true;
}
length = parsed.mode == mesh::cli::TerminalPathMode::Clear
? OUT_PATH_UNKNOWN : parsed.encoded_len;
}
}
const size_t path_bytes = mesh::Packet::isValidPathLen(length)
? mesh::encodedClientPathByteLength(length) : 0;
const bool same = length == *selected_length
&& (path_bytes == 0 || memcmp(path, selected, path_bytes) == 0)
&& (alternate || client->out_path_is_persistable);
if (!same) {
const uint8_t previous_length = *selected_length;
uint8_t previous[MAX_PATH_SIZE];
memcpy(previous, selected, sizeof(previous));
const bool was_persistable = client->out_path_is_persistable;
memcpy(selected, path, sizeof(path));
*selected_length = length;
if (!alternate) client->out_path_is_persistable = true;
// A route never promotes a password-derived session into a saved ACL role.
// Only already-retained entries write through the room's existing filter.
if (saveFilter(client)) {
if (!acl.save(_fs, MyMesh::saveFilter)) {
memcpy(selected, previous, sizeof(previous));
*selected_length = previous_length;
client->out_path_is_persistable = was_persistable;
strcpy(reply, "Err - path save failed");
return true;
}
mesh::resetLazyPersistenceAfterSuccess(dirty_contacts_expiry, contacts_save_failures);
}
}
if (!alternate && length == OUT_PATH_UNKNOWN) strcpy(reply, "> outpath cleared");
else mesh::formatRoomClientPathReply(selected, *selected_length, reply, 157);
return true;
}
bool MyMesh::pushRoomTextToClient(ClientInfo *client, uint32_t timestamp,
const mesh::Identity& author, const char* text,
uint32_t topic_revision) {
@@ -495,7 +625,7 @@ bool MyMesh::pushRoomTextToClient(ClientInfo *client, uint32_t timestamp,
client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD);
}
} else {
sent = sendDirect(reply, client->out_path, client->out_path_len);
sent = sendClientReply(client, reply, 0, _prefs.path_hash_mode + 1);
if (sent) {
uint8_t path_hash_count = client->out_path_len & 63;
@@ -1217,6 +1347,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
*client, client_existed, perm, PERM_ACL_ROLE_MASK,
secret, sender_timestamp, getRTCClock()->getCurrentTime(),
reset_out_path, OUT_PATH_UNKNOWN);
mesh::clearObservedClientPath(*client, OUT_PATH_UNKNOWN);
client->extra.room.sync_since = sender_sync_since;
client->extra.room.pending_ack = 0;
client->extra.room.push_failures = 0;
@@ -1252,7 +1383,9 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, 13);
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
if (path && sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize())) {
mesh::beginObservedClientPath(*client, OUT_PATH_UNKNOWN, futureMillis(60000));
}
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13);
if (reply) {
@@ -1344,6 +1477,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
}
#endif
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post
// An inactive unassigned entry cannot create a reader session by sending
// rejected commands that would otherwise refresh last_activity below.
if ((client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST
&& client->last_activity == 0) return;
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = (data[4] >> 2); // message attempt number, and other flags
@@ -1406,7 +1543,22 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
client->last_timestamp = sender_timestamp;
}
if (cached_retry) {
// Cached results must obey current permissions too. A former admin may
// recover an own-route reply, but cannot replay cached privileged output.
char normalized[mesh::RemoteCliReplyCache::MAX_REPLY_TEXT + 1];
StrHelper::strncpy(normalized, text, sizeof(normalized));
char prefix[4] = {};
const char* body = mesh::normalizeRoomClientPathCommand(normalized, prefix);
const uint8_t role = client->permissions & PERM_ACL_ROLE_MASK;
const bool own_path_allowed =
mesh::classifyRoomClientPathCommand(body) != mesh::RoomClientPathCommand::None
&& (role == PERM_ACL_READ_ONLY || role == PERM_ACL_READ_WRITE
|| role == PERM_ACL_ADMIN
|| (role == PERM_ACL_GUEST && client->last_activity != 0));
if (cached_retry && !client->isAdmin() && !own_path_allowed) {
strcpy((char*)&temp[5], "Err - admin permission required");
temp[4] = (TXT_TYPE_CLI_DATA << 2);
} else if (cached_retry) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: replaying cached remote CLI reply");
size_t cached_len = strlen(cached_response);
if (cached_len > mesh::RemoteCliReplyCache::MAX_REPLY_TEXT) {
@@ -1418,21 +1570,21 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
} else if (repeated_timestamp) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: duplicate remote CLI request has no cached reply");
return;
} else if (client->isAdmin()) {
handleCommand(sender_timestamp, (char*)text, (char*)&temp[5],
i, packet->getPathHashSize());
} else {
if (!handleClientPathCommand(client, (char*)text, (char*)&temp[5])) {
if (client->isAdmin()) {
handleCommand(sender_timestamp, (char*)text, (char*)&temp[5],
i, packet->getPathHashSize());
} else {
strcpy((char*)&temp[5], "Err - admin permission required");
}
}
temp[5 + mesh::RemoteCliReplyCache::MAX_REPLY_TEXT] = 0;
if (temp[5] == 0) strcpy((char*)&temp[5], "OK");
remote_cli_reply_cache.remember(client->id.pub_key, request_id,
command_fingerprint,
(char*)&temp[5]);
temp[4] = (TXT_TYPE_CLI_DATA << 2);
} else {
const char* error = "Err - admin permission required";
strcpy((char*)&temp[5], error);
remote_cli_reply_cache.remember(client->id.pub_key, request_id,
command_fingerprint, error);
temp[4] = (TXT_TYPE_CLI_DATA << 2);
}
// CLI_DATA replies are the result signal; no separate ACK is expected.
}
@@ -1482,13 +1634,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret,
temp, 5 + reply_text_len);
if (reply) {
if (!mesh::Packet::isValidPathLen(client->out_path_len)) {
sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY,
sendClientReply(client, reply, delay_millis + SERVER_RESPONSE_DELAY,
packet->getPathHashSize());
} else {
sendDirect(reply, client->out_path, client->out_path_len,
delay_millis + SERVER_RESPONSE_DELAY);
}
}
}
} else if (type == PAYLOAD_TYPE_REQ && len >= 5) {
@@ -1576,11 +1723,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
} else {
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
if (reply) {
if (mesh::Packet::isValidPathLen(client->out_path_len)) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
}
sendClientReply(client, reply, SERVER_RESPONSE_DELAY,
packet->getPathHashSize());
}
}
}
@@ -1598,15 +1742,22 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
auto client = acl.getClientByIdx(i);
if (!room_access.allowsIdentity(client->id.pub_key)) return false;
const bool captured_login_path = packet->isRouteDirect() && extra_type == 0x0F
&& mesh::captureObservedClientPath(*client, path, path_len, futureMillis(0));
// PATH has no durable replay freshness signal. Keep it RAM-only and do not
// let a replay replace an operator-selected force-flood route.
const bool persistence_allowed = mesh::clientPathPersistenceAllowed(
MyMesh::saveFilter(client), false /* no durable replay proof */);
const mesh::ClientPathUpdateResult path_update =
mesh::applyReceivedClientPath(
*client, path, path_len, persistence_allowed,
OUT_PATH_FORCE_FLOOD);
(void)path_update;
// Preserve normal room-app discovery, but do not let the delayed login
// PATH overwrite a route the user explicitly selected in the meantime.
if (!captured_login_path || !client->out_path_is_persistable
|| !mesh::Packet::isValidPathLen(client->out_path_len)) {
const bool persistence_allowed = mesh::clientPathPersistenceAllowed(
MyMesh::saveFilter(client), false /* no durable replay proof */);
const mesh::ClientPathUpdateResult path_update =
mesh::applyReceivedClientPath(
*client, path, path_len, persistence_allowed,
OUT_PATH_FORCE_FLOOD);
(void)path_update;
}
client->last_activity = getRTCClock()->getCurrentTime();
} else {
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
@@ -2100,7 +2251,7 @@ bool MyMesh::resolveAlertScope(TransportKey& dest) {
return false;
}
void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
bool MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
TransportKey req_scope;
bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
bool req_scope_known = recv_pkt_region != NULL && !is_wildcard
@@ -2108,17 +2259,15 @@ void MyMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, ui
switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) {
case mesh::REPLY_SCOPE_REQUEST:
sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request
break;
return sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request
case mesh::REPLY_SCOPE_DEFAULT:
// requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region.
// un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
break;
return sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
case mesh::REPLY_SCOPE_NONE:
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
break;
return sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
}
return false;
}
void MyMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble) {
+4 -1
View File
@@ -466,7 +466,7 @@ protected:
}
#endif
void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
bool sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
#if MESH_ENABLE_TELEMETRY_HISTORY
bool handleTelemetryHistoryCommand(const char* command, char* reply);
void sampleTelemetryHistory();
@@ -598,6 +598,9 @@ public:
void handleCommand(uint32_t sender_timestamp, char* command, char* reply,
int gpio_client_index = -1,
uint8_t gpio_path_hash_size = 1);
bool handleClientPathCommand(ClientInfo* client, char* command, char* reply);
bool sendClientReply(ClientInfo* client, mesh::Packet* packet,
unsigned long delay_millis, uint8_t path_hash_size);
bool _wireless_usb_command = false;
void handleUsbCommand(char* command, char* reply) {
_wireless_usb_command = true;