Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d30b06d
Block coordinate traffic on configured event channels
ayysasha Jul 17, 2026
f6bd014
Suppress event coordinates in reliable relay paths
ayysasha Jul 17, 2026
2837404
Reject blocked phone coordinates before rate limiting
ayysasha Jul 17, 2026
7ad2d44
Prevent event coordinates from reaching MQTT
ayysasha Jul 17, 2026
f9762a3
Add event coordinate policy preference
ayysasha Jul 17, 2026
ef26f05
Test event coordinate policy in native CI
ayysasha Jul 17, 2026
62253e1
Merge branch 'develop' into feature/event-channel-coordinate-policy
thebentern Jul 17, 2026
b0e2448
Merge branch 'develop' into feature/event-channel-coordinate-policy
thebentern Jul 17, 2026
6c941f3
Make event policy test tolerate a full NodeDB
ayysasha Jul 17, 2026
9d35291
Test Router event coordinate enforcement
ayysasha Jul 17, 2026
0f58722
Test PhoneAPI event coordinate retry handling
ayysasha Jul 17, 2026
0e66177
Test reliable event coordinate suppression
ayysasha Jul 17, 2026
5274efe
Test MQTT event coordinate suppression
ayysasha Jul 17, 2026
470c40b
Run event policy behavioral suites in native CI
ayysasha Jul 17, 2026
6d8943a
Merge develop into feature/event-channel-coordinate-policy
ayysasha Jul 23, 2026
5022977
Merge branch 'develop' into feature/event-channel-coordinate-policy
thebentern Jul 24, 2026
53176b0
Merge branch 'develop' into feature/event-channel-coordinate-policy
ayysasha Jul 24, 2026
a5fb189
Merge develop into feature/event-channel-coordinate-policy
ayysasha Jul 29, 2026
a50bfc1
Merge develop into feature/event-channel-coordinate-policy
thebentern Aug 9, 2026
a3b473f
tests: address CodeRabbit review feedback
thebentern Aug 9, 2026
44167d2
Merge branch 'develop' into feature/event-channel-coordinate-policy
thebentern Aug 11, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/test_native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,16 @@ jobs:
lcov ${{ env.LCOV_CAPTURE_FLAGS }} --test-name tests --output-file coverage_tests.info
sed -i -e "s#${PWD}#.#" coverage_tests.info # Make paths relative.

- name: Event channel policy tests
run: platformio test -e coverage-event-policy -v --junit-output-path event-policy-testreport.xml

- name: Save test results
if: always() # run this step even if previous step failed
uses: actions/upload-artifact@v7
with:
name: platformio-test-report-${{ steps.version.outputs.long }}
overwrite: true
path: ./testreport.xml
path: ./*testreport.xml

- name: Save coverage information
uses: actions/upload-artifact@v7
Expand Down
15 changes: 15 additions & 0 deletions src/mesh/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,21 @@ bool Channels::isWellKnownChannel(ChannelIndex chIndex)
return false;
}

bool Channels::isEventChannel(ChannelIndex chIndex)
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
static const uint8_t configuredEventPsk[] = USERPREFS_CHANNEL_0_PSK;
static_assert(sizeof(configuredEventPsk) == 16 || sizeof(configuredEventPsk) == 32,
"USERPREFS_CHANNEL_0_PSK must be an AES-128 or AES-256 key");
CryptoKey effectiveKey = getKey(chIndex);
return effectiveKey.length == sizeof(configuredEventPsk) &&
memcmp(effectiveKey.bytes, configuredEventPsk, sizeof(configuredEventPsk)) == 0;
#else
(void)chIndex;
return false;
#endif
}

bool Channels::hasDefaultChannel()
{
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
Expand Down
13 changes: 12 additions & 1 deletion src/mesh/Channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
#include "mesh-pb-constants.h"
#include <Arduino.h>

#ifndef USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
#define USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL USERPREFS_EVENT_MODE
Comment thread
ayysasha marked this conversation as resolved.
#endif

#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && !defined(USERPREFS_CHANNEL_0_PSK)
#error "USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL requires USERPREFS_CHANNEL_0_PSK"
#endif

/** A channel number (index into the channel table)
*/
typedef uint8_t ChannelIndex;
Expand Down Expand Up @@ -95,6 +103,9 @@ class Channels
// matches the current preset's name and PSK byte 1.
bool isWellKnownChannel(ChannelIndex chIndex);

// Returns true if this channel's effective key matches USERPREFS_CHANNEL_0_PSK.
bool isEventChannel(ChannelIndex chIndex);

// Returns true if we can be reached via a channel with the default settings given a region and modem preset
bool hasDefaultChannel();

Expand Down Expand Up @@ -164,4 +175,4 @@ bool channelFileUsesPublicKey(const meshtastic_ChannelFile &cf, ChannelIndex chI

static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36,
0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74,
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
11 changes: 10 additions & 1 deletion src/mesh/NextHopRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
if (isRepeated) {
if (!findInTxQueue(p->from, p->id)) {
if (reprocessPacket(p) && !perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
if (reprocessPacket(p) && !isBlockedEventCoordinatePacket(p) && !perhapsRebroadcast(p) && isToUs(p) &&
p->want_ack) {
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
}
}
Expand Down Expand Up @@ -190,6 +191,14 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
/* Check if we should be rebroadcasting this packet if so, do so. */
bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
// Never relay coordinate-bearing packets on the event ("everyone") channel.
// Closes the reliable-retransmit-dupe path that runs before handleReceived().
if (isBlockedEventCoordinatePacket(p)) {
return false;
}
#endif

// Check if traffic management wants to exhaust this packet's hops
bool exhaustHops = false;
#if HAS_TRAFFIC_MANAGEMENT
Expand Down
10 changes: 10 additions & 0 deletions src/mesh/PhoneAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,16 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
}
#endif

// Reject before recording duplicate or per-port cooldown state, so a blocked
// attempt cannot throttle a valid private-channel position retry.
if (isBlockedEventCoordinatePacket(&p)) {
LOG_DEBUG("Suppress phone coordinate send on event (everyone) channel");
meshtastic_QueueStatus qs = router->getQueueStatus();
service->sendQueueStatusToPhone(qs, 0, p.id);
sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Location sharing is disabled on this channel");
return false;
}

#if defined(ARCH_PORTDUINO)
// For use with the simulator, we should not ignore duplicate packets from the phone
if (SimRadio::instance == nullptr)
Expand Down
4 changes: 4 additions & 0 deletions src/mesh/PositionPrecision.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)

uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
{
// Event-channel privacy takes precedence over every stored precision and key policy.
if (channels.isEventChannel(channelIndex))
return 0;

const meshtastic_Channel &ch = channels.getByIndex(channelIndex);
if (ch.role == meshtastic_Channel_Role_DISABLED)
return 0;
Expand Down
6 changes: 6 additions & 0 deletions src/mesh/ReliableRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
*/
ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
{
if (isBlockedEventCoordinatePacket(p)) {
LOG_DEBUG("Suppress reliable coordinate send on event (everyone) channel");
packetPool.release(p);
return meshtastic_Routing_Error_NOT_AUTHORIZED;
}

const GlobalPacketId key(p);
const bool retransmitting = p->want_ack;

Expand Down
74 changes: 70 additions & 4 deletions src/mesh/Router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,52 @@ Allocator<meshtastic_MeshPacket> &packetPool = staticPool;

static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));

static ChannelIndex getEffectiveChannelIndex(const meshtastic_MeshPacket *p)
{
ChannelIndex chIndex = p->channel;
if (nodeDB && isFromUs(p) && !chIndex && !p->pki_encrypted && !isBroadcast(p->to)) {
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
if (node)
chIndex = node->channel;
}
return chIndex;
}

bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p)
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
if (p->pki_encrypted || willUsePki(p)) {
return false;
}
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
return isCoordinatePortnum(p->decoded.portnum) && channels.isEventChannel(getEffectiveChannelIndex(p));
}
return false;
#else
(void)p;
return false;
#endif
}

bool willUsePki(const meshtastic_MeshPacket *p)
{
#if !(MESHTASTIC_EXCLUDE_PKI)
if (p->which_payload_variant != meshtastic_MeshPacket_decoded_tag || !isFromUs(p))
return false;
bool haveDestKey = false;
if (p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP) {
meshtastic_NodeInfoLite_public_key_t destKey = {0, {0}};
haveDestKey = nodeDB->copyPublicKey(p->to, destKey);
if (!haveDestKey && p->pki_encrypted)
haveDestKey = crypto->getPendingPublicKey(p->to, destKey);
}
return wouldEncryptWithPKC(p, getEffectiveChannelIndex(p), haveDestKey);
#else
(void)p;
return false;
#endif
}

struct RoutingAuthCache {
bool valid = false;
// Deliberately NOT initialized in-class as this eats flash space.
Expand Down Expand Up @@ -141,7 +187,6 @@ void resetRoutingAuthEvaluationCount()
}
}
#endif

/**
* Constructor
*
Expand Down Expand Up @@ -360,9 +405,9 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)

// don't override if a channel was requested and no need to set it when PKI is enforced
if (!p->channel && !p->pki_encrypted && !isBroadcast(p->to)) {
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->to);
if (node) {
p->channel = node->channel;
ChannelIndex chIndex = getEffectiveChannelIndex(p);
if (chIndex) {
p->channel = chIndex;
LOG_DEBUG("localSend to channel %d", p->channel);
}
}
Expand Down Expand Up @@ -473,6 +518,12 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
fixPriority(p); // Before encryption, fix the priority if it's unset
// Position precision is an originator-only privacy policy. Relays keep
// p->from as the original sender, so do not rewrite their POSITION_APP payload.
if (isBlockedEventCoordinatePacket(p)) {
LOG_DEBUG("Suppress coordinate send on event (everyone) channel");
packetPool.release(p);
return meshtastic_Routing_Error_NOT_AUTHORIZED;
}

if (isFromUs(p)) {
if (!applyPositionPrecisionForChannel(*p, p->channel)) {
LOG_ERROR("Drop malformed position packet before send");
Expand Down Expand Up @@ -959,6 +1010,11 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
return DecodeState::DECODE_POLICY_REJECT;
#endif

if (isBlockedEventCoordinatePacket(p)) {
LOG_DEBUG("Decoded coordinate packet on event channel; suppress payload logging");
return DecodeState::DECODE_SUCCESS;
}

if (p->decoded.has_bitfield)
p->decoded.want_response |= p->decoded.bitfield & BITFIELD_WANT_RESPONSE_MASK;

Expand Down Expand Up @@ -1436,6 +1492,16 @@ void Router::dispatchReceived(meshtastic_MeshPacket *p, RxSource src)
cancelSending(p->from, p->id);
skipHandle = true;
}

#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
// Discard coordinate-bearing packets that arrive on the event ("everyone")
// channel: don't process, store in NodeDB, or rebroadcast them.
if (!skipHandle && isBlockedEventCoordinatePacket(p)) {
LOG_DEBUG("Drop coordinate packet on event (everyone) channel");
cancelSending(p->from, p->id);
skipHandle = true;
}
Comment thread
thebentern marked this conversation as resolved.
#endif
} else {
printPacket("packet decoding failed or skipped (no PSK?)", p);
}
Expand Down
9 changes: 9 additions & 0 deletions src/mesh/Router.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
#include "concurrency/OSThread.h"
#include <memory>

inline bool isCoordinatePortnum(meshtastic_PortNum portnum)
{
return portnum == meshtastic_PortNum_POSITION_APP || portnum == meshtastic_PortNum_WAYPOINT_APP ||
portnum == meshtastic_PortNum_MAP_REPORT_APP;
}

bool isBlockedEventCoordinatePacket(const meshtastic_MeshPacket *p);
bool willUsePki(const meshtastic_MeshPacket *p);

/// rx_time/has_rx_time for "now": a real epoch when the clock is trustworthy, else a
/// Time::getMillis() placeholder with valid=false.
struct RxTimeStamp {
Expand Down
12 changes: 12 additions & 0 deletions src/mqtt/MQTT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,12 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me
{
if (mp_encrypted.via_mqtt)
return; // Don't send messages that came from MQTT back into MQTT
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
if (isBlockedEventCoordinatePacket(&mp_decoded)) {
LOG_DEBUG("MQTT onSend - Suppress coordinate packet on event channel");
return;
}
#endif
bool uplinkEnabled = false;
for (int i = 0; i <= 7; i++) {
if (channels.getByIndex(i).settings.uplink_enabled)
Expand Down Expand Up @@ -777,6 +783,12 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me

void MQTT::perhapsReportToMap()
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
if (channels.isEventChannel(channels.getPrimaryIndex())) {
LOG_DEBUG("Suppress MQTT map report on event (everyone) channel");
return;
}
#endif
if (!moduleConfig.mqtt.map_reporting_enabled || !moduleConfig.mqtt.map_report_settings.should_report_location ||
!(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly()))
return;
Expand Down
2 changes: 1 addition & 1 deletion test/native-suite-count
Original file line number Diff line number Diff line change
@@ -1 +1 @@
45
46
Loading
Loading