mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 11:52:29 +00:00
Fit Access Control replies to encrypted packet route budgets
This commit is contained in:
1 parent
9268c57f8c
commit
e4420519a4
6 files changed
+389
-15
No files matched your search
@@ -217,6 +217,7 @@ jobs:
|
||||
python3 -B test/test_ota_device_deflate_role.py -v
|
||||
python3 -B test/test_client_acl_spiffs.py
|
||||
python3 -B test/test_client_acl_cli.py -v
|
||||
python3 -B test/test_client_acl_response.py -v
|
||||
|
||||
- name: Verify ESP32 USB reboot sleep grace and G3 button wake
|
||||
run: |
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <helpers/NRF52VoltagePolicy.h>
|
||||
#endif
|
||||
#include <helpers/ClientACLCLI.h>
|
||||
#include <helpers/ClientACLResponse.h>
|
||||
#include <helpers/ClockSyncUtils.h>
|
||||
#include <helpers/ClientLoginPersistence.h>
|
||||
#include <helpers/ClientPathObservation.h>
|
||||
@@ -729,7 +730,12 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
|
||||
int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len,
|
||||
size_t reply_capacity) {
|
||||
if (sender == NULL || payload_len == 0 || reply_capacity < 4) return 0;
|
||||
if (reply_capacity > mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY) {
|
||||
reply_capacity = mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY;
|
||||
}
|
||||
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
||||
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
||||
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
||||
@@ -780,12 +786,15 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
|
||||
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST) {
|
||||
if (!sender->isAdmin() || payload_len < 3) return 0;
|
||||
uint8_t res1 = payload[1]; // reserved for future (extra query params)
|
||||
uint8_t res2 = payload[2];
|
||||
if (res1 == 0 && res2 == 0) {
|
||||
uint8_t ofs = 4;
|
||||
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
||||
size_t ofs = 4;
|
||||
// Legacy ACL replies contain no pagination or total-count field. Keep
|
||||
// the same bounded prefix, but fit the actual encrypted reply route.
|
||||
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= reply_capacity; i++) {
|
||||
auto c = acl.getClientByIdx(i);
|
||||
if (c->permissions == 0) continue; // skip deleted entries
|
||||
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
|
||||
@@ -965,15 +974,15 @@ static bool directPathsEqual(const uint8_t* a_path, uint8_t a_len, const uint8_t
|
||||
return byte_len == 0 || memcmp(a_path, b_path, byte_len) == 0;
|
||||
}
|
||||
|
||||
void MyMesh::sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
|
||||
bool MyMesh::sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
|
||||
TransportKey fallback_scope;
|
||||
const TransportKey* fallback_scope_ptr = NULL;
|
||||
if (recv_pkt_region != NULL && !recv_pkt_region->isWildcard()
|
||||
&& region_map.getTransportKeysFor(*recv_pkt_region, &fallback_scope, 1) > 0) {
|
||||
fallback_scope_ptr = &fallback_scope;
|
||||
}
|
||||
sendClientReplyWithFallbackScope(client, packet, delay_millis, path_hash_size,
|
||||
fallback_scope_ptr);
|
||||
return sendClientReplyWithFallbackScope(client, packet, delay_millis, path_hash_size,
|
||||
fallback_scope_ptr);
|
||||
}
|
||||
|
||||
bool MyMesh::sendClientReplyWithFallbackScope(ClientInfo* client, mesh::Packet* packet,
|
||||
@@ -2826,25 +2835,30 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
|
||||
#endif
|
||||
|
||||
if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!)
|
||||
if (len < 5) return; // timestamp plus request type must be present
|
||||
uint32_t timestamp;
|
||||
memcpy(×tamp, data, 4);
|
||||
|
||||
if (timestamp > client->last_timestamp) { // prevent replay attacks
|
||||
int reply_len = handleRequest(client, timestamp, &data[4], len - 4);
|
||||
const size_t reply_capacity =
|
||||
mesh::clientACLReplyCapacity(packet->isRouteFlood(), packet->path_len);
|
||||
int reply_len = handleRequest(client, timestamp, &data[4], len - 4, reply_capacity);
|
||||
if (reply_len == 0) return; // invalid command
|
||||
|
||||
client->last_timestamp = timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
|
||||
bool reply_queued = false;
|
||||
if (packet->isRouteFlood()) {
|
||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
||||
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
||||
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
if (path) reply_queued = sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
} else {
|
||||
mesh::Packet *reply =
|
||||
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
|
||||
sendClientReply(client, reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
reply_queued = sendClientReply(client, reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
||||
}
|
||||
if (reply_queued) {
|
||||
client->last_timestamp = timestamp;
|
||||
client->last_activity = getRTCClock()->getCurrentTime();
|
||||
}
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
||||
|
||||
@@ -733,7 +733,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len);
|
||||
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len,
|
||||
size_t reply_capacity = MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1));
|
||||
mesh::Packet* createSelfAdvert();
|
||||
void processDeferredCliCommand();
|
||||
void clearDeferredCliCommand();
|
||||
@@ -1003,7 +1004,7 @@ protected:
|
||||
// OTA mesh-integration is centralized in mesh::Mesh (no per-example onOtaRecv / send adapter / tick).
|
||||
|
||||
bool sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
||||
void sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
||||
bool sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
||||
|
||||
public:
|
||||
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <Packet.h>
|
||||
#include "DatagramPayloadLimits.h"
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Match createDatagram() and createPathReturn() admission limits. A flood
|
||||
// request returns its observed path inside the encrypted response, so those
|
||||
// bytes must also be reserved before adding legacy seven-byte ACL entries.
|
||||
static constexpr size_t CLIENT_ACL_DIRECT_REPLY_CAPACITY =
|
||||
DatagramPayloadLimits::maxPlaintext(
|
||||
MAX_PACKET_PAYLOAD, CIPHER_MAC_SIZE, CIPHER_BLOCK_SIZE);
|
||||
|
||||
inline size_t clientACLReplyCapacity(bool flood, uint8_t path_len) {
|
||||
if (!Packet::isValidPathLen(path_len)) return 0;
|
||||
if (!flood) return CLIENT_ACL_DIRECT_REPLY_CAPACITY;
|
||||
const size_t path_bytes = (path_len & 63) * ((path_len >> 6) + 1);
|
||||
// createPathReturn(): path_bytes + extra_len + 5 <=
|
||||
// MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE.
|
||||
const size_t fixed_bytes = 2 + CIPHER_BLOCK_SIZE + 5;
|
||||
return path_bytes + fixed_bytes <= MAX_PACKET_PAYLOAD
|
||||
? MAX_PACKET_PAYLOAD - path_bytes - fixed_bytes : 0;
|
||||
}
|
||||
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,227 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <helpers/ClientACLResponse.h>
|
||||
|
||||
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
||||
#define SERVER_RESPONSE_DELAY 300
|
||||
|
||||
namespace mesh {
|
||||
struct Identity {
|
||||
uint8_t pub_key[PUB_KEY_SIZE] = {};
|
||||
size_t copyHashTo(uint8_t* destination) const {
|
||||
*destination = pub_key[0];
|
||||
return PATH_HASH_SIZE;
|
||||
}
|
||||
};
|
||||
struct Utils {
|
||||
// Keep packet assembly/admission real; replace only the crypto primitive.
|
||||
static int encryptThenMAC(const uint8_t*, uint8_t* destination,
|
||||
const uint8_t* source, int length) {
|
||||
const int encrypted = (length + CIPHER_BLOCK_SIZE - 1) / CIPHER_BLOCK_SIZE * CIPHER_BLOCK_SIZE;
|
||||
memset(destination, 0, encrypted + CIPHER_MAC_SIZE);
|
||||
memcpy(destination + CIPHER_MAC_SIZE, source, length);
|
||||
return encrypted + CIPHER_MAC_SIZE;
|
||||
}
|
||||
};
|
||||
struct Random {
|
||||
void random(uint8_t* data, size_t length) { memset(data, 0x99, length); }
|
||||
};
|
||||
class Mesh {
|
||||
std::vector<Packet*> allocations;
|
||||
public:
|
||||
Identity self_id;
|
||||
Random rng;
|
||||
bool refuse_allocation = false;
|
||||
~Mesh() { for (auto* packet : allocations) delete packet; }
|
||||
Packet* obtainNewPacket() {
|
||||
if (refuse_allocation) return nullptr;
|
||||
auto* packet = new Packet;
|
||||
allocations.push_back(packet);
|
||||
return packet;
|
||||
}
|
||||
uint8_t getContactTxRadio(const Identity&) { return RADIO_TX_AUTO; }
|
||||
Random* getRNG() { return &rng; }
|
||||
Packet* createPathReturn(const Identity&, const uint8_t*, const uint8_t*, uint8_t,
|
||||
uint8_t, const uint8_t*, size_t);
|
||||
Packet* createPathReturn(const uint8_t*, const uint8_t*, const uint8_t*, uint8_t,
|
||||
uint8_t, const uint8_t*, size_t);
|
||||
Packet* createDatagram(uint8_t, const Identity&, const uint8_t*, const uint8_t*, size_t);
|
||||
};
|
||||
}
|
||||
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint8_t permissions = 3;
|
||||
uint32_t last_timestamp = 0, last_activity = 0;
|
||||
bool isAdmin() const { return (permissions & 7) == 3; }
|
||||
};
|
||||
struct ACL {
|
||||
std::vector<ClientInfo> clients;
|
||||
int getNumClients() const { return clients.size(); }
|
||||
ClientInfo* getClientByIdx(int index) { return &clients.at(index); }
|
||||
};
|
||||
struct Clock : mesh::RTCClock {
|
||||
uint32_t now = 100;
|
||||
uint32_t getCurrentTime() override { return now; }
|
||||
void setCurrentTime(uint32_t value) override { now = value; }
|
||||
};
|
||||
class MyMesh : public mesh::Mesh {
|
||||
public:
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD] = {};
|
||||
ACL acl;
|
||||
ClientInfo sender;
|
||||
Clock clock;
|
||||
bool refuse_queue = false;
|
||||
unsigned queued = 0;
|
||||
mesh::Packet* last_reply = nullptr;
|
||||
Clock* getRTCClock() { return &clock; }
|
||||
int handleRequest(ClientInfo*, uint32_t, uint8_t*, size_t,
|
||||
size_t = mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
void receive(mesh::Packet*, uint8_t*, size_t);
|
||||
bool admit(mesh::Packet* packet) {
|
||||
if (!packet || refuse_queue) return false;
|
||||
assert(packet->payload_len <= MAX_PACKET_PAYLOAD);
|
||||
last_reply = packet;
|
||||
++queued;
|
||||
return true;
|
||||
}
|
||||
bool sendFloodReply(mesh::Packet* packet, unsigned long, uint8_t) { return admit(packet); }
|
||||
bool sendClientReply(ClientInfo*, mesh::Packet* packet, unsigned long, uint8_t) { return admit(packet); }
|
||||
};
|
||||
|
||||
#include "production.inc"
|
||||
|
||||
static void fill(ACL& acl, unsigned count, bool sparse = false) {
|
||||
acl.clients.assign(count, ClientInfo{});
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
for (unsigned j = 0; j < PUB_KEY_SIZE; ++j) acl.clients[i].id.pub_key[j] = (i + j) % 255 + 1;
|
||||
if (sparse && i % 3 != 1) acl.clients[i].permissions = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned verify_entries(MyMesh& mesh, int length, size_t capacity) {
|
||||
assert(length >= 4 && size_t(length) <= capacity && (length - 4) % 7 == 0);
|
||||
uint32_t tag;
|
||||
memcpy(&tag, mesh.reply_data, 4);
|
||||
assert(tag == 51);
|
||||
unsigned offset = 4;
|
||||
for (const auto& client : mesh.acl.clients) {
|
||||
if (!client.permissions) continue;
|
||||
if (offset + 7 > size_t(length)) break;
|
||||
assert(!memcmp(mesh.reply_data + offset, client.id.pub_key, 6));
|
||||
assert(mesh.reply_data[offset + 6] == client.permissions);
|
||||
offset += 7;
|
||||
}
|
||||
assert(offset == unsigned(length));
|
||||
return (length - 4) / 7;
|
||||
}
|
||||
|
||||
static void print_legacy(const uint8_t* body, unsigned count, size_t length) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
std::cout << "LEGACY:" << count << ':';
|
||||
for (size_t i = 0; i < length; ++i) std::cout << hex[body[i] >> 4] << hex[body[i] & 15];
|
||||
std::cout << '\n';
|
||||
}
|
||||
|
||||
int main() {
|
||||
uint8_t query[] = {REQ_TYPE_GET_ACCESS_LIST, 0, 0};
|
||||
uint8_t secret[PUB_KEY_SIZE] = {};
|
||||
unsigned paths = 0;
|
||||
for (bool flood : {false, true}) {
|
||||
for (unsigned path_len = 0; path_len < 256; ++path_len) {
|
||||
if (!mesh::Packet::isValidPathLen(path_len)) {
|
||||
assert(mesh::clientACLReplyCapacity(flood, path_len) == 0);
|
||||
continue;
|
||||
}
|
||||
++paths;
|
||||
for (unsigned clients : {0, 1, 22, 23, 24, 25, 32, 256}) {
|
||||
for (bool sparse : {false, true}) {
|
||||
MyMesh mesh;
|
||||
fill(mesh.acl, clients, sparse);
|
||||
const size_t capacity = mesh::clientACLReplyCapacity(flood, path_len);
|
||||
const int length = mesh.handleRequest(&mesh.sender, 51, query, sizeof(query), capacity);
|
||||
const unsigned count = verify_entries(mesh, length, capacity);
|
||||
const unsigned live = std::count_if(mesh.acl.clients.begin(), mesh.acl.clients.end(),
|
||||
[](const ClientInfo& client) { return client.permissions != 0; });
|
||||
assert(count == std::min(live, unsigned((capacity - 4) / 7)));
|
||||
uint8_t path[MAX_PATH_SIZE] = {};
|
||||
mesh::Packet* packet = flood
|
||||
? mesh.createPathReturn(mesh.sender.id, secret, path, path_len,
|
||||
PAYLOAD_TYPE_RESPONSE, mesh.reply_data, length)
|
||||
: mesh.createDatagram(PAYLOAD_TYPE_RESPONSE, mesh.sender.id, secret, mesh.reply_data, length);
|
||||
assert(packet && packet->payload_len <= MAX_PACKET_PAYLOAD);
|
||||
if (path_len == 0 && !sparse) {
|
||||
// Decode the actual zero-padded encrypted response body, with the
|
||||
// reflected tag and flood path envelope removed by Companion.
|
||||
const size_t body_offset = 2 * PATH_HASH_SIZE + CIPHER_MAC_SIZE + 4 + (flood ? 2 : 0);
|
||||
print_legacy(packet->payload + body_offset, count, packet->payload_len - body_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(paths > 200); // Includes zero hops, 63 hops, 64 bytes and 2/3-byte hashes.
|
||||
|
||||
MyMesh mesh;
|
||||
fill(mesh.acl, 32);
|
||||
for (uint8_t permission : {0, 1, 2, 4, 5, 6, 7, 255}) {
|
||||
mesh.sender.permissions = permission;
|
||||
assert(mesh.handleRequest(&mesh.sender, 51, query, sizeof(query)) == 0);
|
||||
}
|
||||
mesh.sender.permissions = 3;
|
||||
uint8_t app_query[] = {REQ_TYPE_GET_ACCESS_LIST, 0, 0, 0xA1, 0xB2, 0xC3, 0xD4};
|
||||
const int app_reply = mesh.handleRequest(&mesh.sender, 51, app_query, sizeof(app_query));
|
||||
assert(verify_entries(mesh, app_reply, mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY) == 23);
|
||||
assert(mesh.handleRequest(nullptr, 51, query, sizeof(query)) == 0);
|
||||
for (size_t length : {0, 1, 2}) assert(mesh.handleRequest(&mesh.sender, 51, query, length) == 0);
|
||||
for (unsigned index : {1, 2}) {
|
||||
query[index] = 1;
|
||||
assert(mesh.handleRequest(&mesh.sender, 51, query, sizeof(query)) == 0);
|
||||
query[index] = 0;
|
||||
}
|
||||
for (size_t capacity = 0; capacity <= MAX_PACKET_PAYLOAD + 10; ++capacity) {
|
||||
const int length = mesh.handleRequest(&mesh.sender, 51, query, sizeof(query), capacity);
|
||||
if (capacity < 4) assert(length == 0);
|
||||
else verify_entries(mesh, length, std::min(capacity, mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY));
|
||||
}
|
||||
|
||||
// The exact receive branch keeps failed allocation/queue attempts retryable.
|
||||
for (bool flood : {false, true}) {
|
||||
MyMesh target;
|
||||
fill(target.acl, 32);
|
||||
target.sender.last_timestamp = 50;
|
||||
target.sender.last_activity = 12;
|
||||
mesh::Packet request;
|
||||
request.header = flood ? ROUTE_TYPE_FLOOD : ROUTE_TYPE_DIRECT;
|
||||
request.path_len = 0x60; // 32 two-byte hops: maximum 64 route bytes.
|
||||
uint8_t data[] = {51, 0, 0, 0, REQ_TYPE_GET_ACCESS_LIST, 0, 0};
|
||||
for (size_t length = 0; length < 7; ++length) target.receive(&request, data, length);
|
||||
assert(target.sender.last_timestamp == 50 && target.queued == 0);
|
||||
target.refuse_allocation = true;
|
||||
target.receive(&request, data, sizeof(data));
|
||||
assert(target.sender.last_timestamp == 50 && target.sender.last_activity == 12);
|
||||
target.refuse_allocation = false;
|
||||
target.refuse_queue = true;
|
||||
target.receive(&request, data, sizeof(data));
|
||||
assert(target.sender.last_timestamp == 50 && target.sender.last_activity == 12);
|
||||
target.refuse_queue = false;
|
||||
target.receive(&request, data, sizeof(data));
|
||||
assert(target.sender.last_timestamp == 51 && target.sender.last_activity == 100 && target.queued == 1);
|
||||
target.receive(&request, data, sizeof(data));
|
||||
data[0] = 49;
|
||||
target.receive(&request, data, sizeof(data));
|
||||
assert(target.sender.last_timestamp == 51 && target.queued == 1);
|
||||
}
|
||||
|
||||
// Login and the next request use the same production monotonic RTC helper.
|
||||
Clock clock;
|
||||
const uint32_t login_tag = clock.getCurrentTimeUnique();
|
||||
const uint32_t request_tag = clock.getCurrentTimeUnique();
|
||||
assert(request_tag > login_tag); // Same-second login does not collide.
|
||||
clock.setCurrentTime(10);
|
||||
assert(clock.getCurrentTimeUnique() > request_tag); // In-boot clock rollback remains monotonic.
|
||||
std::cout << "ACL route and replay checks passed\n";
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute legacy ACL replies through the production route and replay guards."""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/client_acl_response"
|
||||
|
||||
# Unmodified official decoder, meshcore-dev/meshcore_py commit
|
||||
# 1e6fdd47a36ae32c30246a8486b7a9492665579f, src/meshcore/parsing.py (MIT).
|
||||
# https://github.com/meshcore-dev/meshcore_py/blob/1e6fdd47a36ae32c30246a8486b7a9492665579f/src/meshcore/parsing.py
|
||||
def parse_acl(buf):
|
||||
i = 0
|
||||
res = []
|
||||
while i + 7 <= len(buf):
|
||||
key = buf[i : i + 6].hex()
|
||||
perm = buf[i + 6]
|
||||
if key != "000000000000":
|
||||
res.append({"key": key, "perm": perm})
|
||||
i = i + 7
|
||||
return res
|
||||
|
||||
|
||||
class ClientAclResponseTest(unittest.TestCase):
|
||||
def test_production_acl_route_admission_and_replay(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++17 compiler is required")
|
||||
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
||||
handler = extract_braced(source, "int MyMesh::handleRequest(")
|
||||
prefix = handler[:handler.index(" if (payload[0] == REQ_TYPE_GET_STATUS)")]
|
||||
acl = extract_braced(handler, "if (payload[0] == REQ_TYPE_GET_ACCESS_LIST)")
|
||||
generated = prefix + acl + "\nreturn 0;\n}\n"
|
||||
receive = extract_braced(source, "if (type == PAYLOAD_TYPE_REQ) { // request")
|
||||
generated += """
|
||||
void MyMesh::receive(mesh::Packet* packet, uint8_t* data, size_t len) {
|
||||
ClientInfo* client = &sender;
|
||||
const uint8_t type = PAYLOAD_TYPE_REQ;
|
||||
uint8_t secret[PUB_KEY_SIZE] = {};
|
||||
""" + receive + "\n}\n"
|
||||
mesh_source = (ROOT / "src/Mesh.cpp").read_text()
|
||||
generated += "namespace mesh {\n"
|
||||
generated += "#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)\n"
|
||||
for signature in (
|
||||
"Packet* Mesh::createPathReturn(const Identity& dest,",
|
||||
"Packet* Mesh::createPathReturn(const uint8_t* dest_hash,",
|
||||
"Packet* Mesh::createDatagram(",
|
||||
):
|
||||
generated += extract_braced(mesh_source, signature) + "\n"
|
||||
packet_source = (ROOT / "src/Packet.cpp").read_text()
|
||||
for signature in ("Packet::Packet()", "bool Packet::isValidPathLen("):
|
||||
generated += extract_braced(packet_source, signature) + "\n"
|
||||
generated += "}\n"
|
||||
with tempfile.TemporaryDirectory(prefix=".tmp-acl-response-", dir=ROOT) as directory:
|
||||
work = Path(directory)
|
||||
(work / "production.inc").write_text(generated, encoding="ascii")
|
||||
binary = work / "acl-response.exe"
|
||||
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
|
||||
f"-I{work}", f"-I{ROOT / 'src'}",
|
||||
str(FIXTURE / "test_client_acl_response.cpp"), "-o", str(binary)]
|
||||
if sys.platform.startswith("linux"):
|
||||
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"]
|
||||
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn("ACL route and replay checks passed", checked.stdout)
|
||||
decoded = 0
|
||||
for line in checked.stdout.splitlines():
|
||||
if not line.startswith("LEGACY:"):
|
||||
continue
|
||||
_, expected, body = line.split(":")
|
||||
result = parse_acl(bytes.fromhex(body))
|
||||
self.assertEqual(len(result), int(expected))
|
||||
self.assertTrue(all(entry["perm"] == 3 for entry in result))
|
||||
decoded += 1
|
||||
self.assertGreaterEqual(decoded, 10)
|
||||
|
||||
def test_admission_result_and_default_capacity_wiring(self):
|
||||
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
||||
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
|
||||
wrapper = extract_braced(source, "bool MyMesh::sendClientReply(")
|
||||
self.assertIn("return sendClientReplyWithFallbackScope(", wrapper)
|
||||
self.assertIn("bool sendClientReply(", header)
|
||||
self.assertIn("size_t reply_capacity = MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1)", header)
|
||||
self.assertIn("if (len < 5) return;", source)
|
||||
|
||||
def test_same_clock_generates_login_and_binary_request_tags(self):
|
||||
source = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text()
|
||||
for signature in ("int BaseChatMesh::sendLogin(",
|
||||
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t*"):
|
||||
body = extract_braced(source, signature)
|
||||
self.assertEqual(body.count("getRTCClock()->getCurrentTimeUnique()"), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user