* 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:
@@ -488,23 +488,25 @@ protected:
|
|||||||
uint32_t sender_timestamp;
|
uint32_t sender_timestamp;
|
||||||
memcpy(&sender_timestamp, data, 4);
|
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()
|
// yes, is response to pending sendLogin()
|
||||||
pending_login = 0;
|
pending_login = 0;
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
|
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
|
||||||
out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS;
|
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
|
} 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++] = PUSH_CODE_LOGIN_SUCCESS;
|
||||||
|
out_frame[i++] = data[6]; // permissions (eg. is_admin)
|
||||||
} else {
|
} else {
|
||||||
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
|
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
|
memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix
|
||||||
_serial->writeFrame(out_frame, i);
|
_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()
|
// yes, is response to pending sendStatusRequest()
|
||||||
pending_status = 0;
|
pending_status = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ static uint32_t _atoi(const char* sp) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define CMD_GET_STATS 0x01
|
#define CMD_GET_STATUS 0x01
|
||||||
|
|
||||||
|
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
||||||
|
|
||||||
struct RepeaterStats {
|
struct RepeaterStats {
|
||||||
uint16_t batt_milli_volts;
|
uint16_t batt_milli_volts;
|
||||||
@@ -105,13 +107,13 @@ struct RepeaterStats {
|
|||||||
uint32_t total_up_time_secs;
|
uint32_t total_up_time_secs;
|
||||||
uint32_t n_sent_flood, n_sent_direct;
|
uint32_t n_sent_flood, n_sent_direct;
|
||||||
uint32_t n_recv_flood, n_recv_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;
|
uint16_t n_direct_dups, n_flood_dups;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ClientInfo {
|
struct ClientInfo {
|
||||||
mesh::Identity id;
|
mesh::Identity id;
|
||||||
uint32_t last_timestamp;
|
uint32_t last_timestamp, last_activity;
|
||||||
uint8_t secret[PUB_KEY_SIZE];
|
uint8_t secret[PUB_KEY_SIZE];
|
||||||
bool is_admin;
|
bool is_admin;
|
||||||
int8_t out_path_len;
|
int8_t out_path_len;
|
||||||
@@ -146,22 +148,24 @@ class MyMesh : public mesh::Mesh {
|
|||||||
bool _logging;
|
bool _logging;
|
||||||
NodePrefs _prefs;
|
NodePrefs _prefs;
|
||||||
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
||||||
int num_clients;
|
|
||||||
ClientInfo known_clients[MAX_CLIENTS];
|
ClientInfo known_clients[MAX_CLIENTS];
|
||||||
|
|
||||||
ClientInfo* putClient(const mesh::Identity& id) {
|
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 (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
|
||||||
}
|
}
|
||||||
if (num_clients < MAX_CLIENTS) {
|
|
||||||
auto newClient = &known_clients[num_clients++];
|
oldest->id = id;
|
||||||
newClient->id = id;
|
oldest->out_path_len = -1; // initially out_path is unknown
|
||||||
newClient->out_path_len = -1; // initially out_path is unknown
|
oldest->last_timestamp = 0;
|
||||||
newClient->last_timestamp = 0;
|
self_id.calcSharedSecret(oldest->secret, id); // calc ECDH shared secret
|
||||||
self_id.calcSharedSecret(newClient->secret, id); // calc ECDH shared secret
|
return oldest;
|
||||||
return newClient;
|
|
||||||
}
|
|
||||||
return NULL; // table is full
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int handleRequest(ClientInfo* sender, uint8_t* payload, size_t payload_len) {
|
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
|
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||||
|
|
||||||
switch (payload[0]) {
|
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;
|
RepeaterStats stats;
|
||||||
stats.batt_milli_volts = board.getBattMilliVolts();
|
stats.batt_milli_volts = board.getBattMilliVolts();
|
||||||
stats.curr_tx_queue_len = _mgr->getOutboundCount();
|
stats.curr_tx_queue_len = _mgr->getOutboundCount();
|
||||||
@@ -184,6 +188,7 @@ class MyMesh : public mesh::Mesh {
|
|||||||
stats.n_recv_flood = getNumRecvFlood();
|
stats.n_recv_flood = getNumRecvFlood();
|
||||||
stats.n_recv_direct = getNumRecvDirect();
|
stats.n_recv_direct = getNumRecvDirect();
|
||||||
stats.n_full_events = getNumFullEvents();
|
stats.n_full_events = getNumFullEvents();
|
||||||
|
stats.reserved1 = 0;
|
||||||
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
|
||||||
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
|
||||||
|
|
||||||
@@ -324,26 +329,35 @@ protected:
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto client = putClient(sender); // add to known clients (if not already known)
|
auto client = putClient(sender); // add to known clients (if not already known)
|
||||||
if (client == NULL || timestamp <= client->last_timestamp) {
|
if (timestamp <= client->last_timestamp) {
|
||||||
MESH_DEBUG_PRINTLN("Client table full, or replay attack!");
|
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
||||||
return; // FATAL: client table is full -OR- replay attack
|
return; // FATAL: client table is full -OR- replay attack
|
||||||
}
|
}
|
||||||
|
|
||||||
MESH_DEBUG_PRINTLN("Login success!");
|
MESH_DEBUG_PRINTLN("Login success!");
|
||||||
client->last_timestamp = timestamp;
|
client->last_timestamp = timestamp;
|
||||||
|
client->last_activity = getRTCClock()->getCurrentTime();
|
||||||
client->is_admin = is_admin;
|
client->is_admin = is_admin;
|
||||||
|
|
||||||
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||||
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
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()) {
|
if (packet->isRouteFlood()) {
|
||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
// 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,
|
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);
|
if (path) sendFlood(path);
|
||||||
} else {
|
} 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 (reply) {
|
||||||
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
|
||||||
sendDirect(reply, client->out_path, client->out_path_len);
|
sendDirect(reply, client->out_path, client->out_path_len);
|
||||||
@@ -359,7 +373,7 @@ protected:
|
|||||||
|
|
||||||
int searchPeersByHash(const uint8_t* hash) override {
|
int searchPeersByHash(const uint8_t* hash) override {
|
||||||
int n = 0;
|
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)) {
|
if (known_clients[i].id.isHashMatch(hash)) {
|
||||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
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 {
|
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
|
||||||
int i = matching_peer_indexes[peer_idx];
|
int i = matching_peer_indexes[peer_idx];
|
||||||
if (i >= 0 && i < num_clients) {
|
if (i >= 0 && i < MAX_CLIENTS) {
|
||||||
// lookup pre-calculated shared_secret
|
// lookup pre-calculated shared_secret
|
||||||
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
|
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
|
||||||
} else {
|
} 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 {
|
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];
|
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);
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -393,6 +407,7 @@ protected:
|
|||||||
if (reply_len == 0) return; // invalid command
|
if (reply_len == 0) return; // invalid command
|
||||||
|
|
||||||
client->last_timestamp = timestamp;
|
client->last_timestamp = timestamp;
|
||||||
|
client->last_activity = getRTCClock()->getCurrentTime();
|
||||||
|
|
||||||
if (packet->isRouteFlood()) {
|
if (packet->isRouteFlood()) {
|
||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
// 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);
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
||||||
} else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks
|
} else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks
|
||||||
client->last_timestamp = sender_timestamp;
|
client->last_timestamp = sender_timestamp;
|
||||||
|
client->last_activity = getRTCClock()->getCurrentTime();
|
||||||
|
|
||||||
// len can be > original length, but 'text' will be padded with zeroes
|
// 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
|
data[len] = 0; // need to make a C string again, with null terminator
|
||||||
@@ -471,7 +487,7 @@ protected:
|
|||||||
// TODO: prevent replay attacks
|
// TODO: prevent replay attacks
|
||||||
int i = matching_peer_indexes[sender_idx];
|
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);
|
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t) path_len);
|
||||||
auto client = &known_clients[i];
|
auto client = &known_clients[i];
|
||||||
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
|
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)
|
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _board(&board)
|
||||||
{
|
{
|
||||||
my_radio = &radio;
|
my_radio = &radio;
|
||||||
num_clients = 0;
|
memset(known_clients, 0, sizeof(known_clients));
|
||||||
next_local_advert = 0;
|
next_local_advert = 0;
|
||||||
_logging = false;
|
_logging = false;
|
||||||
|
|
||||||
@@ -836,6 +852,4 @@ void loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
the_mesh.loop();
|
the_mesh.loop();
|
||||||
|
|
||||||
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user