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:
co-authored by
Sisyphus
Claude Opus 5
Ben Meadors
parent
fb17f6ddbe
commit
546b9d9e40
18 files changed
+1492
-36
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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};
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 @@
|
||||
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
@@ -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);
|
||||
|
||||
@@ -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() {}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user