mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-22 11:17:49 +10:00
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
@@ -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() {}
|
||||
}
|
||||
Reference in New Issue
Block a user