Browse Source

Add public/channel content filtering to simple_repeater

Lets a repeater decrypt channels it holds the key for (incl. the
built-in public channel) and drop messages matching blocked keywords
or sender names before forwarding. Configured via new "filter" CLI
commands and persisted to /channel_filter.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
pull/2733/head
Kemal Hadimli 4 days ago
parent
commit
1abc715b40
  1. 230
      examples/simple_repeater/MyMesh.cpp
  2. 26
      examples/simple_repeater/MyMesh.h

230
examples/simple_repeater/MyMesh.cpp

@ -1,5 +1,50 @@
#include "MyMesh.h"
#include <algorithm>
#include <ctype.h>
/* --------------------- public-channel content filter ------------------ */
// The well-known MeshCore public channel PSK ("izOH6cXN6mrJ5e26oRXNcg==")
static const uint8_t PUBLIC_CHANNEL_SECRET[16] = {
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
};
static int b64Val(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static int decodeBase64(const char* in, uint8_t* out, int max_out) {
int bits = 0, nbits = 0, n = 0;
for (const char* p = in; *p && *p != '='; p++) {
int v = b64Val(*p);
if (v < 0) continue; // skip whitespace and other non-alphabet chars
bits = (bits << 6) | v;
nbits += 6;
if (nbits >= 8) {
nbits -= 8;
if (n >= max_out) return -1;
out[n++] = (bits >> nbits) & 0xFF;
}
}
return n;
}
static bool icontains(const char* haystack, const char* needle) {
if (!*needle) return false;
for (const char* h = haystack; *h; h++) {
const char* a = h;
const char* b = needle;
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
if (!*b) return true;
}
return false;
}
/* ------------------------------ Config -------------------------------- */
@ -627,6 +672,184 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) {
}
}
bool MyMesh::addFilterChannel(const char *psk_b64) {
if (num_filter_channels >= MAX_FILTER_CHANNELS) return false;
auto ch = &filter_channels[num_filter_channels];
memset(ch->secret, 0, sizeof(ch->secret));
int len;
if (strcmp(psk_b64, "public") == 0) {
memcpy(ch->secret, PUBLIC_CHANNEL_SECRET, sizeof(PUBLIC_CHANNEL_SECRET));
len = sizeof(PUBLIC_CHANNEL_SECRET);
} else {
len = decodeBase64(psk_b64, ch->secret, sizeof(ch->secret));
}
if (len != 16 && len != 32) return false;
mesh::Utils::sha256(ch->hash, sizeof(ch->hash), ch->secret, len);
StrHelper::strncpy(filter_channel_psk[num_filter_channels], psk_b64, FILTER_PSK_B64_LEN);
num_filter_channels++;
return true;
}
int MyMesh::searchChannelsByHash(const uint8_t *hash, mesh::GroupChannel channels[], int max_matches) {
int n = 0;
for (int i = 0; i < num_filter_channels && n < max_matches; i++) {
if (filter_channels[i].hash[0] == hash[0]) {
channels[n++] = filter_channels[i];
}
}
return n;
}
void MyMesh::onGroupDataRecv(mesh::Packet *packet, uint8_t type, const mesh::GroupChannel &channel,
uint8_t *data, size_t len) {
if (type != PAYLOAD_TYPE_GRP_TXT) return; // only inspect channel text messages
if (len < 6) return;
if ((data[4] >> 2) != 0) return; // not a plain-text message
data[len] = 0; // make a C string: "sender_name: text"
const char *msg = (const char *)&data[5];
const char *sep = strstr(msg, ": ");
const char *text = sep ? sep + 2 : msg;
bool blocked = false;
const char *reason = "keyword";
if (sep) {
char sender[FILTER_TERM_LEN];
int slen = sep - msg;
if (slen >= (int)sizeof(sender)) slen = sizeof(sender) - 1;
memcpy(sender, msg, slen);
sender[slen] = 0;
for (int i = 0; i < num_block_senders && !blocked; i++) {
if (icontains(sender, block_senders[i])) { blocked = true; reason = "sender"; }
}
}
for (int i = 0; i < num_block_keywords && !blocked; i++) {
if (icontains(text, block_keywords[i])) blocked = true;
}
if (blocked) {
packet->markDoNotRetransmit(); // routeRecvPacket() will now release instead of forwarding
n_filtered++;
if (_logging) {
File f = openAppend(PACKET_LOG_FILE);
if (f) {
f.print(getLogDateTime());
f.printf(": FILTERED (%s): %s\n", reason, msg);
f.close();
}
}
}
}
void MyMesh::loadChannelFilter() {
num_filter_channels = 0;
num_block_keywords = 0;
num_block_senders = 0;
#if defined(RP2040_PLATFORM)
File f = _fs->open(CHANNEL_FILTER_FILE, "r");
#else
File f = _fs->open(CHANNEL_FILTER_FILE);
#endif
if (!f) return;
char line[FILTER_PSK_B64_LEN + 8];
while (f.available()) {
int n = f.readBytesUntil('\n', (uint8_t *)line, sizeof(line) - 1);
line[n] = 0;
while (n > 0 && (line[n - 1] == '\r' || line[n - 1] == ' ')) line[--n] = 0;
if (n < 3 || line[1] != ' ') continue;
const char *val = &line[2];
if (line[0] == 'C') {
addFilterChannel(val);
} else if (line[0] == 'K' && num_block_keywords < MAX_FILTER_TERMS) {
StrHelper::strncpy(block_keywords[num_block_keywords++], val, FILTER_TERM_LEN);
} else if (line[0] == 'S' && num_block_senders < MAX_FILTER_TERMS) {
StrHelper::strncpy(block_senders[num_block_senders++], val, FILTER_TERM_LEN);
}
}
f.close();
}
void MyMesh::saveChannelFilter() {
_fs->remove(CHANNEL_FILTER_FILE);
File f = openAppend(CHANNEL_FILTER_FILE);
if (!f) return;
for (int i = 0; i < num_filter_channels; i++) f.printf("C %s\n", filter_channel_psk[i]);
for (int i = 0; i < num_block_keywords; i++) f.printf("K %s\n", block_keywords[i]);
for (int i = 0; i < num_block_senders; i++) f.printf("S %s\n", block_senders[i]);
f.close();
}
void MyMesh::handleFilterCommand(char *command, char *reply) {
char *arg = command + 6; // skip "filter"
while (*arg == ' ') arg++;
if (*arg == 0 || strcmp(arg, "list") == 0) {
char *dp = reply;
dp += sprintf(dp, "channels:%d keywords:%d senders:%d filtered:%u", num_filter_channels,
num_block_keywords, num_block_senders, (unsigned)n_filtered);
for (int i = 0; i < num_block_keywords && dp - reply < 120; i++) dp += sprintf(dp, "\nK:%s", block_keywords[i]);
for (int i = 0; i < num_block_senders && dp - reply < 120; i++) dp += sprintf(dp, "\nS:%s", block_senders[i]);
return;
}
if (memcmp(arg, "channel ", 8) == 0) {
char *val = arg + 8;
while (*val == ' ') val++;
if (strcmp(val, "clear") == 0) {
num_filter_channels = 0;
saveChannelFilter();
strcpy(reply, "OK - channels cleared");
} else if (addFilterChannel(val)) {
saveChannelFilter();
sprintf(reply, "OK - %d channel(s)", num_filter_channels);
} else {
strcpy(reply, "Err - bad PSK or list full");
}
return;
}
if (memcmp(arg, "block ", 6) == 0) {
char *val = arg + 6;
while (*val == ' ') val++;
if (*val == 0) { strcpy(reply, "Err - empty keyword"); return; }
if (num_block_keywords >= MAX_FILTER_TERMS) { strcpy(reply, "Err - keyword list full"); return; }
StrHelper::strncpy(block_keywords[num_block_keywords++], val, FILTER_TERM_LEN);
saveChannelFilter();
sprintf(reply, "OK - %d keyword(s)", num_block_keywords);
return;
}
if (memcmp(arg, "sender ", 7) == 0) {
char *val = arg + 7;
while (*val == ' ') val++;
if (*val == 0) { strcpy(reply, "Err - empty sender"); return; }
if (num_block_senders >= MAX_FILTER_TERMS) { strcpy(reply, "Err - sender list full"); return; }
StrHelper::strncpy(block_senders[num_block_senders++], val, FILTER_TERM_LEN);
saveChannelFilter();
sprintf(reply, "OK - %d sender(s)", num_block_senders);
return;
}
if (strcmp(arg, "clear") == 0) {
num_block_keywords = 0;
num_block_senders = 0;
saveChannelFilter();
strcpy(reply, "OK - blocks cleared");
return;
}
if (strcmp(arg, "reset") == 0) {
num_filter_channels = num_block_keywords = num_block_senders = 0;
saveChannelFilter();
strcpy(reply, "OK - filter reset");
return;
}
strcpy(reply, "Err - usage: filter [list|channel <b64|public|clear>|block <kw>|sender <name>|clear|reset]");
}
static bool isShare(const mesh::Packet *packet) {
if (packet->hasTransportCodes()) {
return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0; // codes { 0, 0 } means 'send to nowhere'
@ -862,6 +1085,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
{
last_millis = 0;
uptime_millis = 0;
num_filter_channels = 0;
num_block_keywords = 0;
num_block_senders = 0;
n_filtered = 0;
next_local_advert = next_flood_advert = 0;
dirty_contacts_expiry = 0;
set_radio_at = revert_radio_at = 0;
@ -930,6 +1157,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// load persisted prefs
_cli.loadPrefs(_fs);
acl.load(_fs, self_id);
loadChannelFilter();
// TODO: key_store.begin();
region_map.load(_fs);
@ -1257,6 +1485,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else if (memcmp(command, "filter", 6) == 0 && (command[6] == 0 || command[6] == ' ')) {
handleFilterCommand(command, reply);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}

26
examples/simple_repeater/MyMesh.h

@ -79,6 +79,16 @@ struct NeighbourInfo {
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
#define CHANNEL_FILTER_FILE "/channel_filter"
#ifndef MAX_FILTER_CHANNELS
#define MAX_FILTER_CHANNELS 4
#endif
#ifndef MAX_FILTER_TERMS
#define MAX_FILTER_TERMS 8
#endif
#define FILTER_TERM_LEN 24
#define FILTER_PSK_B64_LEN 48
class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
@ -113,6 +123,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t pending_sf;
uint8_t pending_cr;
int matching_peer_indexes[MAX_CLIENTS];
mesh::GroupChannel filter_channels[MAX_FILTER_CHANNELS];
char filter_channel_psk[MAX_FILTER_CHANNELS][FILTER_PSK_B64_LEN];
uint8_t num_filter_channels;
char block_keywords[MAX_FILTER_TERMS][FILTER_TERM_LEN];
uint8_t num_block_keywords;
char block_senders[MAX_FILTER_TERMS][FILTER_TERM_LEN];
uint8_t num_block_senders;
uint32_t n_filtered;
#if defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
@ -130,6 +149,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
bool addFilterChannel(const char* psk_b64);
void loadChannelFilter();
void saveChannelFilter();
void handleFilterCommand(char* command, char* reply);
protected:
float getAirtimeBudgetFactor() const override {
return _prefs.airtime_factor;
@ -167,6 +191,8 @@ protected:
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override;
void onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len);
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;

Loading…
Cancel
Save