This commit is contained in:
OverkillFPV 2026-04-20 21:10:24 +10:00 committed by GitHub
commit 80dfa37b47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 432 additions and 0 deletions

View file

@ -43,6 +43,9 @@
#define FIRMWARE_VER_LEVEL 2
#define PATH_BLACKLIST_FILE "/path_bl"
#define CHAN_BLACKLIST_FILE "/chan_bl"
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
@ -545,6 +548,295 @@ uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
return getRNG()->nextInt(0, 5*t + 1);
}
/* ----------------------- Blacklist helpers -------------------------------- */
// Returns true if any path entry in the packet matches a path-prefix blacklist entry.
// Comparison uses min(entry.len, hash_size) bytes, so shorter entries match any hash size.
bool MyMesh::isPathBlacklisted(const mesh::Packet* packet) const {
uint8_t hash_size = packet->getPathHashSize();
uint8_t hash_count = packet->getPathHashCount();
const uint8_t* ptr = packet->path;
for (uint8_t h = 0; h < hash_count; h++, ptr += hash_size) {
for (int b = 0; b < MAX_BLACKLIST_ENTRIES; b++) {
if (_path_blacklist[b].len == 0) continue;
uint8_t cmp_len = (_path_blacklist[b].len < hash_size) ? _path_blacklist[b].len : hash_size;
if (memcmp(ptr, _path_blacklist[b].prefix, cmp_len) == 0) return true;
}
}
return false;
}
// Returns true if the channel hash at the start of a GRP_TXT/GRP_DATA payload matches
// a channel-hash blacklist entry (hex prefix or #channel_name with decrypt verification).
bool MyMesh::isChanBlacklisted(const mesh::Packet* packet) const {
uint8_t pt = packet->getPayloadType();
if (pt != PAYLOAD_TYPE_GRP_TXT && pt != PAYLOAD_TYPE_GRP_DATA) return false;
if (packet->payload_len < PATH_HASH_SIZE) return false;
// Check hex prefix entries
for (int b = 0; b < MAX_BLACKLIST_ENTRIES; b++) {
if (_chan_blacklist[b].len == 0) continue;
uint8_t avail = (packet->payload_len < MAX_PATH_PREFIX_LEN) ? (uint8_t)packet->payload_len : MAX_PATH_PREFIX_LEN;
uint8_t cmp_len = (_chan_blacklist[b].len < avail) ? _chan_blacklist[b].len : avail;
if (memcmp(packet->payload, _chan_blacklist[b].prefix, cmp_len) == 0) return true;
}
// Check #channel_name entries with test decryption
if (_num_chan_name_filters > 0 && packet->payload_len > PATH_HASH_SIZE + CIPHER_MAC_SIZE) {
uint8_t channel_hash = packet->payload[0];
const uint8_t* macAndData = &packet->payload[PATH_HASH_SIZE]; // MAC + encrypted data
int enc_len = packet->payload_len - PATH_HASH_SIZE;
for (int i = 0; i < _num_chan_name_filters; i++) {
// Quick hash check first
if (channel_hash != _chan_name_filters[i].hash[0]) continue;
// Try to decrypt to verify this is actually the channel
uint8_t tmp[MAX_PACKET_PAYLOAD];
int len = mesh::Utils::MACThenDecrypt(_chan_name_filters[i].secret, tmp, macAndData, enc_len);
if (len > 0) return true; // Successfully decrypted, confirmed channel match
}
}
return false;
}
bool MyMesh::addToBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len) {
if (len == 0 || len > MAX_PATH_PREFIX_LEN) return false;
// if already present, consider success
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (list[i].len == len && memcmp(list[i].prefix, prefix, len) == 0) return true;
}
// find an empty slot
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (list[i].len == 0) {
list[i].len = len;
memcpy(list[i].prefix, prefix, len);
return true;
}
}
return false; // list is full
}
bool MyMesh::removeFromBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len) {
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (list[i].len == len && memcmp(list[i].prefix, prefix, len) == 0) {
list[i].len = 0; // mark slot as empty
return true;
}
}
return false;
}
void MyMesh::formatBlacklist(const BlacklistEntry* list, char* reply) {
char* dp = reply;
int count = 0;
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (list[i].len == 0) continue;
if (count > 0) *dp++ = '\n';
mesh::Utils::toHex(dp, list[i].prefix, list[i].len);
dp += list[i].len * 2;
count++;
}
if (count == 0) {
strcpy(reply, "-none-");
} else {
*dp = 0;
}
}
void MyMesh::loadBlacklist(const char* fname, BlacklistEntry* list) {
memset(list, 0, sizeof(BlacklistEntry) * MAX_BLACKLIST_ENTRIES);
#if defined(RP2040_PLATFORM)
File f = _fs->open(fname, "r");
#else
File f = _fs->open(fname);
#endif
if (!f) return;
int idx = 0;
char line[MAX_PATH_PREFIX_LEN * 2 + 4];
int line_len = 0;
while (f.available() && idx < MAX_BLACKLIST_ENTRIES) {
int c = f.read();
if (c < 0) break;
if (c == '\n' || c == '\r') {
if (line_len >= 2 && (line_len % 2 == 0)) {
line[line_len] = 0;
uint8_t prefix[MAX_PATH_PREFIX_LEN];
int byte_len = line_len / 2;
if (byte_len <= MAX_PATH_PREFIX_LEN && mesh::Utils::fromHex(prefix, byte_len, line)) {
list[idx].len = (uint8_t)byte_len;
memcpy(list[idx].prefix, prefix, byte_len);
idx++;
}
}
line_len = 0;
} else if (line_len < (int)(sizeof(line) - 1)) {
line[line_len++] = (char)c;
}
}
// handle last line with no trailing newline
if (line_len >= 2 && (line_len % 2 == 0) && idx < MAX_BLACKLIST_ENTRIES) {
line[line_len] = 0;
uint8_t prefix[MAX_PATH_PREFIX_LEN];
int byte_len = line_len / 2;
if (byte_len <= MAX_PATH_PREFIX_LEN && mesh::Utils::fromHex(prefix, byte_len, line)) {
list[idx].len = (uint8_t)byte_len;
memcpy(list[idx].prefix, prefix, byte_len);
}
}
f.close();
}
void MyMesh::saveBlacklist(const char* fname, const BlacklistEntry* list) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(fname);
File f = _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File f = _fs->open(fname, "w");
#else
File f = _fs->open(fname, "w", true);
#endif
if (!f) return;
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (list[i].len == 0) continue;
char hex[MAX_PATH_PREFIX_LEN * 2 + 2];
mesh::Utils::toHex(hex, list[i].prefix, list[i].len);
f.println(hex);
}
f.close();
}
/* ------------------- Channel name filter helpers -------------------------- */
void MyMesh::deriveChanNameFilter(ChanNameFilter& entry, const char* name) {
// Derive channel secret: first 16 bytes of sha256(name), zero-padded to PUB_KEY_SIZE
// to match GroupChannel.secret layout (MACThenDecrypt reads PUB_KEY_SIZE bytes)
uint8_t full_hash[32];
mesh::Utils::sha256(full_hash, 32, (const uint8_t*)name, strlen(name));
memset(entry.secret, 0, sizeof(entry.secret));
memcpy(entry.secret, full_hash, CIPHER_KEY_SIZE);
// Derive channel hash from the secret (using 16-byte key length, matching addChannel)
mesh::Utils::sha256(entry.hash, sizeof(entry.hash), entry.secret, CIPHER_KEY_SIZE);
StrHelper::strncpy(entry.name, name, sizeof(entry.name));
}
bool MyMesh::addChanNameFilter(const char* name) {
// Check for duplicate
for (int i = 0; i < _num_chan_name_filters; i++) {
if (strcmp(_chan_name_filters[i].name, name) == 0) return true; // already exists
}
if (_num_chan_name_filters >= MAX_CHAN_NAME_FILTERS) return false; // full
deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], name);
_num_chan_name_filters++;
return true;
}
bool MyMesh::removeChanNameFilter(const char* name) {
for (int i = 0; i < _num_chan_name_filters; i++) {
if (strcmp(_chan_name_filters[i].name, name) == 0) {
// shift remaining entries down
for (int j = i; j < _num_chan_name_filters - 1; j++) {
_chan_name_filters[j] = _chan_name_filters[j + 1];
}
_num_chan_name_filters--;
memset(&_chan_name_filters[_num_chan_name_filters], 0, sizeof(ChanNameFilter));
return true;
}
}
return false;
}
void MyMesh::loadChanBlacklist(const char* fname) {
// Load both hex prefix entries and #channel_name entries from the same file
loadBlacklist(fname, _chan_blacklist); // loads hex entries into _chan_blacklist
// Now re-read the file to pick up #channel_name lines
_num_chan_name_filters = 0;
memset(_chan_name_filters, 0, sizeof(_chan_name_filters));
#if defined(RP2040_PLATFORM)
File f = _fs->open(fname, "r");
#else
File f = _fs->open(fname);
#endif
if (!f) return;
char line[40];
int line_len = 0;
while (f.available() && _num_chan_name_filters < MAX_CHAN_NAME_FILTERS) {
int c = f.read();
if (c < 0) break;
if (c == '\n' || c == '\r') {
if (line_len > 0 && line[0] == '#') {
line[line_len] = 0;
deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], line);
_num_chan_name_filters++;
}
line_len = 0;
} else if (line_len < (int)(sizeof(line) - 1)) {
line[line_len++] = (char)c;
}
}
// handle last line with no trailing newline
if (line_len > 0 && line[0] == '#' && _num_chan_name_filters < MAX_CHAN_NAME_FILTERS) {
line[line_len] = 0;
deriveChanNameFilter(_chan_name_filters[_num_chan_name_filters], line);
_num_chan_name_filters++;
}
f.close();
}
void MyMesh::saveChanBlacklist(const char* fname) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(fname);
File f = _fs->open(fname, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File f = _fs->open(fname, "w");
#else
File f = _fs->open(fname, "w", true);
#endif
if (!f) return;
// Write hex prefix entries
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (_chan_blacklist[i].len == 0) continue;
char hex[MAX_PATH_PREFIX_LEN * 2 + 2];
mesh::Utils::toHex(hex, _chan_blacklist[i].prefix, _chan_blacklist[i].len);
f.println(hex);
}
// Write #channel_name entries
for (int i = 0; i < _num_chan_name_filters; i++) {
f.println(_chan_name_filters[i].name);
}
f.close();
}
void MyMesh::formatChanBlacklist(char* reply) {
char* dp = reply;
char* end = reply + MAX_PACKET_PAYLOAD - 6; // leave room for null + safety
int count = 0;
// Format hex prefix entries
for (int i = 0; i < MAX_BLACKLIST_ENTRIES; i++) {
if (_chan_blacklist[i].len == 0) continue;
if (dp + _chan_blacklist[i].len * 2 + 1 >= end) break;
if (count > 0) *dp++ = '\n';
mesh::Utils::toHex(dp, _chan_blacklist[i].prefix, _chan_blacklist[i].len);
dp += _chan_blacklist[i].len * 2;
count++;
}
// Format #channel_name entries
for (int i = 0; i < _num_chan_name_filters; i++) {
int len = strlen(_chan_name_filters[i].name);
if (dp + len + 1 >= end) break;
if (count > 0) *dp++ = '\n';
memcpy(dp, _chan_name_filters[i].name, len);
dp += len;
count++;
}
if (count == 0) {
strcpy(reply, "-none-");
} else {
*dp = 0;
}
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
// just try to determine region for packet (apply later in allowPacketForward())
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
@ -558,6 +850,19 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) {
} else {
recv_pkt_region = NULL;
}
// Drop packets whose path contains a blacklisted node prefix
if (isPathBlacklisted(pkt)) {
MESH_DEBUG_PRINTLN("filterRecvFloodPacket: path prefix blacklisted, dropping!");
return true;
}
// Drop group-channel packets whose channel hash is blacklisted
if (isChanBlacklisted(pkt)) {
MESH_DEBUG_PRINTLN("filterRecvFloodPacket: channel hash blacklisted, dropping!");
return true;
}
// do normal processing
return false;
}
@ -864,6 +1169,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_logging = false;
region_load_active = false;
memset(_path_blacklist, 0, sizeof(_path_blacklist));
memset(_chan_blacklist, 0, sizeof(_chan_blacklist));
memset(_chan_name_filters, 0, sizeof(_chan_name_filters));
_num_chan_name_filters = 0;
#if MAX_NEIGHBOURS
memset(neighbours, 0, sizeof(neighbours));
#endif
@ -926,6 +1236,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
acl.load(_fs, self_id);
// TODO: key_store.begin();
region_map.load(_fs);
loadBlacklist(PATH_BLACKLIST_FILE, _path_blacklist);
loadChanBlacklist(CHAN_BLACKLIST_FILE);
// establish default-scope
{
@ -1251,6 +1563,93 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else if (memcmp(command, "blacklist ", 10) == 0) {
// Commands:
// blacklist path list
// blacklist path add <hex>[,<hex>,...] (each hex = 2-8 even chars → 1-4 bytes)
// blacklist path rem <hex>[,<hex>,...]
// blacklist path clear
// blacklist chan list
// blacklist chan add <hex|#name>[,<hex|#name>,...]
// blacklist chan rem <hex|#name>[,<hex|#name>,...]
// blacklist chan clear
const char* parts[5];
int n = mesh::Utils::parseTextParts(command, parts, 5, ' ');
BlacklistEntry* list = NULL;
const char* list_file = NULL;
bool is_chan = false;
if (n >= 2 && strcmp(parts[1], "path") == 0) {
list = _path_blacklist; list_file = PATH_BLACKLIST_FILE;
} else if (n >= 2 && strcmp(parts[1], "chan") == 0) {
list = _chan_blacklist; list_file = CHAN_BLACKLIST_FILE;
is_chan = true;
}
if (list && n >= 3 && strcmp(parts[2], "list") == 0) {
if (is_chan) {
formatChanBlacklist(reply);
} else {
formatBlacklist(list, reply);
}
} else if (list && n >= 3 && strcmp(parts[2], "clear") == 0) {
memset(list, 0, sizeof(BlacklistEntry) * MAX_BLACKLIST_ENTRIES);
if (is_chan) {
_num_chan_name_filters = 0;
memset(_chan_name_filters, 0, sizeof(_chan_name_filters));
saveChanBlacklist(list_file);
} else {
saveBlacklist(list_file, list);
}
strcpy(reply, "OK");
} else if (list && n >= 4 && (strcmp(parts[2], "add") == 0 || strcmp(parts[2], "rem") == 0)) {
bool is_add = (parts[2][0] == 'a');
// parts[3] may be a comma-separated list of hex entries or #channel_name entries
char tokens[MAX_PATH_PREFIX_LEN * 2 * MAX_BLACKLIST_ENTRIES + MAX_BLACKLIST_ENTRIES + 2];
strncpy(tokens, parts[3], sizeof(tokens) - 1);
tokens[sizeof(tokens) - 1] = 0;
const char* tok_parts[MAX_BLACKLIST_ENTRIES];
int tok_n = mesh::Utils::parseTextParts(tokens, tok_parts, MAX_BLACKLIST_ENTRIES, ',');
bool any_ok = false, any_err = false;
for (int t = 0; t < tok_n; t++) {
if (is_chan && tok_parts[t][0] == '#') {
// #channel_name entry
bool ok = is_add ? addChanNameFilter(tok_parts[t])
: removeChanNameFilter(tok_parts[t]);
if (ok) any_ok = true; else any_err = true;
} else {
int hex_str_len = strlen(tok_parts[t]);
if (hex_str_len < 2 || hex_str_len > MAX_PATH_PREFIX_LEN * 2 || (hex_str_len % 2) != 0) {
any_err = true; continue;
}
uint8_t prefix[MAX_PATH_PREFIX_LEN];
int byte_len = hex_str_len / 2;
if (!mesh::Utils::fromHex(prefix, byte_len, tok_parts[t])) {
any_err = true; continue;
}
bool ok = is_add ? addToBlacklist(list, prefix, (uint8_t)byte_len)
: removeFromBlacklist(list, prefix, (uint8_t)byte_len);
if (ok) any_ok = true; else any_err = true;
}
}
// auto-save on any successful mutation
if (any_ok) {
if (is_chan) {
saveChanBlacklist(list_file);
} else {
saveBlacklist(list_file, list);
}
}
if (any_ok && !any_err) strcpy(reply, "OK");
else if (any_ok && any_err) strcpy(reply, "OK (partial)");
else strcpy(reply, is_add ? "Err - list full or bad input" : "Err - not found or bad input");
} else {
strcpy(reply, "Err - usage: blacklist <path|chan> <list|add|rem|clear> [hex|#name[,...]]");
}
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}

View file

@ -61,6 +61,21 @@ struct RepeaterStats {
#define MAX_CLIENTS 32
#endif
#define MAX_PATH_PREFIX_LEN 4
#define MAX_BLACKLIST_ENTRIES 16
#define MAX_CHAN_NAME_FILTERS 8
struct BlacklistEntry {
uint8_t len; // 0 = empty slot
uint8_t prefix[MAX_PATH_PREFIX_LEN];
};
struct ChanNameFilter {
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
char name[32];
};
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
@ -103,6 +118,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
unsigned long pending_discover_until;
bool region_load_active;
unsigned long dirty_contacts_expiry;
BlacklistEntry _path_blacklist[MAX_BLACKLIST_ENTRIES];
BlacklistEntry _chan_blacklist[MAX_BLACKLIST_ENTRIES];
ChanNameFilter _chan_name_filters[MAX_CHAN_NAME_FILTERS];
int _num_chan_name_filters;
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
@ -130,6 +149,20 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
bool isPathBlacklisted(const mesh::Packet* packet) const;
bool isChanBlacklisted(const mesh::Packet* packet) const;
void loadBlacklist(const char* fname, BlacklistEntry* list);
void saveBlacklist(const char* fname, const BlacklistEntry* list);
bool addToBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len);
bool removeFromBlacklist(BlacklistEntry* list, const uint8_t* prefix, uint8_t len);
void formatBlacklist(const BlacklistEntry* list, char* reply);
void deriveChanNameFilter(ChanNameFilter& entry, const char* name);
bool addChanNameFilter(const char* name);
bool removeChanNameFilter(const char* name);
void loadChanBlacklist(const char* fname);
void saveChanBlacklist(const char* fname);
void formatChanBlacklist(char* reply);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;