Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build_as_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
src_filter = [
'+<*.cpp>',
'+<helpers/*.cpp>',
'+<helpers/packet_filter/*.cpp>',
'+<helpers/sensors>',
'+<helpers/radiolib/*.cpp>',
'+<helpers/ui/MomentaryButton.cpp>',
Expand Down
119 changes: 119 additions & 0 deletions docs/cli_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore
- [Operational](#operational)
- [Neighbors](#neighbors-repeater-only)
- [Statistics](#statistics)
- [Packet Filtering](#packet-filtering-repeater-only)
- [Logging](#logging)
- [Information](#info)
- [Configuration](#configuration)
Expand Down Expand Up @@ -144,6 +145,124 @@ This document provides an overview of CLI commands that can be sent to MeshCore

---

## Packet Filtering (Repeater Only)

Packet filters let a repeater decline to forward selected flood packets. Rules are evaluated before forwarding; packets generated by the local node are not affected. Rules are persisted on the node and reloaded at boot.

The maximum number of active rules is set by `PACKET_FILTER_MAX_RULES` at build time. The default is `20`.

### Show packet filter help
**Usage:** `block help`

**Response:**
```
block list [page]
block del <index>
block clear
block stats
block advert help
block channelmessage help
```

---

### List packet filter rules
**Usage:** `block list [page]`

**Parameters:**
- `page`: Optional page number. Defaults to `1`.

**Response:** One numbered rule per line, or `-none-` when no rules are active.

If there are more rules than fit in one response packet, the output ends with `..N`, where `N` is the next page number.

**Examples:**
```
block list
block list 2
```

Example paged response:
```
1: advert repeater 60 2
2: channelmessage #test *spam*
..2
```

---

### Delete a packet filter rule
**Usage:** `block del <index>`

**Parameters:**
- `index`: Rule index shown by `block list`.

Deletes the selected rule and updates the persisted rules file. The next added rule reuses the first inactive rule slot.

**Example:**
```
block del 1
```

---

### Clear packet filter rules
**Usage:** `block clear`

Clears all active rules and updates the persisted rules file.

---

### Show packet filter statistics
**Usage:** `block stats`

Shows the number of packets blocked since boot or since the filter statistics were reset by firmware.

---

### Rate-limit forwarded adverts
**Usage:** `block advert <advert_type> <period_minutes> [max]`

**Parameters:**
- `advert_type`: `companion`, `repeater`, `room`, or a numeric advert type from `1` to `15`
- `period_minutes`: Length of the rate-limit window in minutes (`1`-`65535`)
- `max`: Optional maximum adverts per sender public key in each window. Defaults to `1`.

**Example:**
```
block advert repeater 60 2
```

Allows each repeater to have at most two forwarded adverts per 60-minute window.

**Notes:**
- This rule only matches node advertisement packets with the selected advert type.
- The limiter uses fixed windows, not a rolling window. For a 60-minute period, the per-sender count resets when the next 60-minute bucket starts. A sender near a bucket boundary can therefore have up to `max` adverts forwarded at the end of one period and another `max` at the start of the next period.
- Sender public keys are tracked with salted Bloom filters. If a filter saturates, the rule temporarily fails open for the rest of the window instead of blocking packets incorrectly.

---

### Block public channel messages by pattern
**Usage:** `block channelmessage <channel_name> "<pattern>"`

**Parameters:**
- `channel_name`: A public channel name whose secret can be derived from the name. Examples: `Public`, `#test`.
- `pattern`: Text pattern to block. `*` matches zero or more characters and `?` matches one character. Matching is case-sensitive and must match the complete channel message text.

**Examples:**
```
block channelmessage #test "*spam*"
block channelmessage Public "badword*"
block channelmessage #test "BadUser: *"
block channelmessage #test "*: *BadWord*"
```

**Notes:**
- This rule decrypts only public channel messages whose channel secret can be derived from the channel name. It does not block private channels with random secrets.
- Channel message text includes the sender prefix used by MeshCore group text messages, normally `<sender name>: <message body>`. To block all messages from a specific displayed sender name, match that prefix explicitly, such as `BadUser: *`. To block messages containing a word regardless of sender, include the separator in the pattern, such as `*: *BadWord*`.

---

## Logging

### Begin capture of rx log to node storage
Expand Down
10 changes: 10 additions & 0 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ bool MyMesh::allowPacketForward(const mesh::Packet *packet) {
return false;
}
}
if (!packet_filter.shouldRepeatPacket(packet)) {
MESH_DEBUG_PRINTLN("allowPacketForward: packet blocked by packet filter");
return false;
}
return true;
}

Expand Down Expand Up @@ -845,6 +849,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
mesh::RTCClock &rtc, mesh::MeshTables &tables)
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
region_map(key_store), temp_map(key_store),
packet_filter(&rng),
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
telemetry(MAX_PACKET_PAYLOAD - 4),
discover_limiter(4, 120), // max 4 every 2 minutes
Expand Down Expand Up @@ -926,6 +931,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
acl.load(_fs, self_id);
// TODO: key_store.begin();
region_map.load(_fs);
packet_filter.load(_fs, PACKET_FILTER_RULES_FILE);

// establish default-scope
{
Expand Down Expand Up @@ -1163,6 +1169,7 @@ void MyMesh::clearStats() {
radio_driver.resetStats();
resetStats();
((SimpleMeshTables *)getTables())->resetStats();
packet_filter.resetStats();
}

void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) {
Expand Down Expand Up @@ -1251,6 +1258,9 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sendNodeDiscoverReq();
strcpy(reply, "OK - Discover sent");
}
} else if (memcmp(command, "block", 5) == 0 && (command[5] == 0 || command[5] == ' ')) {
// Packet filtering coommands.
packet_filter.handleBlockCommand(_fs, PACKET_FILTER_RULES_FILE, command, reply, 160);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <helpers/StatsFormatHelper.h>
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include <helpers/PacketFilter.h>
#include "RateLimiter.h"

#ifdef WITH_BRIDGE
Expand Down Expand Up @@ -79,6 +80,7 @@ struct NeighbourInfo {
#define FIRMWARE_ROLE "repeater"

#define PACKET_LOG_FILE "/packet_log"
#define PACKET_FILTER_RULES_FILE "/packet_filter"

class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
FILESYSTEM* _fs;
Expand All @@ -95,6 +97,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
uint8_t reply_path_hash_size;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
PacketFilter packet_filter;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
TransportKey default_scope;
Expand Down
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1
build_src_filter =
+<*.cpp>
+<helpers/*.cpp>
+<helpers/packet_filter/*.cpp>
+<helpers/radiolib/*.cpp>
+<helpers/bridges/BridgeBase.cpp>
+<helpers/ui/MomentaryButton.cpp>
Expand Down
37 changes: 37 additions & 0 deletions src/GroupChannel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "GroupChannel.h"
#include "Utils.h"

#define DEFAULT_PUBLIC_CHANNEL_SECRET_HEX "8b3387e9c5cdea6ac9e5edbaa115cd72"

namespace mesh {

bool GroupChannel::deriveHash(int secret_len) {
if (secret_len != 16 && secret_len != 32) return false;

Utils::sha256(hash, PATH_HASH_SIZE, secret, secret_len);
return true;
}

void GroupChannel::deriveHash() {
static uint8_t zeroes[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
if (memcmp(&secret[16], zeroes, 16) == 0) {
deriveHash(16);
} else {
deriveHash(32);
}
}

bool GroupChannel::derivePublicSecret(const char* name, uint8_t* dest_secret) {
if (name == NULL || dest_secret == NULL) return false;

memset(dest_secret, 0, PUB_KEY_SIZE);

if (strcmp(name, "Public") == 0) {
return Utils::fromHex(dest_secret, 16, DEFAULT_PUBLIC_CHANNEL_SECRET_HEX);
}

Utils::sha256(dest_secret, 16, (const uint8_t*)name, strlen(name));
return true;
}

}
17 changes: 17 additions & 0 deletions src/GroupChannel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <MeshCore.h>
#include <string.h>

namespace mesh {

class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];

void deriveHash();
bool deriveHash(int secret_len);
static bool derivePublicSecret(const char* name, uint8_t* dest_secret);
};
}
7 changes: 1 addition & 6 deletions src/Mesh.h
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
#pragma once

#include <Dispatcher.h>
#include <GroupChannel.h>

namespace mesh {

class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
};

/**
* An abstraction of the data tables needed to be maintained
*/
Expand Down
10 changes: 2 additions & 8 deletions src/helpers/BaseChatMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,7 @@ ChannelDetails* BaseChatMesh::addChannel(const char* name, const char* psk_base6
memset(dest->channel.secret, 0, sizeof(dest->channel.secret));
int len = decode_base64((unsigned char *) psk_base64, strlen(psk_base64), dest->channel.secret);
if (len == 32 || len == 16) {
mesh::Utils::sha256(dest->channel.hash, sizeof(dest->channel.hash), dest->channel.secret, len);
dest->channel.deriveHash(len);
StrHelper::strncpy(dest->name, name, sizeof(dest->name));
num_channels++;
return dest;
Expand All @@ -876,15 +876,9 @@ bool BaseChatMesh::getChannel(int idx, ChannelDetails& dest) {
return false;
}
bool BaseChatMesh::setChannel(int idx, const ChannelDetails& src) {
static uint8_t zeroes[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };

if (idx >= 0 && idx < MAX_GROUP_CHANNELS) {
channels[idx] = src;
if (memcmp(&src.channel.secret[16], zeroes, 16) == 0) {
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 16); // 128-bit key
} else {
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 32); // 256-bit key
}
channels[idx].channel.deriveHash();
return true;
}
return false;
Expand Down
Loading