* 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)
This commit is contained in:
Scott Powell 2025-02-24 16:01:14 +11:00
parent d880a48b85
commit d81bbe56a0
2 changed files with 47 additions and 31 deletions

View file

@ -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;

View file

@ -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
}