Skip to content

Commit 9ba8d6f

Browse files
author
Scott Powell
committed
Merge branch 'rep-room-acl' into dev
2 parents d86851b + 98b524b commit 9ba8d6f

File tree

6 files changed

+317
-241
lines changed

6 files changed

+317
-241
lines changed

examples/simple_repeater/MyMesh.cpp

Lines changed: 133 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,13 @@
4343
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
4444
#define REQ_TYPE_KEEP_ALIVE 0x02
4545
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
46+
#define REQ_TYPE_GET_ACCESS_LIST 0x05
4647

4748
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
4849

4950
#define CLI_REPLY_DELAY_MILLIS 600
5051

51-
52-
ClientInfo *MyMesh::putClient(const mesh::Identity &id) {
53-
uint32_t min_time = 0xFFFFFFFF;
54-
ClientInfo *oldest = &known_clients[0];
55-
for (int i = 0; i < MAX_CLIENTS; i++) {
56-
if (known_clients[i].last_activity < min_time) {
57-
oldest = &known_clients[i];
58-
min_time = oldest->last_activity;
59-
}
60-
if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known
61-
}
62-
63-
oldest->id = id;
64-
oldest->out_path_len = -1; // initially out_path is unknown
65-
oldest->last_timestamp = 0;
66-
return oldest;
67-
}
52+
#define LAZY_CONTACTS_WRITE_DELAY 5000
6853

6954
void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) {
7055
#if MAX_NEIGHBOURS // check if neighbours enabled
@@ -93,15 +78,61 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn
9378
#endif
9479
}
9580

96-
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload,
97-
size_t payload_len) {
81+
uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) {
82+
ClientInfo* client;
83+
if (data[0] == 0) { // blank password, just check if sender is in ACL
84+
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
85+
if (client == NULL) {
86+
#if MESH_DEBUG
87+
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
88+
#endif
89+
return 0;
90+
}
91+
} else {
92+
uint8_t perms;
93+
if (strcmp((char *)data, _prefs.password) == 0) { // check for valid admin password
94+
perms = PERM_ACL_ADMIN;
95+
} else if (strcmp((char *)data, _prefs.guest_password) == 0) { // check guest password
96+
perms = PERM_ACL_GUEST;
97+
} else {
98+
#if MESH_DEBUG
99+
MESH_DEBUG_PRINTLN("Invalid password: %s", data);
100+
#endif
101+
return 0;
102+
}
103+
104+
client = acl.putClient(sender, 0); // add to contacts (if not already known)
105+
if (sender_timestamp <= client->last_timestamp) {
106+
MESH_DEBUG_PRINTLN("Possible login replay attack!");
107+
return 0; // FATAL: client table is full -OR- replay attack
108+
}
109+
110+
MESH_DEBUG_PRINTLN("Login success!");
111+
client->last_timestamp = sender_timestamp;
112+
client->last_activity = getRTCClock()->getCurrentTime();
113+
client->permissions |= perms;
114+
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
115+
116+
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
117+
}
118+
119+
uint32_t now = getRTCClock()->getCurrentTimeUnique();
120+
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
121+
reply_data[4] = RESP_SERVER_LOGIN_OK;
122+
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
123+
reply_data[6] = client->isAdmin() ? 1 : 0;
124+
reply_data[7] = client->permissions;
125+
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
126+
127+
return 12; // reply length
128+
}
129+
130+
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
98131
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
99132
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
100-
memcpy(reply_data, &sender_timestamp,
101-
4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
133+
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
102134

103-
switch (payload[0]) {
104-
case REQ_TYPE_GET_STATUS: { // guests can also access this now
135+
if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now
105136
RepeaterStats stats;
106137
stats.batt_milli_volts = board.getBattMilliVolts();
107138
stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF);
@@ -125,18 +156,31 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
125156

126157
return 4 + sizeof(stats); // reply_len
127158
}
128-
case REQ_TYPE_GET_TELEMETRY_DATA: {
159+
if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
129160
uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
130161

131162
telemetry.reset();
132163
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
133164
// query other sensors -- target specific
134-
sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry);
165+
sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry);
135166

136167
uint8_t tlen = telemetry.getSize();
137168
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
138169
return 4 + tlen; // reply_len
139170
}
171+
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
172+
uint8_t res1 = payload[1]; // reserved for future (extra query params)
173+
uint8_t res2 = payload[2];
174+
if (res1 == 0 && res2 == 0) {
175+
uint8_t ofs = 4;
176+
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
177+
auto c = acl.getClientByIdx(i);
178+
if (c->permissions == 0) continue; // skip deleted entries
179+
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
180+
reply_data[ofs++] = c->permissions;
181+
}
182+
return ofs;
183+
}
140184
}
141185
return 0; // unknown command
142186
}
@@ -261,65 +305,26 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
261305
uint32_t timestamp;
262306
memcpy(&timestamp, data, 4);
263307

264-
bool is_admin;
265-
data[len] = 0; // ensure null terminator
266-
if (strcmp((char *)&data[4], _prefs.password) == 0) { // check for valid password
267-
is_admin = true;
268-
} else if (strcmp((char *)&data[4], _prefs.guest_password) == 0) { // check guest password
269-
is_admin = false;
270-
} else {
271-
#if MESH_DEBUG
272-
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]);
273-
#endif
274-
return;
275-
}
276-
277-
auto client = putClient(sender); // add to known clients (if not already known)
278-
if (timestamp <= client->last_timestamp) {
279-
MESH_DEBUG_PRINTLN("Possible login replay attack!");
280-
return; // FATAL: client table is full -OR- replay attack
281-
}
282-
283-
MESH_DEBUG_PRINTLN("Login success!");
284-
client->last_timestamp = timestamp;
285-
client->last_activity = getRTCClock()->getCurrentTime();
286-
client->is_admin = is_admin;
287-
memcpy(client->secret, secret, PUB_KEY_SIZE);
308+
uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]);
288309

289-
uint32_t now = getRTCClock()->getCurrentTimeUnique();
290-
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
291-
#if 0
292-
memcpy(&reply_data[4], "OK", 2); // legacy response
293-
#else
294-
reply_data[4] = RESP_SERVER_LOGIN_OK;
295-
reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16)
296-
reply_data[6] = is_admin ? 1 : 0;
297-
reply_data[7] = 0; // FUTURE: reserved
298-
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
299-
#endif
310+
if (reply_len == 0) return; // invalid request
300311

301312
if (packet->isRouteFlood()) {
302313
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
303-
mesh::Packet *path = createPathReturn(sender, client->secret, packet->path, packet->path_len,
304-
PAYLOAD_TYPE_RESPONSE, reply_data, 12);
314+
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
315+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
305316
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
306317
} else {
307-
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12);
308-
if (reply) {
309-
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
310-
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
311-
} else {
312-
sendFlood(reply, SERVER_RESPONSE_DELAY);
313-
}
314-
}
318+
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
319+
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
315320
}
316321
}
317322
}
318323

319324
int MyMesh::searchPeersByHash(const uint8_t *hash) {
320325
int n = 0;
321-
for (int i = 0; i < MAX_CLIENTS; i++) {
322-
if (known_clients[i].id.isHashMatch(hash)) {
326+
for (int i = 0; i < acl.getNumClients(); i++) {
327+
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
323328
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
324329
}
325330
}
@@ -328,9 +333,9 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) {
328333

329334
void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
330335
int i = matching_peer_indexes[peer_idx];
331-
if (i >= 0 && i < MAX_CLIENTS) {
336+
if (i >= 0 && i < acl.getNumClients()) {
332337
// lookup pre-calculated shared_secret
333-
memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE);
338+
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
334339
} else {
335340
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
336341
}
@@ -352,12 +357,12 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32
352357
void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret,
353358
uint8_t *data, size_t len) {
354359
int i = matching_peer_indexes[sender_idx];
355-
if (i < 0 ||
356-
i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
360+
if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
357361
MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i);
358362
return;
359363
}
360-
auto client = &known_clients[i];
364+
ClientInfo* client = acl.getClientByIdx(i);
365+
361366
if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!)
362367
uint32_t timestamp;
363368
memcpy(&timestamp, data, 4);
@@ -388,7 +393,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
388393
} else {
389394
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
390395
}
391-
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->is_admin) { // a CLI command
396+
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command
392397
uint32_t sender_timestamp;
393398
memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
394399
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
457462
// TODO: prevent replay attacks
458463
int i = matching_peer_indexes[sender_idx];
459464

460-
if (i >= 0 &&
461-
i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context)
465+
if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context)
462466
MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len);
463-
auto client = &known_clients[i];
467+
auto client = acl.getClientByIdx(i);
468+
464469
memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect()
470+
client->last_activity = getRTCClock()->getCurrentTime();
465471
} else {
466472
MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i);
467473
}
@@ -480,8 +486,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
480486
, bridge(_mgr, &rtc)
481487
#endif
482488
{
483-
memset(known_clients, 0, sizeof(known_clients));
484489
next_local_advert = next_flood_advert = 0;
490+
dirty_contacts_expiry = 0;
485491
set_radio_at = revert_radio_at = 0;
486492
_logging = false;
487493

@@ -515,6 +521,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
515521
// load persisted prefs
516522
_cli.loadPrefs(_fs);
517523

524+
acl.load(_fs);
525+
518526
#ifdef WITH_BRIDGE
519527
bridge.begin();
520528
#endif
@@ -664,7 +672,43 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
664672
command += 3;
665673
}
666674

667-
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
675+
// handle ACL related commands
676+
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
677+
char* hex = &command[8];
678+
char* sp = strchr(hex, ' '); // look for separator char
679+
if (sp == NULL) {
680+
strcpy(reply, "Err - bad params");
681+
} else {
682+
*sp++ = 0; // replace space with null terminator
683+
684+
uint8_t pubkey[PUB_KEY_SIZE];
685+
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
686+
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
687+
uint8_t perms = atoi(sp);
688+
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
689+
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
690+
strcpy(reply, "OK");
691+
} else {
692+
strcpy(reply, "Err - invalid params");
693+
}
694+
} else {
695+
strcpy(reply, "Err - bad pubkey");
696+
}
697+
}
698+
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
699+
Serial.println("ACL:");
700+
for (int i = 0; i < acl.getNumClients(); i++) {
701+
auto c = acl.getClientByIdx(i);
702+
if (c->permissions == 0) continue; // skip deleted (or guest) entries
703+
704+
Serial.printf("%02X ", c->permissions);
705+
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
706+
Serial.printf("\n");
707+
}
708+
reply[0] = 0;
709+
} else{
710+
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
711+
}
668712
}
669713

670714
void MyMesh::loop() {
@@ -698,4 +742,10 @@ void MyMesh::loop() {
698742
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
699743
MESH_DEBUG_PRINTLN("Radio params restored");
700744
}
745+
746+
// is pending dirty contacts write needed?
747+
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
748+
acl.save(_fs);
749+
dirty_contacts_expiry = 0;
750+
}
701751
}

examples/simple_repeater/MyMesh.h

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <helpers/IdentityStore.h>
1919
#include <helpers/AdvertDataHelpers.h>
2020
#include <helpers/TxtDataHelpers.h>
21+
#include <helpers/ClientACL.h>
2122
#include <RTClib.h>
2223
#include <target.h>
2324

@@ -52,15 +53,6 @@ struct RepeaterStats {
5253
uint32_t total_rx_air_time_secs;
5354
};
5455

55-
struct ClientInfo {
56-
mesh::Identity id;
57-
uint32_t last_timestamp, last_activity;
58-
uint8_t secret[PUB_KEY_SIZE];
59-
bool is_admin;
60-
int8_t out_path_len;
61-
uint8_t out_path[MAX_PATH_SIZE];
62-
};
63-
6456
#ifndef MAX_CLIENTS
6557
#define MAX_CLIENTS 32
6658
#endif
@@ -91,7 +83,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
9183
NodePrefs _prefs;
9284
CommonCLI _cli;
9385
uint8_t reply_data[MAX_PACKET_PAYLOAD];
94-
ClientInfo known_clients[MAX_CLIENTS];
86+
ClientACL acl;
87+
unsigned long dirty_contacts_expiry;
9588
#if MAX_NEIGHBOURS
9689
NeighbourInfo neighbours[MAX_NEIGHBOURS];
9790
#endif
@@ -108,8 +101,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
108101
ESPNowBridge bridge;
109102
#endif
110103

111-
ClientInfo* putClient(const mesh::Identity& id);
112104
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr);
105+
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data);
113106
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
114107
mesh::Packet* createSelfAdvert();
115108

0 commit comments

Comments
 (0)