diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4265ca6a..dad2dd83 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -43,28 +43,13 @@ #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ #define CLI_REPLY_DELAY_MILLIS 600 - -ClientInfo *MyMesh::putClient(const mesh::Identity &id) { - uint32_t min_time = 0xFFFFFFFF; - ClientInfo *oldest = &known_clients[0]; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].last_activity < min_time) { - oldest = &known_clients[i]; - min_time = oldest->last_activity; - } - if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known - } - - oldest->id = id; - oldest->out_path_len = -1; // initially out_path is unknown - oldest->last_timestamp = 0; - return oldest; -} +#define LAZY_CONTACTS_WRITE_DELAY 5000 void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled @@ -93,15 +78,61 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn #endif } -int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, - size_t payload_len) { +uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) { + ClientInfo* client; + if (data[0] == 0) { // blank password, just check if sender is in ACL + client = acl.getClient(sender.pub_key, PUB_KEY_SIZE); + if (client == NULL) { + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Login, sender not in ACL"); + #endif + return 0; + } + } else { + uint8_t perms; + if (strcmp((char *)data, _prefs.password) == 0) { // check for valid admin password + perms = PERM_ACL_ADMIN; + } else if (strcmp((char *)data, _prefs.guest_password) == 0) { // check guest password + perms = PERM_ACL_GUEST; + } else { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", data); +#endif + return 0; + } + + client = acl.putClient(sender, 0); // add to contacts (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return 0; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->permissions |= perms; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = client->isAdmin() ? 1 : 0; + reply_data[7] = client->permissions; + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + + return 12; // reply length +} + +int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) { // uint32_t now = getRTCClock()->getCurrentTimeUnique(); // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(reply_data, &sender_timestamp, - 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - switch (payload[0]) { - case REQ_TYPE_GET_STATUS: { // guests can also access this now + if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now RepeaterStats stats; stats.batt_milli_volts = board.getBattMilliVolts(); stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); @@ -125,18 +156,31 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t return 4 + sizeof(stats); // reply_len } - case REQ_TYPE_GET_TELEMETRY_DATA: { + if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific - sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry); + sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry); uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } + if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && (sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) { + uint8_t res1 = payload[1]; // reserved for future (extra query params) + uint8_t res2 = payload[2]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; // skip deleted entries + memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix + reply_data[ofs++] = c->permissions; + } + return ofs; + } } return 0; // unknown command } @@ -261,65 +305,26 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m uint32_t timestamp; memcpy(×tamp, data, 4); - bool is_admin; - data[len] = 0; // ensure null terminator - if (strcmp((char *)&data[4], _prefs.password) == 0) { // check for valid password - is_admin = true; - } else if (strcmp((char *)&data[4], _prefs.guest_password) == 0) { // check guest password - is_admin = false; - } else { -#if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); -#endif - return; - } + uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); - auto client = putClient(sender); // add to known clients (if not already known) - if (timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("Possible login replay attack!"); - return; // FATAL: client table is full -OR- replay attack - } - - MESH_DEBUG_PRINTLN("Login success!"); - client->last_timestamp = timestamp; - client->last_activity = getRTCClock()->getCurrentTime(); - client->is_admin = is_admin; - memcpy(client->secret, secret, PUB_KEY_SIZE); - - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp -#if 0 - memcpy(&reply_data[4], "OK", 2); // legacy response -#else - reply_data[4] = RESP_SERVER_LOGIN_OK; - reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = is_admin ? 1 : 0; - reply_data[7] = 0; // FUTURE: reserved - getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness -#endif + if (reply_len == 0) return; // invalid request if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet *path = createPathReturn(sender, client->secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 12); + mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); } } } int MyMesh::searchPeersByHash(const uint8_t *hash) { int n = 0; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].id.isHashMatch(hash)) { + for (int i = 0; i < acl.getNumClients(); i++) { + if (acl.getClientByIdx(i)->id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } } @@ -328,9 +333,9 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) { void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < MAX_CLIENTS) { + if (i >= 0 && i < acl.getNumClients()) { // lookup pre-calculated shared_secret - memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); + memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE); } else { MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); } @@ -352,12 +357,12 @@ 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 < 0 || - i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + 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; } - auto client = &known_clients[i]; + ClientInfo* client = acl.getClientByIdx(i); + if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) uint32_t timestamp; memcpy(×tamp, data, 4); @@ -388,7 +393,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->is_admin) { // a CLI command + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command uint32_t sender_timestamp; memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint flags = (data[4] >> 2); // message attempt number, and other flags @@ -457,11 +462,12 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t // TODO: prevent replay attacks int i = matching_peer_indexes[sender_idx]; - if (i >= 0 && - i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); - auto client = &known_clients[i]; + auto client = acl.getClientByIdx(i); + memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); } @@ -480,8 +486,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc , bridge(_mgr, &rtc) #endif { - memset(known_clients, 0, sizeof(known_clients)); next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; @@ -515,6 +521,8 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); + acl.load(_fs); + #ifdef WITH_BRIDGE bridge.begin(); #endif @@ -664,7 +672,43 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply command += 3; } - _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + // handle sensor-specific CLI commands + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} + char* hex = &command[8]; + char* sp = strchr(hex, ' '); // look for separator char + if (sp == NULL) { + strcpy(reply, "Err - bad params"); + } else { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { + uint8_t perms = atoi(sp); + if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save() + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - invalid params"); + } + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + Serial.println("ACL:"); + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; // skip deleted (or guest) entries + + Serial.printf("%02X ", c->permissions); + mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); + Serial.printf("\n"); + } + reply[0] = 0; + } else{ + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } } void MyMesh::loop() { @@ -698,4 +742,10 @@ void MyMesh::loop() { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); MESH_DEBUG_PRINTLN("Radio params restored"); } + + // is there are pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + acl.save(_fs); + dirty_contacts_expiry = 0; + } } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 06e7ebe4..798d31f2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -52,15 +53,6 @@ struct RepeaterStats { uint32_t total_rx_air_time_secs; }; -struct ClientInfo { - mesh::Identity id; - uint32_t last_timestamp, last_activity; - uint8_t secret[PUB_KEY_SIZE]; - bool is_admin; - int8_t out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; -}; - #ifndef MAX_CLIENTS #define MAX_CLIENTS 32 #endif @@ -91,7 +83,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { NodePrefs _prefs; CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; - ClientInfo known_clients[MAX_CLIENTS]; + ClientACL acl; + unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif @@ -108,8 +101,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif - ClientInfo* putClient(const mesh::Identity& id); 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); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert();