Block coordinate traffic on configured event channels (#11045)

* Block coordinate traffic on configured event channels

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Suppress event coordinates in reliable relay paths

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Reject blocked phone coordinates before rate limiting

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Prevent event coordinates from reaching MQTT

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Add event coordinate policy preference

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test event coordinate policy in native CI

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Make event policy test tolerate a full NodeDB

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test Router event coordinate enforcement

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test PhoneAPI event coordinate retry handling

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test reliable event coordinate suppression

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Test MQTT event coordinate suppression

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Run event policy behavioral suites in native CI

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* tests: address CodeRabbit review feedback

- test_event_channel_phone_api: complete the setUp/tearDown save-restore
  pair. GlobalState now carries cryptLock and myNodeInfo; setUp() nulls
  cryptLock before constructing MockRouter (Router's ctor asserts it is
  unset), and tearDown() restores both so the suite leaves no global
  mutated. Not reachable today - the globals start null in this binary -
  but the pair was asymmetric.

- Replace the strcpy calls this branch added on Channel.settings.name
  (char[12]) with the bounded form the rest of the test tree already uses,
  strncpy(dst, src, sizeof(dst) - 1). Covers the flagged site in
  test_nexthop_routing plus the six equivalents in
  test_event_channel_phone_api, test_mqtt and test_position_precision,
  which trip the same ast-grep dangerous-buffer-functions-cpp rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ayysasha
2026-08-11 12:34:08 +00:00
committed by GitHub
co-authored by Sisyphus Claude Opus 5 Ben Meadors
parent fb17f6ddbe
commit 546b9d9e40
18 changed files with 1492 additions and 36 deletions
+15
View File
@@ -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
+12 -1
View File
@@ -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
#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;
@@ -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();
@@ -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};
+10 -1
View File
@@ -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);
}
}
@@ -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
+10
View File
@@ -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)
+4
View File
@@ -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;
+6
View File
@@ -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;
+70 -4
View File
@@ -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.
@@ -141,7 +187,6 @@ void resetRoutingAuthEvaluationCount()
}
}
#endif
/**
* Constructor
*
@@ -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);
}
}
@@ -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");
@@ -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;
@@ -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;
}
#endif
} else {
printPacket("packet decoding failed or skipped (no PSK?)", p);
}
+9
View File
@@ -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 {
+12
View File
@@ -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)
@@ -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;