From d81bbe56a0eb781eab627041417d2ac3f9249023 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 24 Feb 2025 16:01:14 +1100 Subject: [PATCH] * repeater: login now evicts oldest client table entry if full. New RESP_SERVER_LOGIN_OK login response format (with permissions byte) * companion radio: PUSH_CODE_LOGIN_SUCCESS now returns 'permissions' byte (0 for legacy "OK" response) --- examples/companion_radio/main.cpp | 10 +++-- examples/simple_repeater/main.cpp | 68 +++++++++++++++++++------------ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index c34cac26..07b6e507 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -488,23 +488,25 @@ protected: uint32_t sender_timestamp; memcpy(&sender_timestamp, data, 4); - if (memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response + if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response // yes, is response to pending sendLogin() pending_login = 0; int i = 0; if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = 0; // legacy: is_admin = false } else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response - // TODO: check the keep_alive_interval in data[] + // keep_alive_interval = data[5] * 16 out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; + out_frame[i++] = data[6]; // permissions (eg. is_admin) } else { out_frame[i++] = PUSH_CODE_LOGIN_FAIL; + out_frame[i++] = 0; // reserved } - out_frame[i++] = 0; // reserved memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix _serial->writeFrame(out_frame, i); - } else if (len > 4 && memcmp(&pending_status, contact.id.pub_key, 4) == 0) { // check for status response + } else if (len > 4 && pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0) { // check for status response // yes, is response to pending sendStatusRequest() pending_status = 0; diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 1b79feb4..e3ec8d34 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -92,7 +92,9 @@ static uint32_t _atoi(const char* sp) { return n; } -#define CMD_GET_STATS 0x01 +#define CMD_GET_STATUS 0x01 + +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ struct RepeaterStats { uint16_t batt_milli_volts; @@ -105,13 +107,13 @@ struct RepeaterStats { uint32_t total_up_time_secs; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; - uint32_t n_full_events; + uint16_t n_full_events, reserved1; uint16_t n_direct_dups, n_flood_dups; }; struct ClientInfo { mesh::Identity id; - uint32_t last_timestamp; + uint32_t last_timestamp, last_activity; uint8_t secret[PUB_KEY_SIZE]; bool is_admin; int8_t out_path_len; @@ -146,22 +148,24 @@ class MyMesh : public mesh::Mesh { bool _logging; NodePrefs _prefs; uint8_t reply_data[MAX_PACKET_PAYLOAD]; - int num_clients; ClientInfo known_clients[MAX_CLIENTS]; ClientInfo* putClient(const mesh::Identity& id) { - for (int i = 0; i < num_clients; i++) { + 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 } - if (num_clients < MAX_CLIENTS) { - auto newClient = &known_clients[num_clients++]; - newClient->id = id; - newClient->out_path_len = -1; // initially out_path is unknown - newClient->last_timestamp = 0; - self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret - return newClient; - } - return NULL; // table is full + + oldest->id = id; + oldest->out_path_len = -1; // initially out_path is unknown + oldest->last_timestamp = 0; + self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret + return oldest; } int handleRequest(ClientInfo* sender, uint8_t* payload, size_t payload_len) { @@ -169,7 +173,7 @@ class MyMesh : public mesh::Mesh { memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp switch (payload[0]) { - case CMD_GET_STATS: { // guests can also access this now + case CMD_GET_STATUS: { // guests can also access this now RepeaterStats stats; stats.batt_milli_volts = board.getBattMilliVolts(); stats.curr_tx_queue_len = _mgr->getOutboundCount(); @@ -184,6 +188,7 @@ class MyMesh : public mesh::Mesh { stats.n_recv_flood = getNumRecvFlood(); stats.n_recv_direct = getNumRecvDirect(); stats.n_full_events = getNumFullEvents(); + stats.reserved1 = 0; stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); @@ -324,26 +329,35 @@ protected: } auto client = putClient(sender); // add to known clients (if not already known) - if (client == NULL || timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("Client table full, or replay attack!"); + 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; uint32_t now = getRTCClock()->getCurrentTimeUnique(); memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(&reply_data[4], "OK", 2); + #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 (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, 4 + 2); + PAYLOAD_TYPE_RESPONSE, reply_data, 12); if (path) sendFlood(path); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 4 + 2); + 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); @@ -359,7 +373,7 @@ protected: int searchPeersByHash(const uint8_t* hash) override { int n = 0; - for (int i = 0; i < num_clients; i++) { + for (int i = 0; i < MAX_CLIENTS; i++) { if (known_clients[i].id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } @@ -369,7 +383,7 @@ protected: void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override { int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < num_clients) { + if (i >= 0 && i < MAX_CLIENTS) { // lookup pre-calculated shared_secret memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); } else { @@ -379,7 +393,7 @@ protected: void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override { int i = matching_peer_indexes[sender_idx]; - if (i < 0 || i >= num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i < 0 || i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); return; } @@ -393,6 +407,7 @@ protected: if (reply_len == 0) return; // invalid command client->last_timestamp = timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response @@ -421,6 +436,7 @@ protected: MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); // len can be > original length, but 'text' will be padded with zeroes data[len] = 0; // need to make a C string again, with null terminator @@ -471,7 +487,7 @@ protected: // TODO: prevent replay attacks int i = matching_peer_indexes[sender_idx]; - if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i >= 0 && i < MAX_CLIENTS) { // 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]; memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() @@ -488,7 +504,7 @@ public: : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _board(&board) { my_radio = &radio; - num_clients = 0; + memset(known_clients, 0, sizeof(known_clients)); next_local_advert = 0; _logging = false; @@ -836,6 +852,4 @@ void loop() { } the_mesh.loop(); - - // TODO: periodically check for OLD/inactive entries in known_clients[], and evict }