diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 666f79fc5..24bcfac5e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -119,7 +119,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr MESH_DEBUG_PRINTLN("Login success!"); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); - client->permissions &= ~0x03; + client->permissions &= ~PERM_ACL_ROLE_MASK; client->permissions |= perms; memcpy(client->shared_secret, secret, PUB_KEY_SIZE); @@ -136,7 +136,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp reply_data[4] = RESP_SERVER_LOGIN_OK; reply_data[5] = 0; // Legacy: was recommended keep-alive interval (secs / 16) - reply_data[6] = client->isAdmin() ? 1 : 0; + reply_data[6] = (client->isAdmin() || client->isRegionMgr()) ? 1 : 0; reply_data[7] = client->permissions; getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness reply_data[12] = FIRMWARE_VER_LEVEL; // New field @@ -682,7 +682,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); } - } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && (client->isAdmin() || client->isRegionMgr())) { // a CLI command 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 @@ -719,7 +719,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (is_retry) { *reply = 0; } else { - handleCommand(sender_timestamp, command, reply); + handleCommand(client, command, reply); } int text_len = strlen(reply); if (text_len > 0) { @@ -1165,7 +1165,27 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } -void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { +// Whitelist helper for region manager command perms +static bool isRegionMgrAllowed(const char* cmd) { + while(*cmd == ' ') cmd++; // skip leading spaces + // region commands (read + write region map) + if (memcmp(cmd, "region", 6) == 0) return true; + // read-only getters / status + if (memcmp(cmd, "get ", 4) == 0) return true; + if (memcmp(cmd, "ver", 3) == 0) return true; + if (memcmp(cmd, "board", 5) == 0) return true; + // "neighbors" (plural) is read-only; reject "neighbor.remove" by checking next char + if (memcmp(cmd, "neighbors", 9) == 0) return true; + // bare "clock" is read-only; "clock sync" must be denied + if (memcmp(cmd, "clock", 5) == 0 && memcmp(cmd, "clock sync", 10) != 0) return true; + // sensor reads only + if (memcmp(cmd, "sensor get ", 11) == 0) return true; + if (memcmp(cmd, "sensor list", 11) == 0) return true; + return false; +} + +void MyMesh::handleCommand(ClientInfo* sender, char *command, char *reply) { + uint32_t sender_timestamp = sender ? sender->last_timestamp : 0; // Serial CLI passes NULL if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation region_map = temp_map; // copy over the temp instance as new current map @@ -1208,6 +1228,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply command += 3; } + // Region managers are limited to read-only queries and region commands + // Admins are unrestricted + if (sender && !sender->isAdmin() && sender->isRegionMgr()) { + if (!isRegionMgrAllowed(command)) { + strcpy(reply, "Err - not permitted"); + return; + } + } + // handle ACL related commands if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} char* hex = &command[8]; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8ed0317e6..c14a25d7a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -223,7 +223,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; - void handleCommand(uint32_t sender_timestamp, char* command, char* reply); + void handleCommand(ClientInfo* sender, char* command, char* reply); void loop(); #if defined(WITH_BRIDGE) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index e37078ce5..5d6bb2605 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -124,7 +124,7 @@ void loop() { Serial.print('\n'); command[len - 1] = 0; // replace newline with C string null terminator char reply[160]; - the_mesh.handleCommand(0, command, reply); // NOTE: there is no sender_timestamp via serial! + the_mesh.handleCommand(NULL, command, reply); // NOTE: sender is NULL via serial if (reply[0]) { Serial.print(" -> "); Serial.println(reply); } diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 128238273..8abdd565e 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -99,7 +99,8 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { ClientInfo* oldest = &clients[MAX_CLIENTS - 1]; for (int i = 0; i < num_clients; i++) { if (id.matches(clients[i].id)) return &clients[i]; // already known - if (!clients[i].isAdmin() && clients[i].last_activity < min_time) { + if ( (!clients[i].isAdmin() && !clients[i].isRegionMgr()) + && clients[i].last_activity < min_time) { oldest = &clients[i]; min_time = oldest->last_activity; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index b758f7068..1961546d1 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -4,11 +4,12 @@ #include #include -#define PERM_ACL_ROLE_MASK 3 // lower 2 bits +#define PERM_ACL_ROLE_MASK 7 // lower 3 bits #define PERM_ACL_GUEST 0 #define PERM_ACL_READ_ONLY 1 #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 +#define PERM_ACL_REGION_MGR 4 #define OUT_PATH_UNKNOWN 0xFF @@ -31,6 +32,7 @@ struct ClientInfo { } extra; bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } + bool isRegionMgr() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_REGION_MGR; } }; #ifndef MAX_CLIENTS