Fit Access Control replies to encrypted packet route budgets

This commit is contained in:
mikecarper committed 2026-10-04 14:24:03 -07:00
1 parent 9268c57f8c
commit e4420519a4
6 files changed
+389 -15

No files matched your search

+1
View File
@@ -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: |
+27 -13
View File
@@ -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(&timestamp, 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");
+3 -2
View File
@@ -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);
+26
View File
@@ -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";
}
+105
View File
@@ -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()