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 files changed
+1492 -36

No files matched your search

+4 -1
View File
@@ -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
+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;
+1 -1
View File
@@ -1 +1 @@
45
46
@@ -0,0 +1,263 @@
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "RadioInterface.h"
#include "Router.h"
#include "StreamAPI.h"
#include "TestUtil.h"
#include "mesh-pb-constants.h"
#include <cstring>
#include <memory>
#include <unity.h>
#include <vector>
namespace
{
constexpr PacketId BLOCKED_PACKET_ID = 0x10203040;
constexpr PacketId FOLLOWUP_PACKET_ID = 0x50607080;
constexpr ChannelIndex EVENT_CHANNEL = 0;
constexpr ChannelIndex PRIVATE_CHANNEL = 1;
constexpr NodeNum REMOTE_NODE = 0x12345678;
class MockRadioInterface : public RadioInterface
{
public:
ErrorCode send(meshtastic_MeshPacket *packet) override
{
packetPool.release(packet);
return ERRNO_OK;
}
uint32_t getPacketTime(uint32_t, bool) override { return 0; }
};
class MockRouter : public Router
{
public:
MockRouter() { addInterface(std::make_unique<MockRadioInterface>()); }
~MockRouter()
{
delete cryptLock;
cryptLock = nullptr;
}
ErrorCode send(meshtastic_MeshPacket *packet) override
{
sentPackets.push_back(*packet);
packetPool.release(packet);
return ERRNO_OK;
}
std::vector<meshtastic_MeshPacket> sentPackets;
};
class MockMeshService : public MeshService
{
public:
~MockMeshService()
{
while (auto *status = getQueueStatusForPhone()) {
releaseQueueStatusToPool(status);
}
}
void sendClientNotification(meshtastic_ClientNotification *notification) override
{
notifications.push_back(*notification);
releaseClientNotificationToPool(notification);
}
void assertQueueStatus(PacketId packetId)
{
auto *status = getQueueStatusForPhone();
TEST_ASSERT_NOT_NULL(status);
TEST_ASSERT_EQUAL_UINT32(packetId, status->mesh_packet_id);
releaseQueueStatusToPool(status);
}
std::vector<meshtastic_ClientNotification> notifications;
};
class TestStreamAPI : public StreamAPI
{
public:
TestStreamAPI() : StreamAPI(nullptr) {}
bool checkIsConnected() override { return true; }
};
struct GlobalState {
MeshService *service;
Router *router;
NodeDB *nodeDB;
// Router's ctor asserts !cryptLock and allocates one; ~MockRouter() deletes it. Save the
// incoming lock so the restored router keeps the one it was built with.
concurrency::Lock *cryptLock;
meshtastic_MyNodeInfo myNodeInfo;
Channels channels;
meshtastic_ChannelFile channelFile;
meshtastic_LocalConfig config;
meshtastic_LocalModuleConfig moduleConfig;
meshtastic_DeviceState deviceState;
};
GlobalState *savedState;
MockMeshService *mockService;
MockRouter *mockRouter;
NodeDB *mockNodeDB;
TestStreamAPI *streamAPI;
void configureChannels()
{
const meshtastic_ChannelFile defaultChannelFile = meshtastic_ChannelFile_init_default;
channelFile = defaultChannelFile;
channelFile.channels_count = 2;
auto &eventChannel = channelFile.channels[EVENT_CHANNEL];
eventChannel.index = EVENT_CHANNEL;
eventChannel.has_settings = true;
eventChannel.role = meshtastic_Channel_Role_PRIMARY;
strncpy(eventChannel.settings.name, "everyone", sizeof(eventChannel.settings.name) - 1);
#ifdef USERPREFS_CHANNEL_0_PSK
static const uint8_t eventPsk[] = USERPREFS_CHANNEL_0_PSK;
eventChannel.settings.psk.size = sizeof(eventPsk);
memcpy(eventChannel.settings.psk.bytes, eventPsk, sizeof(eventPsk));
#endif
auto &privateChannel = channelFile.channels[PRIVATE_CHANNEL];
privateChannel.index = PRIVATE_CHANNEL;
privateChannel.has_settings = true;
privateChannel.role = meshtastic_Channel_Role_SECONDARY;
strncpy(privateChannel.settings.name, "private", sizeof(privateChannel.settings.name) - 1);
privateChannel.settings.psk.size = 32;
memset(privateChannel.settings.psk.bytes, 0xab, privateChannel.settings.psk.size);
channels.onConfigChanged();
}
meshtastic_ToRadio makePositionToRadio(PacketId id, ChannelIndex channel)
{
meshtastic_ToRadio message = meshtastic_ToRadio_init_default;
const meshtastic_MeshPacket defaultPacket = meshtastic_MeshPacket_init_default;
message.which_payload_variant = meshtastic_ToRadio_packet_tag;
message.packet = defaultPacket;
message.packet.to = REMOTE_NODE;
message.packet.id = id;
message.packet.channel = channel;
message.packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
message.packet.decoded.portnum = meshtastic_PortNum_POSITION_APP;
return message;
}
bool sendToRadio(const meshtastic_ToRadio &message)
{
uint8_t encoded[meshtastic_ToRadio_size] = {};
const size_t encodedSize =
pb_encode_to_bytes(encoded, sizeof(encoded), &meshtastic_ToRadio_msg, const_cast<meshtastic_ToRadio *>(&message));
if (encodedSize == 0) {
return false;
}
return streamAPI->handleToRadio(encoded, encodedSize);
}
void assertSentPacket(size_t index, PacketId id, ChannelIndex channel)
{
TEST_ASSERT_GREATER_THAN(index, mockRouter->sentPackets.size());
const auto &packet = mockRouter->sentPackets[index];
TEST_ASSERT_EQUAL_UINT32(id, packet.id);
TEST_ASSERT_EQUAL_UINT8(channel, packet.channel);
TEST_ASSERT_EQUAL(meshtastic_PortNum_POSITION_APP, packet.decoded.portnum);
}
} // namespace
void setUp(void)
{
savedState =
new GlobalState{service, router, nodeDB, cryptLock, myNodeInfo, channels, channelFile, config, moduleConfig, devicestate};
service = mockService = new MockMeshService();
nodeDB = mockNodeDB = new NodeDB();
myNodeInfo.my_node_num = 0x87654321;
configureChannels();
cryptLock = nullptr; // Router's ctor asserts this is unset before allocating its own.
router = mockRouter = new MockRouter();
streamAPI = new TestStreamAPI();
testDelay(1);
}
void tearDown(void)
{
delete streamAPI;
streamAPI = nullptr;
delete mockRouter;
mockRouter = nullptr;
delete mockNodeDB;
mockNodeDB = nullptr;
delete mockService;
mockService = nullptr;
service = savedState->service;
router = savedState->router;
nodeDB = savedState->nodeDB;
cryptLock = savedState->cryptLock; // ~MockRouter() nulled it; hand the saved router its own back.
myNodeInfo = savedState->myNodeInfo;
channels = savedState->channels;
channelFile = savedState->channelFile;
config = savedState->config;
moduleConfig = savedState->moduleConfig;
devicestate = savedState->deviceState;
delete savedState;
savedState = nullptr;
}
static void test_event_position_ingress_does_not_poison_retry_state()
{
const auto eventAttempt = makePositionToRadio(BLOCKED_PACKET_ID, EVENT_CHANNEL);
const auto sameIdPrivateRetry = makePositionToRadio(BLOCKED_PACKET_ID, PRIVATE_CHANNEL);
const auto immediatePrivateFollowup = makePositionToRadio(FOLLOWUP_PACKET_ID, PRIVATE_CHANNEL);
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
TEST_ASSERT_FALSE(sendToRadio(eventAttempt));
TEST_ASSERT_EQUAL(0, mockRouter->sentPackets.size());
mockService->assertQueueStatus(BLOCKED_PACKET_ID);
TEST_ASSERT_EQUAL(1, mockService->notifications.size());
TEST_ASSERT_EQUAL_UINT32(BLOCKED_PACKET_ID, mockService->notifications[0].reply_id);
TEST_ASSERT_TRUE(sendToRadio(sameIdPrivateRetry));
TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size());
assertSentPacket(0, BLOCKED_PACKET_ID, PRIVATE_CHANNEL);
mockService->assertQueueStatus(BLOCKED_PACKET_ID);
TEST_ASSERT_FALSE(sendToRadio(immediatePrivateFollowup));
TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size());
mockService->assertQueueStatus(FOLLOWUP_PACKET_ID);
TEST_ASSERT_EQUAL(1, mockService->notifications.size());
#else
TEST_ASSERT_TRUE(sendToRadio(eventAttempt));
TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size());
assertSentPacket(0, BLOCKED_PACKET_ID, EVENT_CHANNEL);
mockService->assertQueueStatus(BLOCKED_PACKET_ID);
TEST_ASSERT_EQUAL(0, mockService->notifications.size());
TEST_ASSERT_FALSE(sendToRadio(sameIdPrivateRetry));
TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size());
TEST_ASSERT_NULL(mockService->getQueueStatusForPhone());
TEST_ASSERT_FALSE(sendToRadio(immediatePrivateFollowup));
TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size());
mockService->assertQueueStatus(FOLLOWUP_PACKET_ID);
TEST_ASSERT_EQUAL(0, mockService->notifications.size());
#endif
}
extern "C" {
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_event_position_ingress_does_not_poison_retry_state);
exit(UNITY_END());
}
void loop() {}
}
@@ -0,0 +1,405 @@
#include "MeshTypes.h"
#include "TestUtil.h"
#include <unity.h>
#include "airtime.h"
#include "mesh/Channels.h"
#include "mesh/CryptoEngine.h"
#include "mesh/MeshModule.h"
#include "mesh/MeshRadio.h"
#include "mesh/MeshService.h"
#include "mesh/NodeDB.h"
#include "mesh/Router.h"
#if ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#endif
#include <array>
#include <cstdio>
#include <cstring>
#include <memory>
#include <pb_encode.h>
#include <vector>
#if ARCH_PORTDUINO
#define EVENT_ROUTER_TEST_ENTRY extern "C"
#else
#define EVENT_ROUTER_TEST_ENTRY
#endif
namespace
{
constexpr NodeNum kLocalNode = 0x11111111;
constexpr NodeNum kRemoteNode = 0x22222222;
constexpr NodeNum kPkiPeer = 0x33333333;
constexpr ChannelIndex kEventChannel = 0;
constexpr ChannelIndex kPrivateChannel = 1;
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL
constexpr bool kBlockEventCoordinates = true;
constexpr ErrorCode kExpectedEventTxResult = meshtastic_Routing_Error_NOT_AUTHORIZED;
constexpr size_t kExpectedEventDeliveryCount = 0;
#else
constexpr bool kBlockEventCoordinates = false;
constexpr ErrorCode kExpectedEventTxResult = ERRNO_OK;
constexpr size_t kExpectedEventDeliveryCount = 3;
#endif
constexpr std::array<meshtastic_PortNum, 3> kCoordinatePorts = {
meshtastic_PortNum_POSITION_APP,
meshtastic_PortNum_WAYPOINT_APP,
meshtastic_PortNum_MAP_REPORT_APP,
};
class TestNodeDB : public NodeDB
{
public:
void clearTestNodes()
{
testNodes.clear();
meshNodes = &testNodes;
numMeshNodes = 0;
}
void addNode(NodeNum num, ChannelIndex channel, const uint8_t *publicKey = nullptr)
{
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
node.num = num;
node.channel = channel;
if (publicKey) {
node.public_key.size = 32;
memcpy(node.public_key.bytes, publicKey, 32);
}
testNodes.push_back(node);
meshNodes = &testNodes;
numMeshNodes = testNodes.size();
}
private:
std::vector<meshtastic_NodeInfoLite> testNodes;
};
class CaptureRadio : public RadioInterface
{
public:
ErrorCode send(meshtastic_MeshPacket *packet) override
{
packets.push_back(*packet);
packetPool.release(packet);
return ERRNO_OK;
}
uint32_t getPacketTime(uint32_t, bool = false) override { return 0; }
std::vector<meshtastic_MeshPacket> packets;
};
class CaptureModule : public MeshModule
{
public:
CaptureModule() : MeshModule("event-router-capture") { encryptedOk = true; }
bool wantPacket(const meshtastic_MeshPacket *) override { return true; }
ProcessMessage handleReceived(const meshtastic_MeshPacket &packet) override
{
packets.push_back(packet);
return ProcessMessage::CONTINUE;
}
std::vector<meshtastic_MeshPacket> packets;
};
struct SavedGlobals {
meshtastic_LocalConfig config;
meshtastic_LocalModuleConfig moduleConfig;
meshtastic_ChannelFile channelFile;
meshtastic_User owner;
meshtastic_MyNodeInfo myNodeInfo;
NodeDB *nodeDB;
Router *router;
MeshService *service;
AirTime *airTime;
concurrency::Lock *cryptLock;
#if ARCH_PORTDUINO
bool forceSimRadio;
#endif
};
SavedGlobals saved;
TestNodeDB *testNodeDB = nullptr;
Router *testRouter = nullptr;
CaptureRadio *captureRadio = nullptr;
CaptureModule *captureModule = nullptr;
AirTime *testAirTime = nullptr;
static void installChannels()
{
memset(&channelFile, 0, sizeof(channelFile));
channelFile.channels_count = 2;
meshtastic_Channel &event = channelFile.channels[kEventChannel];
memset(&event, 0, sizeof(event));
event.index = kEventChannel;
event.role = meshtastic_Channel_Role_PRIMARY;
event.has_settings = true;
strncpy(event.settings.name, "everyone", sizeof(event.settings.name) - 1);
#ifdef USERPREFS_CHANNEL_0_PSK
static const uint8_t eventKey[] = USERPREFS_CHANNEL_0_PSK;
static_assert(sizeof(eventKey) == 16 || sizeof(eventKey) == 32);
event.settings.psk.size = sizeof(eventKey);
memcpy(event.settings.psk.bytes, eventKey, sizeof(eventKey));
#else
static const uint8_t eventKey[16] = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f};
event.settings.psk.size = sizeof(eventKey);
memcpy(event.settings.psk.bytes, eventKey, sizeof(eventKey));
#endif
meshtastic_Channel &privateChannel = channelFile.channels[kPrivateChannel];
memset(&privateChannel, 0, sizeof(privateChannel));
privateChannel.index = kPrivateChannel;
privateChannel.role = meshtastic_Channel_Role_SECONDARY;
privateChannel.has_settings = true;
strncpy(privateChannel.settings.name, "private", sizeof(privateChannel.settings.name) - 1);
privateChannel.settings.psk.size = 32;
for (size_t i = 0; i < privateChannel.settings.psk.size; ++i)
privateChannel.settings.psk.bytes[i] = static_cast<uint8_t>(0x80 + i);
channels.onConfigChanged();
}
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum port, NodeNum from, NodeNum to, ChannelIndex channel)
{
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
packet.from = from;
packet.to = to;
packet.id = 0x40000000u + static_cast<uint32_t>(port);
packet.channel = channel;
packet.hop_start = 3;
packet.hop_limit = 3;
packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
packet.decoded.portnum = port;
if (port == meshtastic_PortNum_POSITION_APP) {
meshtastic_Position position = meshtastic_Position_init_zero;
position.has_latitude_i = true;
position.latitude_i = 374221234;
position.has_longitude_i = true;
position.longitude_i = -1220845678;
packet.decoded.payload.size = pb_encode_to_bytes(packet.decoded.payload.bytes, sizeof(packet.decoded.payload.bytes),
&meshtastic_Position_msg, &position);
} else {
packet.decoded.payload.size = 1;
packet.decoded.payload.bytes[0] = 0x5a;
}
return packet;
}
static ErrorCode sendCoordinate(meshtastic_PortNum port, ChannelIndex channel, NodeNum to = NODENUM_BROADCAST)
{
meshtastic_MeshPacket *packet = testRouter->allocForSending();
TEST_ASSERT_NOT_NULL(packet);
const meshtastic_MeshPacket contents = makeDecodedPacket(port, kLocalNode, to, channel);
packet->to = contents.to;
packet->channel = contents.channel;
packet->decoded = contents.decoded;
return testRouter->send(packet);
}
static void receivePacket(const meshtastic_MeshPacket &contents)
{
meshtastic_MeshPacket *packet = packetPool.allocCopy(contents);
TEST_ASSERT_NOT_NULL(packet);
testRouter->enqueueReceivedMessage(packet);
testRouter->runOnce();
}
static void test_tx_event_channel_enforces_compile_time_policy_for_all_coordinate_ports()
{
TEST_ASSERT_EQUAL(kBlockEventCoordinates, channels.isEventChannel(kEventChannel));
for (meshtastic_PortNum port : kCoordinatePorts) {
const size_t before = captureRadio->packets.size();
TEST_ASSERT_EQUAL_INT(kExpectedEventTxResult, sendCoordinate(port, kEventChannel));
TEST_ASSERT_EQUAL_UINT32(before + (kBlockEventCoordinates ? 0 : 1), captureRadio->packets.size());
}
}
static void test_rx_event_channel_enforces_compile_time_policy_for_all_coordinate_ports()
{
for (meshtastic_PortNum port : kCoordinatePorts)
receivePacket(makeDecodedPacket(port, kRemoteNode, NODENUM_BROADCAST, kEventChannel));
TEST_ASSERT_EQUAL_UINT32(kExpectedEventDeliveryCount, captureModule->packets.size());
}
static void test_private_channel_preserves_legacy_tx_and_rx_for_all_coordinate_ports()
{
TEST_ASSERT_FALSE(channels.isEventChannel(kPrivateChannel));
for (meshtastic_PortNum port : kCoordinatePorts) {
TEST_ASSERT_EQUAL_INT(ERRNO_OK, sendCoordinate(port, kPrivateChannel));
receivePacket(makeDecodedPacket(port, kRemoteNode, NODENUM_BROADCAST, kPrivateChannel));
}
TEST_ASSERT_EQUAL_UINT32(kCoordinatePorts.size(), captureRadio->packets.size());
TEST_ASSERT_EQUAL_UINT32(kCoordinatePorts.size(), captureModule->packets.size());
}
#if !(MESHTASTIC_EXCLUDE_PKI)
static void test_tx_event_coordinate_that_uses_pki_reaches_radio()
{
uint8_t peerPublic[32], peerPrivate[32];
uint8_t localPublic[32], localPrivate[32];
crypto->generateKeyPair(peerPublic, peerPrivate);
crypto->generateKeyPair(localPublic, localPrivate);
config.has_security = true;
config.security.private_key.size = 32;
config.security.public_key.size = 32;
memcpy(config.security.private_key.bytes, localPrivate, 32);
memcpy(config.security.public_key.bytes, localPublic, 32);
crypto->setDHPrivateKey(localPrivate);
testNodeDB->addNode(kPkiPeer, kEventChannel, peerPublic);
TEST_ASSERT_EQUAL_INT(ERRNO_OK, sendCoordinate(meshtastic_PortNum_WAYPOINT_APP, kEventChannel, kPkiPeer));
TEST_ASSERT_EQUAL_UINT32(1, captureRadio->packets.size());
TEST_ASSERT_TRUE(captureRadio->packets.front().pki_encrypted);
TEST_ASSERT_EQUAL(meshtastic_MeshPacket_encrypted_tag, captureRadio->packets.front().which_payload_variant);
}
#endif
static void test_opaque_tx_is_not_misclassified_as_coordinates()
{
meshtastic_MeshPacket *outgoing = testRouter->allocForSending();
TEST_ASSERT_NOT_NULL(outgoing);
outgoing->channel = channels.getHash(kEventChannel);
outgoing->which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
outgoing->encrypted.size = 1;
outgoing->encrypted.bytes[0] = 0xa5;
TEST_ASSERT_EQUAL_INT(ERRNO_OK, testRouter->send(outgoing));
TEST_ASSERT_EQUAL_UINT32(1, captureRadio->packets.size());
}
static void test_capture_endpoints_release_packet_pool_ownership()
{
constexpr size_t iterations = 64;
for (size_t i = 0; i < iterations; ++i) {
meshtastic_MeshPacket *outgoing = testRouter->allocForSending();
TEST_ASSERT_NOT_NULL(outgoing);
outgoing->channel = kPrivateChannel;
outgoing->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
outgoing->decoded.payload.size = 1;
outgoing->decoded.payload.bytes[0] = static_cast<uint8_t>(i);
TEST_ASSERT_EQUAL_INT(ERRNO_OK, testRouter->send(outgoing));
meshtastic_MeshPacket incoming =
makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, NODENUM_BROADCAST, kPrivateChannel);
incoming.id += i;
receivePacket(incoming);
}
TEST_ASSERT_EQUAL_UINT32(iterations, captureRadio->packets.size());
TEST_ASSERT_EQUAL_UINT32(iterations, captureModule->packets.size());
}
} // namespace
void setUp(void)
{
saved.config = config;
saved.moduleConfig = moduleConfig;
saved.channelFile = channelFile;
saved.owner = owner;
saved.myNodeInfo = myNodeInfo;
saved.nodeDB = nodeDB;
saved.router = router;
saved.service = service;
saved.airTime = airTime;
saved.cryptLock = cryptLock;
#if ARCH_PORTDUINO
saved.forceSimRadio = portduino_config.force_simradio;
#endif
testNodeDB = new TestNodeDB();
testNodeDB->clearTestNodes();
nodeDB = testNodeDB;
memset(&config, 0, sizeof(config));
config.lora.override_duty_cycle = true;
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
memset(&moduleConfig, 0, sizeof(moduleConfig));
memset(&owner, 0, sizeof(owner));
memset(&myNodeInfo, 0, sizeof(myNodeInfo));
myNodeInfo.my_node_num = kLocalNode;
service = nullptr;
#if ARCH_PORTDUINO
portduino_config.force_simradio = false;
#endif
installChannels();
testAirTime = new AirTime();
airTime = testAirTime;
cryptLock = nullptr;
testRouter = new Router();
router = testRouter;
std::unique_ptr<CaptureRadio> radio(new CaptureRadio());
captureRadio = radio.get();
testRouter->addInterface(std::move(radio));
captureModule = new CaptureModule();
}
void tearDown(void)
{
delete captureModule;
captureModule = nullptr;
router = nullptr;
delete testRouter;
testRouter = nullptr;
captureRadio = nullptr;
delete cryptLock;
cryptLock = saved.cryptLock;
delete testNodeDB;
testNodeDB = nullptr;
delete testAirTime;
testAirTime = nullptr;
config = saved.config;
moduleConfig = saved.moduleConfig;
channelFile = saved.channelFile;
owner = saved.owner;
myNodeInfo = saved.myNodeInfo;
channels.onConfigChanged();
nodeDB = saved.nodeDB;
router = saved.router;
service = saved.service;
airTime = saved.airTime;
#if ARCH_PORTDUINO
portduino_config.force_simradio = saved.forceSimRadio;
#endif
}
EVENT_ROUTER_TEST_ENTRY void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
printf("\n=== Router event-channel coordinate enforcement ===\n");
RUN_TEST(test_tx_event_channel_enforces_compile_time_policy_for_all_coordinate_ports);
RUN_TEST(test_rx_event_channel_enforces_compile_time_policy_for_all_coordinate_ports);
RUN_TEST(test_private_channel_preserves_legacy_tx_and_rx_for_all_coordinate_ports);
#if !(MESHTASTIC_EXCLUDE_PKI)
RUN_TEST(test_tx_event_coordinate_that_uses_pki_reaches_radio);
#endif
RUN_TEST(test_opaque_tx_is_not_misclassified_as_coordinates);
RUN_TEST(test_capture_endpoints_release_packet_pool_ownership);
exit(UNITY_END());
}
EVENT_ROUTER_TEST_ENTRY void loop() {}
+125 -1
View File
@@ -338,16 +338,64 @@ const meshtastic_MeshPacket encrypted = {
.encrypted = {.size = 0},
.id = 3,
};
void configureCoordinatePolicyChannels(bool eventChannelIsPrimary = true)
{
memset(&channelFile, 0, sizeof(channelFile));
channelFile.channels_count = 2;
auto &eventChannel = channelFile.channels[0];
eventChannel.index = 0;
eventChannel.has_settings = true;
strncpy(eventChannel.settings.name, "everyone", sizeof(eventChannel.settings.name) - 1);
eventChannel.settings.uplink_enabled = true;
eventChannel.settings.downlink_enabled = true;
eventChannel.role = eventChannelIsPrimary ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY;
#ifdef USERPREFS_CHANNEL_0_PSK
static const uint8_t configuredEventPsk[] = USERPREFS_CHANNEL_0_PSK;
eventChannel.settings.psk.size = sizeof(configuredEventPsk);
memcpy(eventChannel.settings.psk.bytes, configuredEventPsk, sizeof(configuredEventPsk));
#endif
auto &privateChannel = channelFile.channels[1];
privateChannel.index = 1;
privateChannel.has_settings = true;
strncpy(privateChannel.settings.name, "private", sizeof(privateChannel.settings.name) - 1);
privateChannel.settings.psk.size = 32;
memset(privateChannel.settings.psk.bytes, 0xab, privateChannel.settings.psk.size);
privateChannel.settings.uplink_enabled = true;
privateChannel.settings.downlink_enabled = true;
privateChannel.role = eventChannelIsPrimary ? meshtastic_Channel_Role_SECONDARY : meshtastic_Channel_Role_PRIMARY;
channels.onConfigChanged();
}
meshtastic_MeshPacket makePositionPacket(ChannelIndex channel)
{
meshtastic_MeshPacket packet = decoded;
packet.to = NODENUM_BROADCAST;
packet.channel = channel;
packet.decoded.portnum = meshtastic_PortNum_POSITION_APP;
return packet;
}
void clearPublicationState()
{
TEST_ASSERT_EQUAL(0, unitTest->queueSize());
pubsub->published_.clear();
mockMeshService->messages_.clear();
}
} // namespace
// Initialize mocks and configuration before running each test.
void setUp(void)
{
config = meshtastic_LocalConfig_init_zero;
memset(&config, 0, sizeof(config));
moduleConfig.mqtt =
meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true};
moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{
.publish_interval_secs = 0, .position_precision = 14, .should_report_location = true};
memset(&channelFile, 0, sizeof(channelFile));
channelFile.channels[0] = meshtastic_Channel{
.index = 0,
.has_settings = true,
@@ -355,6 +403,7 @@ void setUp(void)
.role = meshtastic_Channel_Role_PRIMARY,
};
channelFile.channels_count = 1;
channels.onConfigChanged();
owner = meshtastic_User{.id = "!12345678"};
myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 0x12345678}; // Match the expected gateway ID in topic
localPosition =
@@ -412,6 +461,50 @@ void test_sendDirectlyConnectedEncrypted(void)
TEST_ASSERT_EQUAL(encrypted.id, env.packet->id);
}
void test_eventPositionPublicationFollowsCompileTimePolicy(void)
{
configureCoordinatePolicyChannels();
clearPublicationState();
const meshtastic_MeshPacket position = makePositionPacket(0);
mqtt->onSend(encrypted, position, 0);
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
TEST_ASSERT_TRUE(pubsub->published_.empty());
TEST_ASSERT_EQUAL(0, unitTest->queueSize());
#else
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
#endif
}
void test_privatePositionStillPublishesWithEventPolicy(void)
{
configureCoordinatePolicyChannels();
clearPublicationState();
const meshtastic_MeshPacket position = makePositionPacket(1);
mqtt->onSend(encrypted, position, 1);
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
TEST_ASSERT_EQUAL_STRING("msh/2/e/private/!12345678", pubsub->published_.front().first.c_str());
}
void test_explicitPkiPositionStillPublishesWithEventPolicy(void)
{
configureCoordinatePolicyChannels();
clearPublicationState();
meshtastic_MeshPacket position = makePositionPacket(0);
meshtastic_MeshPacket encryptedPki = encrypted;
position.to = 2;
position.pki_encrypted = true;
encryptedPki.pki_encrypted = true;
mqtt->onSend(encryptedPki, position, 0);
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
TEST_ASSERT_EQUAL_STRING("msh/2/e/PKI/!12345678", pubsub->published_.front().first.c_str());
}
// Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false.
void test_proxyToMeshServiceDecoded(void)
{
@@ -918,6 +1011,32 @@ void test_reportToMapDefaultImprecise(void)
TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str());
}
void test_eventPrimaryMapReportFollowsCompileTimePolicy(void)
{
configureCoordinatePolicyChannels();
clearPublicationState();
unitTest->reportToMap();
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
TEST_ASSERT_TRUE(pubsub->published_.empty());
TEST_ASSERT_EQUAL(0, unitTest->queueSize());
#else
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
#endif
}
void test_privatePrimaryMapReportStillPublishesWithEventPolicy(void)
{
configureCoordinatePolicyChannels(false);
clearPublicationState();
unitTest->reportToMap();
TEST_ASSERT_EQUAL(1, pubsub->published_.size());
TEST_ASSERT_EQUAL_STRING("msh/2/map/", pubsub->published_.front().first.c_str());
}
// Location is sent over the phone proxy.
void test_reportToMapImpreciseProxied(void)
{
@@ -1135,6 +1254,9 @@ void setup()
UNITY_BEGIN();
RUN_TEST(test_sendDirectlyConnectedDecoded);
RUN_TEST(test_sendDirectlyConnectedEncrypted);
RUN_TEST(test_eventPositionPublicationFollowsCompileTimePolicy);
RUN_TEST(test_privatePositionStillPublishesWithEventPolicy);
RUN_TEST(test_explicitPkiPositionStillPublishesWithEventPolicy);
RUN_TEST(test_proxyToMeshServiceDecoded);
RUN_TEST(test_proxyToMeshServiceEncrypted);
RUN_TEST(test_dontMqttMeOnPublicServer);
@@ -1171,6 +1293,8 @@ void setup()
RUN_TEST(test_publishTextMessageDirect);
RUN_TEST(test_publishTextMessageWithProxy);
RUN_TEST(test_reportToMapDefaultImprecise);
RUN_TEST(test_eventPrimaryMapReportFollowsCompileTimePolicy);
RUN_TEST(test_privatePrimaryMapReportStillPublishesWithEventPolicy);
RUN_TEST(test_reportToMapImpreciseProxied);
RUN_TEST(test_usingDefaultServer);
RUN_TEST(test_usingDefaultServerWithPort);
+348 -27
View File
@@ -11,15 +11,21 @@
#include "TestUtil.h"
#include <unity.h>
#include "airtime.h"
#include "configuration.h"
#include "gps/RTC.h"
#include "mesh/Default.h"
#include "mesh/NextHopRouter.h"
#include "mesh/NodeDB.h"
#include "mesh/RadioInterface.h"
#include "mesh/ReliableRouter.h"
#include "modules/RoutingModule.h"
#include <cstdio>
#include <cstring>
#include <list>
#include <memory>
#include <tuple>
#include <vector>
#define MSG_BUF_LEN 200
#define TEST_MSG_FMT(fmt, ...) \
@@ -30,6 +36,13 @@
} while (0)
static constexpr NodeNum kLocalNode = 0x11111111; // last byte 0x11
static constexpr NodeNum kRemoteNode = 0x22222222;
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
static constexpr bool kEventPolicyEnabled = true;
#else
static constexpr bool kEventPolicyEnabled = false;
#endif
// ---------------------------------------------------------------------------
// MockNodeDB - inject nodes with controlled last byte, hop distance, age, role, favorite flag.
@@ -93,6 +106,15 @@ class NextHopRouterTestShim : public NextHopRouter
using NextHopRouter::relayOpaquePacket;
using Router::shouldDecrementHopLimit; // protected in Router
bool filterViaFlooding(const meshtastic_MeshPacket *p) { return FloodingRouter::shouldFilterReceived(p); }
bool filterViaNextHop(const meshtastic_MeshPacket *p) { return NextHopRouter::shouldFilterReceived(p); }
void clearPendingForTest()
{
while (!pending.empty())
stopRetransmission(pending.begin()->first);
}
void resetRouteHealthForTest()
{
for (auto &h : routeHealth)
@@ -100,10 +122,8 @@ class NextHopRouterTestShim : public NextHopRouter
}
};
// ---------------------------------------------------------------------------
// MockRadioInterface - mirrors RadioLibInterface::send()'s NODENUM_BROADCAST_NO_LORA branch, which
// returns ERRNO_SHOULD_RELEASE without releasing.
// ---------------------------------------------------------------------------
// Mirrors RadioLibInterface::send()'s NODENUM_BROADCAST_NO_LORA branch, which
// returns ERRNO_SHOULD_RELEASE without releasing the packet.
class MockRadioInterface : public RadioInterface
{
public:
@@ -132,8 +152,137 @@ class MockRadioInterface : public RadioInterface
uint8_t lastHopStart = 0;
};
class CaptureRadioInterface : public RadioInterface
{
public:
ErrorCode send(meshtastic_MeshPacket *p) override
{
sentPackets.push_back(*p);
packetPool.release(p);
return ERRNO_OK;
}
bool cancelSending(NodeNum from, PacketId id) override
{
(void)from;
(void)id;
cancelCount++;
return false;
}
bool findInTxQueue(NodeNum from, PacketId id) override
{
(void)from;
(void)id;
return false;
}
uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override
{
(void)totalPacketLen;
(void)received;
return 0;
}
void reset()
{
sentPackets.clear();
cancelCount = 0;
}
std::vector<meshtastic_MeshPacket> sentPackets;
uint32_t cancelCount = 0;
};
class ReliableRouterTestShim : public ReliableRouter
{
public:
ReliableRouterTestShim() : ReliableRouter() {}
size_t pendingCount() const { return pending.size(); }
void seedRetry(const meshtastic_MeshPacket &p, uint8_t attempts)
{
auto *copy = packetPool.allocCopy(p);
TEST_ASSERT_NOT_NULL(copy);
startRetransmission(copy, attempts);
}
void makeRetryDue(NodeNum from, PacketId id)
{
PendingPacket *record = findPendingPacket(from, id);
TEST_ASSERT_NOT_NULL(record);
record->nextTxMsec = 0;
}
int32_t runDueRetries() { return doRetransmissions(); }
void sniffForTest(const meshtastic_MeshPacket *p, const meshtastic_Routing *routing)
{
ReliableRouter::sniffReceived(p, routing);
}
void clearPendingForTest()
{
while (!pending.empty())
stopRetransmission(pending.begin()->first);
}
};
class MockRoutingModule : public RoutingModule
{
public:
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
bool ackWantsAck = false) override
{
ackNaks.emplace_back(err, to, idFrom, chIndex, hopLimit, ackWantsAck);
}
std::list<std::tuple<meshtastic_Routing_Error, NodeNum, PacketId, ChannelIndex, uint8_t, bool>> ackNaks;
};
class ScopedAirTimeFixture
{
public:
ScopedAirTimeFixture() : previous(airTime) { airTime = &instance; }
~ScopedAirTimeFixture() { airTime = previous; }
private:
AirTime instance;
AirTime *previous;
};
static meshtastic_MeshPacket makeRebroadcastCandidate(NodeNum to)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = kRemoteNode;
p.to = to;
p.id = 0x0BADF00D;
p.hop_start = 3;
p.hop_limit = 3;
p.next_hop = NO_NEXT_HOP_PREFERENCE;
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
p.encrypted.size = 8;
return p;
}
static MockNodeDB *mockNodeDB = nullptr;
static NextHopRouterTestShim *shim = nullptr;
static ReliableRouterTestShim *reliableShim = nullptr;
static CaptureRadioInterface *nextHopRadio = nullptr;
static CaptureRadioInterface *reliableRadio = nullptr;
static MockRoutingModule *mockRoutingModule = nullptr;
static std::unique_ptr<ScopedAirTimeFixture> airTimeFixture;
static PacketId nextBehaviorPacketId = 0x70000000;
static MockRadioInterface *installMockIface()
{
MockRadioInterface *mock = new MockRadioInterface();
// addInterface replaces and destroys the suite's original capture interface.
// Clear its borrowed pointer before the next Unity setUp() runs.
nextHopRadio = nullptr;
shim->addInterface(std::unique_ptr<RadioInterface>(mock));
return mock;
}
static constexpr uint32_t TTL = NextHopRouter::ROUTE_TTL_MSEC;
static constexpr uint8_t THRESH = NextHopRouter::ROUTE_FAILURE_THRESHOLD;
@@ -150,12 +299,84 @@ static meshtastic_MeshPacket makeRelayedPacket(uint8_t relay, uint8_t hopsAway)
return p;
}
static meshtastic_Channel makeBehaviorChannel(meshtastic_Channel_Role role, const char *name)
{
meshtastic_Channel channel = meshtastic_Channel_init_default;
channel.has_settings = true;
channel.role = role;
channel.settings.has_module_settings = true;
channel.settings.module_settings.position_precision = 16;
strncpy(channel.settings.name, name, sizeof(channel.settings.name) - 1);
return channel;
}
static void configureBehaviorChannels()
{
memset(&channelFile, 0, sizeof(channelFile));
channelFile.channels_count = 2;
meshtastic_Channel eventChannel = makeBehaviorChannel(meshtastic_Channel_Role_PRIMARY, "everyone");
eventChannel.index = 0;
#ifdef USERPREFS_CHANNEL_0_PSK
static const uint8_t eventPsk[] = USERPREFS_CHANNEL_0_PSK;
eventChannel.settings.psk.size = sizeof(eventPsk);
memcpy(eventChannel.settings.psk.bytes, eventPsk, sizeof(eventPsk));
#endif
meshtastic_Channel privateChannel = makeBehaviorChannel(meshtastic_Channel_Role_SECONDARY, "private");
privateChannel.index = 1;
privateChannel.settings.psk.size = 32;
memset(privateChannel.settings.psk.bytes, 0xAB, privateChannel.settings.psk.size);
channelFile.channels[0] = eventChannel;
channelFile.channels[1] = privateChannel;
channels.onConfigChanged();
}
static meshtastic_MeshPacket makeBehaviorPacket(meshtastic_PortNum portnum, NodeNum from, NodeNum to, uint8_t channel,
bool wantAck = false)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = from;
p.to = to;
p.id = nextBehaviorPacketId++;
p.channel = channel;
p.hop_start = 3;
p.hop_limit = 3;
p.relay_node = 0x22;
p.next_hop = NO_NEXT_HOP_PREFERENCE;
p.want_ack = wantAck;
p.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
p.decoded.portnum = portnum;
return p;
}
static meshtastic_MeshPacket *allocBehaviorPacket(meshtastic_PortNum portnum, NodeNum to, uint8_t channel, bool wantAck)
{
auto packet = makeBehaviorPacket(portnum, kLocalNode, to, channel, wantAck);
auto *allocated = packetPool.allocCopy(packet);
TEST_ASSERT_NOT_NULL(allocated);
return allocated;
}
void setUp(void)
{
myNodeInfo.my_node_num = kLocalNode;
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL;
config.lora.override_duty_cycle = true;
config.security.private_key.size = 0;
owner.is_licensed = false;
mockNodeDB->clearTestNodes();
shim->resetRouteHealthForTest();
shim->clearPendingForTest();
reliableShim->clearPendingForTest();
if (nextHopRadio)
nextHopRadio->reset();
reliableRadio->reset();
mockRoutingModule->ackNaks.clear();
configureBehaviorChannels();
}
void tearDown(void) {}
@@ -446,33 +667,112 @@ void test_hoplimit_decrement_when_resolved_not_favorite(void)
}
// ===========================================================================
// Rebroadcast of NODENUM_BROADCAST_NO_LORA
// Group 5 - event-coordinate routing behavior
// ===========================================================================
static MockRadioInterface *installMockIface()
void test_eventPolicy_reliableOriginSendSuppressesTxAndPending(void)
{
MockRadioInterface *m = new MockRadioInterface();
shim->addInterface(std::unique_ptr<RadioInterface>(m));
return m;
ErrorCode result = reliableShim->send(
allocBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, NODENUM_BROADCAST, /*event channel=*/0, /*wantAck=*/true));
if (kEventPolicyEnabled) {
TEST_ASSERT_EQUAL_INT(meshtastic_Routing_Error_NOT_AUTHORIZED, result);
TEST_ASSERT_EQUAL_UINT32(0, reliableRadio->sentPackets.size());
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
} else {
TEST_ASSERT_EQUAL_INT(ERRNO_OK, result);
TEST_ASSERT_EQUAL_UINT32(1, reliableRadio->sentPackets.size());
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
}
}
// Eligible for rebroadcast: not from/to us, hops left, nonzero id, no next-hop preference.
// Encrypted variant so Router::send() skips the encode path.
static meshtastic_MeshPacket makeRebroadcastCandidate(NodeNum to)
void test_eventPolicy_reliablePrivateCoordinateStillSends(void)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = 0x22222222; // not us
p.to = to;
p.id = 0x0BADF00D;
p.hop_start = 3;
p.hop_limit = 3;
p.next_hop = NO_NEXT_HOP_PREFERENCE;
p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
p.encrypted.size = 8;
return p;
ErrorCode result = reliableShim->send(
allocBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, NODENUM_BROADCAST, /*private channel=*/1, /*wantAck=*/true));
TEST_ASSERT_EQUAL_INT(ERRNO_OK, result);
TEST_ASSERT_EQUAL_UINT32(1, reliableRadio->sentPackets.size());
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
}
void test_eventPolicy_floodingDuplicateSuppressesCoordinateButRelaysText(void)
{
mockNodeDB->addNode(kRemoteNode, 0, true, 0);
auto coordinate = makeBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, kRemoteNode, NODENUM_BROADCAST, 0);
TEST_ASSERT_FALSE(shim->filterViaFlooding(&coordinate));
TEST_ASSERT_TRUE(shim->filterViaFlooding(&coordinate));
TEST_ASSERT_EQUAL_UINT32(kEventPolicyEnabled ? 0 : 1, nextHopRadio->sentPackets.size());
nextHopRadio->reset();
auto text = makeBehaviorPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, NODENUM_BROADCAST, 0);
TEST_ASSERT_FALSE(shim->filterViaFlooding(&text));
TEST_ASSERT_TRUE(shim->filterViaFlooding(&text));
TEST_ASSERT_EQUAL_UINT32(1, nextHopRadio->sentPackets.size());
}
void test_eventPolicy_nextHopDuplicateSuppressesEventButRelaysPrivateCoordinate(void)
{
mockNodeDB->addNode(kRemoteNode, 0, true, 0);
auto eventCoordinate = makeBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, kRemoteNode, NODENUM_BROADCAST, 0);
TEST_ASSERT_FALSE(shim->filterViaNextHop(&eventCoordinate));
TEST_ASSERT_TRUE(shim->filterViaNextHop(&eventCoordinate));
TEST_ASSERT_EQUAL_UINT32(kEventPolicyEnabled ? 0 : 1, nextHopRadio->sentPackets.size());
nextHopRadio->reset();
auto privateCoordinate = makeBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, kRemoteNode, NODENUM_BROADCAST, 1);
TEST_ASSERT_FALSE(shim->filterViaNextHop(&privateCoordinate));
TEST_ASSERT_TRUE(shim->filterViaNextHop(&privateCoordinate));
TEST_ASSERT_EQUAL_UINT32(1, nextHopRadio->sentPackets.size());
}
void test_eventPolicy_repeatedLocalPacketSuppressesCoordinateAckButKeepsTextAck(void)
{
mockNodeDB->addNode(kRemoteNode, 0, true, 0);
auto coordinate = makeBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, kRemoteNode, kLocalNode, 0, /*wantAck=*/true);
TEST_ASSERT_FALSE(shim->filterViaNextHop(&coordinate));
TEST_ASSERT_TRUE(shim->filterViaNextHop(&coordinate));
TEST_ASSERT_EQUAL_UINT32(kEventPolicyEnabled ? 0 : 1, mockRoutingModule->ackNaks.size());
mockRoutingModule->ackNaks.clear();
auto text = makeBehaviorPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kRemoteNode, kLocalNode, 0, /*wantAck=*/true);
TEST_ASSERT_FALSE(shim->filterViaNextHop(&text));
TEST_ASSERT_TRUE(shim->filterViaNextHop(&text));
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
const auto &ack = mockRoutingModule->ackNaks.front();
TEST_ASSERT_EQUAL_INT(meshtastic_Routing_Error_NONE, std::get<0>(ack));
TEST_ASSERT_EQUAL_HEX32(kRemoteNode, std::get<1>(ack));
TEST_ASSERT_EQUAL_HEX32(text.id, std::get<2>(ack));
}
void test_eventPolicy_seededRetrySuppressesTxUntilGateOff(void)
{
auto coordinate = makeBehaviorPacket(meshtastic_PortNum_WAYPOINT_APP, kLocalNode, NODENUM_BROADCAST, 0, /*wantAck=*/true);
reliableShim->seedRetry(coordinate, /*attempts=*/2);
reliableShim->makeRetryDue(kLocalNode, coordinate.id);
reliableShim->runDueRetries();
TEST_ASSERT_EQUAL_UINT32(kEventPolicyEnabled ? 0 : 1, reliableRadio->sentPackets.size());
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
}
void test_reliableAckStopsNormalPendingTransmission(void)
{
auto original = makeBehaviorPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, kLocalNode, kRemoteNode, 1, /*wantAck=*/true);
reliableShim->seedRetry(original, NextHopRouter::NUM_RELIABLE_RETX);
TEST_ASSERT_EQUAL_UINT32(1, reliableShim->pendingCount());
auto ack = makeBehaviorPacket(meshtastic_PortNum_ROUTING_APP, kRemoteNode, kLocalNode, 1);
ack.decoded.request_id = original.id;
meshtastic_Routing routing = meshtastic_Routing_init_zero;
routing.error_reason = meshtastic_Routing_Error_NONE;
reliableShim->sniffForTest(&ack, &routing);
TEST_ASSERT_EQUAL_UINT32(0, reliableShim->pendingCount());
}
// Control: proves the NO_LORA case below turns on the `to` field alone.
void test_rebroadcast_normal_broadcast_is_relayed(void)
{
MockRadioInterface *mockIface = installMockIface();
@@ -491,13 +791,10 @@ void test_rebroadcast_no_lora_broadcast_is_not_relayed(void)
TEST_ASSERT_EQUAL_MESSAGE(0, mockIface->sendCount, "no packet should be handed to the radio at all");
}
// Declining mock bypasses the guard so send() is reached; the release itself is only observable as
// a sanitizer leak report, not an assertion.
void test_rebroadcast_declined_send_releases_packet(void)
{
MockRadioInterface *mockIface = installMockIface();
mockIface->declineAll = true;
meshtastic_MeshPacket p = makeRebroadcastCandidate(NODENUM_BROADCAST);
TEST_ASSERT_TRUE_MESSAGE(shim->perhapsRebroadcast(&p), "the rebroadcast must still be attempted");
@@ -551,10 +848,23 @@ void setup()
initializeTestEnvironment();
UNITY_BEGIN();
airTimeFixture = std::make_unique<ScopedAirTimeFixture>();
mockNodeDB = new MockNodeDB();
shim = new NextHopRouterTestShim();
reliableShim = new ReliableRouterTestShim();
nodeDB = mockNodeDB;
auto nextRadio = std::make_unique<CaptureRadioInterface>();
nextHopRadio = nextRadio.get();
shim->addInterface(std::move(nextRadio));
auto reliableCapture = std::make_unique<CaptureRadioInterface>();
reliableRadio = reliableCapture.get();
reliableShim->addInterface(std::move(reliableCapture));
mockRoutingModule = new MockRoutingModule();
routingModule = mockRoutingModule;
printf("\n=== resolveLastByte (M1) ===\n");
RUN_TEST(test_resolve_none_when_empty);
RUN_TEST(test_resolve_zero_byte_is_none);
@@ -594,6 +904,15 @@ void setup()
RUN_TEST(test_hoplimit_decrement_on_colliding_favorites);
RUN_TEST(test_hoplimit_decrement_when_resolved_not_favorite);
printf("\n=== event-coordinate routing behavior ===\n");
RUN_TEST(test_eventPolicy_reliableOriginSendSuppressesTxAndPending);
RUN_TEST(test_eventPolicy_reliablePrivateCoordinateStillSends);
RUN_TEST(test_eventPolicy_floodingDuplicateSuppressesCoordinateButRelaysText);
RUN_TEST(test_eventPolicy_nextHopDuplicateSuppressesEventButRelaysPrivateCoordinate);
RUN_TEST(test_eventPolicy_repeatedLocalPacketSuppressesCoordinateAckButKeepsTextAck);
RUN_TEST(test_eventPolicy_seededRetrySuppressesTxUntilGateOff);
RUN_TEST(test_reliableAckStopsNormalPendingTransmission);
printf("\n=== rebroadcast of NODENUM_BROADCAST_NO_LORA ===\n");
RUN_TEST(test_rebroadcast_normal_broadcast_is_relayed);
RUN_TEST(test_rebroadcast_no_lora_broadcast_is_not_relayed);
@@ -602,7 +921,9 @@ void setup()
RUN_TEST(test_event_mode_hop_behavior);
#endif
exit(UNITY_END());
int result = UNITY_END();
airTimeFixture.reset();
exit(result);
}
void loop() {}
+185
View File
@@ -1,11 +1,16 @@
#include "Channels.h"
#include "GeoCoord.h"
#include "NodeDB.h"
#include "PositionPrecision.h"
#include "Router.h"
#include "TestUtil.h"
#include "mesh-pb-constants.h"
#include <cstdint>
#include <cstring>
#include <unity.h>
#if ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#endif
static meshtastic_Position makePosition()
{
@@ -129,6 +134,8 @@ static void test_getPositionPrecisionForChannel_clampsPreciseOnDefaultKeyChannel
channels.initDefaults(); // channel 0: primary, default key (psk {0x01}) -> publicly decryptable
uint8_t idx = 0;
meshtastic_Channel &ch = channels.getByIndex(idx);
ch.settings.psk.size = 1;
ch.settings.psk.bytes[0] = 0x01;
ch.settings.has_module_settings = true;
ch.settings.module_settings.position_precision = 32; // user requests "Precise" on a public channel
@@ -236,6 +243,178 @@ static void test_geocoord_extreme_coords_no_oob()
}
}
static void configureEventChannels(bool eventAtIndexOne, bool inheritEventKeyOnSecondary)
{
memset(&channelFile, 0, sizeof(channelFile));
channelFile.channels_count = 2;
meshtastic_Channel eventChannel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 16);
meshtastic_Channel otherChannel = makeChannel(meshtastic_Channel_Role_SECONDARY, true, 16);
strncpy(eventChannel.settings.name, "everyone", sizeof(eventChannel.settings.name) - 1);
#ifdef USERPREFS_CHANNEL_0_PSK
static const uint8_t configuredEventPsk[] = USERPREFS_CHANNEL_0_PSK;
eventChannel.settings.psk.size = sizeof(configuredEventPsk);
memcpy(eventChannel.settings.psk.bytes, configuredEventPsk, sizeof(configuredEventPsk));
#endif
if (!inheritEventKeyOnSecondary) {
otherChannel.settings.psk.size = 32;
memset(otherChannel.settings.psk.bytes, 0xAB, 32);
strncpy(otherChannel.settings.name, "private", sizeof(otherChannel.settings.name) - 1);
}
eventChannel.index = eventAtIndexOne ? 1 : 0;
otherChannel.index = eventAtIndexOne ? 0 : 1;
channelFile.channels[eventChannel.index] = eventChannel;
channelFile.channels[otherChannel.index] = otherChannel;
channels.onConfigChanged();
}
static void test_getPositionPrecisionForChannel_eventChannelClampedToZero()
{
// The event ("everyone") channel must never share location, even when the
// stored precision is non-zero. Under the block gate the clamp forces 0;
// otherwise the stored value is honored like any other channel.
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
configureEventChannels(false, false);
TEST_ASSERT_TRUE(channels.isEventChannel(0));
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(0));
#else
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 16);
TEST_ASSERT_EQUAL_UINT32(16, getPositionPrecisionForChannel(channel));
#endif
}
static void test_eventChannelIdentity_usesEffectiveKeyAndSurvivesReorder()
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
configureEventChannels(false, true);
TEST_ASSERT_TRUE(channels.isEventChannel(0));
TEST_ASSERT_TRUE(channels.isEventChannel(1));
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(1));
configureEventChannels(true, false);
TEST_ASSERT_FALSE(channels.isEventChannel(0));
TEST_ASSERT_TRUE(channels.isEventChannel(1));
TEST_ASSERT_EQUAL_UINT32(16, getPositionPrecisionForChannel(0));
TEST_ASSERT_EQUAL_UINT32(0, getPositionPrecisionForChannel(1));
#else
TEST_ASSERT_FALSE(channels.isEventChannel(0));
#endif
}
static meshtastic_MeshPacket makeDecodedPacket(meshtastic_PortNum portnum, uint8_t channelIndex)
{
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_default;
packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
packet.decoded.portnum = portnum;
packet.channel = channelIndex;
return packet;
}
static void test_eventCoordinatePolicy_coversPortsAndExcludesPki()
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
configureEventChannels(false, false);
auto position = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, 0);
auto waypoint = makeDecodedPacket(meshtastic_PortNum_WAYPOINT_APP, 0);
auto mapReport = makeDecodedPacket(meshtastic_PortNum_MAP_REPORT_APP, 0);
auto text = makeDecodedPacket(meshtastic_PortNum_TEXT_MESSAGE_APP, 0);
TEST_ASSERT_TRUE(isBlockedEventCoordinatePacket(&position));
TEST_ASSERT_TRUE(isBlockedEventCoordinatePacket(&waypoint));
TEST_ASSERT_TRUE(isBlockedEventCoordinatePacket(&mapReport));
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&text));
waypoint.pki_encrypted = true;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&waypoint));
waypoint.pki_encrypted = false;
waypoint.to = 0x12345678;
config.security.private_key.size = 32;
owner.is_licensed = false;
#if ARCH_PORTDUINO
portduino_config.force_simradio = false;
#endif
TEST_ASSERT_TRUE(willUsePki(&waypoint));
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&waypoint));
#else
auto position = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, 0);
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&position));
#endif
}
static void test_eventCoordinatePolicy_doesNotClassifyOpaquePacketsByHash()
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
configureEventChannels(false, false);
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_default;
packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
packet.channel = channels.getHash(0);
packet.from = 0;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&packet));
packet.pki_encrypted = true;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&packet));
packet.channel = 0;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&packet));
packet.pki_encrypted = false;
packet.channel = channels.getHash(0);
packet.from = 0x12345678;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&packet));
packet.from = 0;
packet.channel = channels.getHash(1);
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&packet));
#else
TEST_ASSERT_TRUE(true);
#endif
}
static void test_eventCoordinatePolicy_usesResolvedUnicastChannel()
{
#if USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL && defined(USERPREFS_CHANNEL_0_PSK)
NodeDB *savedNodeDB = nodeDB;
nodeDB = new NodeDB();
configureEventChannels(false, false);
meshtastic_NodeInfoLite *node =
nodeDB->getNumMeshNodes() > 1 ? nodeDB->getMeshNodeByIndex(1) : nodeDB->getOrCreateMeshNode(0x12345678);
TEST_ASSERT_NOT_NULL(node);
const NodeNum destination = node->num;
const uint8_t savedChannel = node->channel;
auto position = makeDecodedPacket(meshtastic_PortNum_POSITION_APP, 0);
position.to = destination;
node->channel = 1;
TEST_ASSERT_FALSE(isBlockedEventCoordinatePacket(&position));
position.from = 0x87654321;
TEST_ASSERT_TRUE(isBlockedEventCoordinatePacket(&position));
position.from = 0;
node->channel = 0;
TEST_ASSERT_TRUE(isBlockedEventCoordinatePacket(&position));
node->channel = savedChannel;
delete nodeDB;
nodeDB = savedNodeDB;
#else
TEST_ASSERT_TRUE(true);
#endif
}
static void test_getPositionPrecisionForChannel_nonEventFullKeyIsHonored()
{
// A private channel with a full 32-byte key that is not the configured
// channel-0 PSK must be
// unaffected by the clamp on either side of the gate.
meshtastic_Channel channel = makeChannel(meshtastic_Channel_Role_PRIMARY, true, 16);
channel.settings.psk.size = 32;
memset(channel.settings.psk.bytes, 0xAB, 32);
TEST_ASSERT_EQUAL_UINT32(16, getPositionPrecisionForChannel(channel));
}
void setUp(void) {}
void tearDown(void) {}
@@ -262,6 +441,12 @@ void setup()
RUN_TEST(test_cryptoKeyIsPublic_aes256KeyIsPrivate);
RUN_TEST(test_cryptoKeyIsPublic_invalidKeyIsNotPublic);
RUN_TEST(test_geocoord_extreme_coords_no_oob);
RUN_TEST(test_getPositionPrecisionForChannel_eventChannelClampedToZero);
RUN_TEST(test_eventChannelIdentity_usesEffectiveKeyAndSurvivesReorder);
RUN_TEST(test_eventCoordinatePolicy_coversPortsAndExcludesPki);
RUN_TEST(test_eventCoordinatePolicy_doesNotClassifyOpaquePacketsByHash);
RUN_TEST(test_eventCoordinatePolicy_usesResolvedUnicastChannel);
RUN_TEST(test_getPositionPrecisionForChannel_nonEventFullKeyIsHonored);
exit(UNITY_END());
}
+1
View File
@@ -25,6 +25,7 @@
// "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, and LOST AND FOUND roles are restricted.
// "USERPREFS_EVENT_MODE": "1",
// "USERPREFS_EVENT_MODE_HOP_LIMIT": "3", // Event-mode default and firmware-generated/relay hop cap (0-7; default 3)
// "USERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL": "1", // Block location TX + discard inbound location on channels keyed with USERPREFS_CHANNEL_0_PSK. Defaults on under EVENT_MODE.
// "USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS": "1", // Extend TMM position dedup and precision clamping to private/custom-key channels (default: well-known channels only)
// "USERPREFS_FIRMWARE_EDITION": "meshtastic_FirmwareEdition_BURNING_MAN",
// "USERPREFS_FIXED_BLUETOOTH": "121212",
+12
View File
@@ -137,6 +137,18 @@ test_testing_command =
${platformio.build_dir}/${this.__env__}/meshtasticd
-s
[env:coverage-event-policy]
extends = env:coverage
build_flags = ${env:coverage.build_flags}
-DUSERPREFS_BLOCK_POSITION_ON_EVENT_CHANNEL=1
-DUSERPREFS_CHANNEL_0_PSK='{0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f}'
test_filter =
test_position_precision
test_event_channel_router
test_nexthop_routing
test_event_channel_phone_api
test_mqtt
; ---------------------------------------------------------------------------
; Native build for macOS (Darwin / arm64 + x86_64). Headless meshtasticd that
; runs in SimRadio mode (`-s`) or against real LoRa hardware via a CH341