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
6954void 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 (×tamp, 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
319324int 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
329334void 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
352357void 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 (×tamp, 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
670714void 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}
0 commit comments