mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 14:54:44 -05:00
PhoneAPI rate-limits a phone packet before MeshService clears the from the client supplied, so a forged from sent the RATE_LIMIT_EXCEEDED NAK over the air to that node. sendRoutingErrorResponse only answers phone packets, so it now addresses our own node.
1110 lines
40 KiB
C++
1110 lines
40 KiB
C++
#include "MeshModule.h"
|
|
#include "MeshTypes.h"
|
|
#include "TestUtil.h"
|
|
#include <unity.h>
|
|
|
|
#include "configuration.h"
|
|
#include "gps/RTC.h"
|
|
#include "mesh/CryptoEngine.h"
|
|
#include "mesh/MeshService.h"
|
|
#include "mesh/NodeDB.h"
|
|
#include "mesh/RadioInterface.h"
|
|
#include "mesh/Router.h"
|
|
#include "modules/NeighborInfoModule.h"
|
|
#include "modules/RoutingModule.h"
|
|
#include "support/MockMeshService.h"
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
constexpr NodeNum LOCAL_NODE = 0x11111111;
|
|
constexpr NodeNum REMOTE_NODE = 0x22222222;
|
|
|
|
// Minimal concrete subclass for testing the base class helper
|
|
class TestModule : public MeshModule
|
|
{
|
|
public:
|
|
TestModule() : MeshModule("TestModule") {}
|
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override { return true; }
|
|
using MeshModule::currentRequest;
|
|
using MeshModule::isMultiHopBroadcastRequest;
|
|
};
|
|
|
|
class MockNodeDB : public NodeDB
|
|
{
|
|
};
|
|
|
|
class MockRadioInterface : public RadioInterface
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) override
|
|
{
|
|
(void)totalPacketLen;
|
|
(void)received;
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
class MockRouter : public Router
|
|
{
|
|
public:
|
|
ErrorCode send(meshtastic_MeshPacket *p) override
|
|
{
|
|
sentPackets.push_back(*p);
|
|
packetPool.release(p);
|
|
return ERRNO_OK;
|
|
}
|
|
|
|
std::vector<meshtastic_MeshPacket> sentPackets;
|
|
};
|
|
|
|
struct AckNak {
|
|
meshtastic_Routing_Error error;
|
|
NodeNum to;
|
|
PacketId requestId;
|
|
ChannelIndex channel;
|
|
};
|
|
|
|
class MockRoutingModule : public RoutingModule
|
|
{
|
|
public:
|
|
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
|
bool ackWantsAck = false, const meshtastic_MeshPacket *relaySource = nullptr) override
|
|
{
|
|
(void)hopLimit;
|
|
(void)ackWantsAck;
|
|
(void)relaySource;
|
|
ackNaks.push_back({err, to, idFrom, chIndex});
|
|
}
|
|
|
|
std::vector<AckNak> ackNaks;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override
|
|
{
|
|
(void)p;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Refuses every packet the way AdminModule does: an error reply in myReply, whatever want_response says.
|
|
class RefusingModule : public MeshModule
|
|
{
|
|
public:
|
|
RefusingModule() : MeshModule("admin-shaped", meshtastic_PortNum_ADMIN_APP) {}
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp);
|
|
return ProcessMessage::CONTINUE;
|
|
}
|
|
};
|
|
|
|
class SyntheticReplyModule : public MeshModule
|
|
{
|
|
public:
|
|
SyntheticReplyModule(const char *name, meshtastic_PortNum modulePort, meshtastic_PortNum replyPort,
|
|
bool acceptsEveryPort = false)
|
|
: MeshModule(name, modulePort), replyPort(replyPort), acceptsEveryPort(acceptsEveryPort)
|
|
{
|
|
isPromiscuous = acceptsEveryPort;
|
|
}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return acceptsEveryPort || p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->decoded.portnum = replyPort;
|
|
return reply;
|
|
}
|
|
|
|
private:
|
|
meshtastic_PortNum replyPort;
|
|
bool acceptsEveryPort;
|
|
};
|
|
|
|
class ObservingIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ObservingIgnoreModule() : MeshModule("storeforward-shaped", meshtastic_PortNum_STORE_FORWARD_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
ignoreRequest = true;
|
|
return ProcessMessage::CONTINUE;
|
|
}
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
class ReplyIgnoreModule : public MeshModule
|
|
{
|
|
public:
|
|
ReplyIgnoreModule() : MeshModule("reply-ignore", meshtastic_PortNum_NEIGHBORINFO_APP) {}
|
|
|
|
uint32_t allocReplyCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
meshtastic_MeshPacket *allocReply() override
|
|
{
|
|
allocReplyCalls++;
|
|
ignoreRequest = true;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
// Sends, from inside callModules(), a self-addressed reply and a local broadcast - the fan-out an
|
|
// AdminModule config save performs. Both go out through service->sendToMesh() while a
|
|
// handleReceived() frame is live, so both must be deferred rather than handled re-entrantly.
|
|
class NestedFanoutModule : public MeshModule
|
|
{
|
|
public:
|
|
NestedFanoutModule() : MeshModule("nested-fanout", meshtastic_PortNum_PRIVATE_APP) {}
|
|
uint32_t handleCalls = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
handleCalls++;
|
|
|
|
// (a) a reply addressed to ourselves - exercises the isToUs() deferral branch
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0xBEEF;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
|
|
// (b) a local broadcast - exercises the broadcast branch (loopback deferred, TX immediate)
|
|
meshtastic_MeshPacket *bcast = router->allocForSending();
|
|
bcast->to = NODENUM_BROADCAST;
|
|
bcast->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
service->sendToMesh(bcast);
|
|
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// loopbackOk so it also runs for drained RX_SRC_LOCAL packets; each generation sends the next, so
|
|
// the drain must keep processing a chain that grows while it is draining.
|
|
class ChainSendModule : public MeshModule
|
|
{
|
|
public:
|
|
ChainSendModule() : MeshModule("chain-send", meshtastic_PortNum_PRIVATE_APP) { loopbackOk = true; }
|
|
uint32_t handleCalls = 0;
|
|
uint32_t maxGeneration = 0;
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
handleCalls++;
|
|
uint32_t generation = mp.decoded.request_id;
|
|
if (generation > maxGeneration)
|
|
maxGeneration = generation;
|
|
|
|
if (generation < 3) {
|
|
meshtastic_MeshPacket *next = router->allocForSending();
|
|
next->to = LOCAL_NODE;
|
|
next->decoded.portnum = ourPortNum;
|
|
next->decoded.request_id = generation + 1;
|
|
service->sendToMesh(next); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
};
|
|
|
|
// Sends a burst of self-addressed replies from one dispatch, to overflow the deferred queue.
|
|
class BurstSendModule : public MeshModule
|
|
{
|
|
public:
|
|
explicit BurstSendModule(uint32_t count) : MeshModule("burst-send", meshtastic_PortNum_PRIVATE_APP), count(count) {}
|
|
|
|
protected:
|
|
bool wantPacket(const meshtastic_MeshPacket *p) override { return p->decoded.portnum == ourPortNum; }
|
|
|
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override
|
|
{
|
|
(void)mp;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
meshtastic_MeshPacket *reply = router->allocForSending();
|
|
reply->to = LOCAL_NODE;
|
|
reply->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
reply->decoded.request_id = 0x100 + i;
|
|
service->sendToMesh(reply); // default src == RX_SRC_LOCAL
|
|
}
|
|
return ProcessMessage::STOP;
|
|
}
|
|
|
|
private:
|
|
uint32_t count;
|
|
};
|
|
|
|
static TestModule *testModule;
|
|
static meshtastic_MeshPacket testPacket;
|
|
static MockNodeDB *mockNodeDB;
|
|
static MockMeshService *mockService;
|
|
static MockRouter *mockRouter;
|
|
static MockRoutingModule *mockRoutingModule;
|
|
static NeighborInfoModule *realNeighborInfoModule;
|
|
static RoutingModule *realRoutingModule;
|
|
static std::vector<MeshModule *> dispatchModules;
|
|
|
|
template <typename T> static T *registerDispatchModule(T *module)
|
|
{
|
|
dispatchModules.push_back(module);
|
|
return module;
|
|
}
|
|
|
|
// Swap the mocked RoutingModule for a real one. tearDown() owns the cleanup because a failed
|
|
// assertion longjmps out of the test, which would otherwise leave it registered in MeshModule::modules.
|
|
static void installRealRoutingModule()
|
|
{
|
|
realRoutingModule = new RoutingModule();
|
|
routingModule = realRoutingModule;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeRequest(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero;
|
|
packet.from = REMOTE_NODE;
|
|
packet.to = LOCAL_NODE;
|
|
packet.id = 0x12345678;
|
|
packet.channel = 0;
|
|
packet.hop_start = 3;
|
|
packet.hop_limit = 3;
|
|
packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
packet.decoded.portnum = port;
|
|
packet.decoded.want_response = true;
|
|
return packet;
|
|
}
|
|
|
|
static void dispatch(meshtastic_PortNum port)
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(port);
|
|
MeshModule::callModules(request);
|
|
}
|
|
|
|
// Dispatch a want_response==false trigger addressed to us through the real router, so a module's
|
|
// handler runs inside a live handleReceived() frame (handleDepth == 1) and any packet it sends is
|
|
// deferred. requestId is carried in decoded.request_id for modules that key on a generation.
|
|
static void dispatchTrigger(meshtastic_PortNum port, uint32_t requestId = 0)
|
|
{
|
|
meshtastic_MeshPacket trigger = meshtastic_MeshPacket_init_zero;
|
|
trigger.from = LOCAL_NODE;
|
|
trigger.to = LOCAL_NODE;
|
|
trigger.id = 0x7A190000 + requestId;
|
|
trigger.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
trigger.decoded.portnum = port;
|
|
trigger.decoded.request_id = requestId;
|
|
service->sendToMesh(packetPool.allocCopy(trigger), RX_SRC_USER);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
config = meshtastic_LocalConfig_init_zero;
|
|
moduleConfig = meshtastic_LocalModuleConfig_init_zero;
|
|
channelFile = meshtastic_ChannelFile_init_zero;
|
|
owner = meshtastic_User_init_zero;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockNodeDB = new MockNodeDB();
|
|
nodeDB = mockNodeDB;
|
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
|
|
|
mockService = new MockMeshService();
|
|
service = mockService;
|
|
|
|
channels.initDefaults();
|
|
channels.onConfigChanged();
|
|
|
|
mockRouter = new MockRouter();
|
|
mockRouter->addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
|
router = mockRouter;
|
|
|
|
mockRoutingModule = new MockRoutingModule();
|
|
routingModule = mockRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
testModule = new TestModule();
|
|
memset(&testPacket, 0, sizeof(testPacket));
|
|
TestModule::currentRequest = &testPacket;
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
for (auto it = dispatchModules.rbegin(); it != dispatchModules.rend(); ++it)
|
|
delete *it;
|
|
dispatchModules.clear();
|
|
|
|
delete realNeighborInfoModule;
|
|
realNeighborInfoModule = nullptr;
|
|
|
|
delete testModule;
|
|
testModule = nullptr;
|
|
|
|
delete realRoutingModule;
|
|
realRoutingModule = nullptr;
|
|
|
|
delete mockRoutingModule;
|
|
mockRoutingModule = nullptr;
|
|
routingModule = nullptr;
|
|
|
|
while (auto *status = mockService->getQueueStatusForPhone())
|
|
mockService->releaseQueueStatusToPool(status);
|
|
while (auto *toPhone = mockService->getForPhone())
|
|
mockService->releaseToPool(toPhone);
|
|
delete mockService;
|
|
mockService = nullptr;
|
|
service = nullptr;
|
|
|
|
delete mockRouter;
|
|
mockRouter = nullptr;
|
|
router = nullptr;
|
|
|
|
delete mockNodeDB;
|
|
mockNodeDB = nullptr;
|
|
nodeDB = nullptr;
|
|
}
|
|
|
|
// Zero-hop broadcast (hop_limit == hop_start): should be allowed
|
|
static void test_zeroHopBroadcast_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 3; // Not yet relayed
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Multi-hop broadcast (hop_limit < hop_start): should be blocked
|
|
static void test_multiHopBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4; // Already relayed 3 hops
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Direct message (not broadcast): should always be allowed regardless of hops
|
|
static void test_directMessage_isAllowed()
|
|
{
|
|
testPacket.to = 0x12345678; // Specific node
|
|
testPacket.hop_start = 7;
|
|
testPacket.hop_limit = 4;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_limit == 0 (fully relayed): should be blocked
|
|
static void test_fullyRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 0;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// No current request: should not crash, should return false
|
|
static void test_noCurrentRequest_isAllowed()
|
|
{
|
|
TestModule::currentRequest = NULL;
|
|
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Broadcast with hop_start == 0 (legacy or local): should be allowed
|
|
static void test_legacyPacket_zeroHopStart_isAllowed()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 0;
|
|
testPacket.hop_limit = 0;
|
|
|
|
// hop_limit == hop_start, so not multi-hop
|
|
TEST_ASSERT_FALSE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
// Single hop relayed broadcast (hop_limit = hop_start - 1): should be blocked
|
|
static void test_singleHopRelayedBroadcast_isBlocked()
|
|
{
|
|
testPacket.to = NODENUM_BROADCAST;
|
|
testPacket.hop_start = 3;
|
|
testPacket.hop_limit = 2;
|
|
|
|
TEST_ASSERT_TRUE(testModule->isMultiHopBroadcastRequest());
|
|
}
|
|
|
|
static void test_replyPortMatches_ownPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_neighborInfoVsTelemetry()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_NEIGHBORINFO_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_positionRequest()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_TRUE(MeshModule::replyPortMatches(meshtastic_PortNum_POSITION_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownModulePort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_UNKNOWN_APP, request));
|
|
}
|
|
|
|
static void test_replyPortMatches_unknownRequestPort()
|
|
{
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_UNKNOWN_APP);
|
|
|
|
TEST_ASSERT_FALSE(MeshModule::replyPortMatches(meshtastic_PortNum_TELEMETRY_APP, request));
|
|
}
|
|
|
|
static void test_dispatch_foreignPortOffenderCannotShadowOwner()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_ownerPortStillReplies()
|
|
{
|
|
auto *offender = registerDispatchModule(new SyntheticReplyModule("neighbor-shaped", meshtastic_PortNum_NEIGHBORINFO_APP,
|
|
meshtastic_PortNum_NEIGHBORINFO_APP, true));
|
|
auto *owner = registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, offender->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, owner->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_NEIGHBORINFO_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_crossPortReplyUsesRequestOwner()
|
|
{
|
|
auto *position = registerDispatchModule(
|
|
new SyntheticReplyModule("position-owner", meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_ATAK_PLUGIN_V2));
|
|
|
|
dispatch(meshtastic_PortNum_POSITION_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, position->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ATAK_PLUGIN_V2, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_foreignPortObserverCanSuppressNak()
|
|
{
|
|
auto *observer = registerDispatchModule(new ObservingIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_TEXT_MESSAGE_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, observer->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
static void test_dispatch_noResponderSendsNak()
|
|
{
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, mockRoutingModule->ackNaks[0].to);
|
|
}
|
|
|
|
// A refused admin write asks for an ACK but no response. The refusal must reach the sender as a NAK,
|
|
// or ReliableRouter's plain ACK tells the client the write was applied.
|
|
static void test_dispatch_refusalWithoutWantResponse_sendsNak()
|
|
{
|
|
registerDispatchModule(new RefusingModule());
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_ADMIN_APP);
|
|
request.decoded.want_response = false;
|
|
request.want_ack = true;
|
|
|
|
MeshModule::callModules(request);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
const meshtastic_MeshPacket &nak = mockRouter->sentPackets[0];
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ROUTING_APP, nak.decoded.portnum);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, nak.to);
|
|
TEST_ASSERT_EQUAL_HEX32(request.id, nak.decoded.request_id);
|
|
TEST_ASSERT_FALSE(nak.want_ack);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// Without want_ack the sender expects no answer at all, so the refusal is still discarded.
|
|
static void test_dispatch_refusalWithoutWantAck_isDiscarded()
|
|
{
|
|
registerDispatchModule(new RefusingModule());
|
|
meshtastic_MeshPacket request = makeRequest(meshtastic_PortNum_ADMIN_APP);
|
|
request.decoded.want_response = false;
|
|
|
|
MeshModule::callModules(request);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
}
|
|
|
|
// PhoneAPI rate-limits a phone packet before MeshService clears the from the client supplied, so the
|
|
// NAK must go to our own node. Addressed by that from, a client could send it over the air to any node.
|
|
static void test_phoneErrorResponse_ignoresForgedFrom()
|
|
{
|
|
meshtastic_MeshPacket fromPhone = makeRequest(meshtastic_PortNum_POSITION_APP);
|
|
fromPhone.from = REMOTE_NODE;
|
|
|
|
service->sendRoutingErrorResponse(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, &fromPhone);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_RATE_LIMIT_EXCEEDED, mockRoutingModule->ackNaks[0].error);
|
|
TEST_ASSERT_EQUAL_HEX32(LOCAL_NODE, mockRoutingModule->ackNaks[0].to);
|
|
TEST_ASSERT_EQUAL_HEX32(fromPhone.id, mockRoutingModule->ackNaks[0].requestId);
|
|
}
|
|
|
|
static void test_dispatch_ignoreRequestIsClearedPerPacket()
|
|
{
|
|
auto *ignoring = registerDispatchModule(new ReplyIgnoreModule());
|
|
|
|
dispatch(meshtastic_PortNum_NEIGHBORINFO_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, ignoring->allocReplyCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRoutingModule->ackNaks.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NO_RESPONSE, mockRoutingModule->ackNaks[0].error);
|
|
}
|
|
|
|
static void test_dispatch_realNeighborInfoCannotShadowTelemetryOwner()
|
|
{
|
|
moduleConfig.neighbor_info.enabled = true;
|
|
realNeighborInfoModule = new NeighborInfoModule();
|
|
registerDispatchModule(
|
|
new SyntheticReplyModule("telemetry-owner", meshtastic_PortNum_TELEMETRY_APP, meshtastic_PortNum_TELEMETRY_APP));
|
|
|
|
dispatch(meshtastic_PortNum_TELEMETRY_APP);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_TELEMETRY_APP, mockRouter->sentPackets[0].decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A reply addressed to ourselves reaches the phone queue, with SHOULD_RELEASE reported as success.
|
|
static void test_localReplyToSelf_isDeliveredToPhone()
|
|
{
|
|
meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero;
|
|
reply.from = LOCAL_NODE;
|
|
reply.to = LOCAL_NODE;
|
|
reply.id = 0xABCD1234;
|
|
reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
reply.decoded.portnum = meshtastic_PortNum_ADMIN_APP;
|
|
reply.decoded.request_id = 0x12345678;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(reply)); // default src == RX_SRC_LOCAL, as callModules sends replies
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xABCD1234, toPhone->id);
|
|
TEST_ASSERT_EQUAL_UINT32(0x12345678, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // exactly one delivery
|
|
|
|
meshtastic_QueueStatus *qs = mockService->getQueueStatusForPhone();
|
|
TEST_ASSERT_NOT_NULL(qs);
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size()); // nothing went toward the radio
|
|
}
|
|
|
|
// handleFromRadio() is private to MeshService, which befriends RoutingModule and, under
|
|
// PIO_UNIT_TESTING, this seam.
|
|
class MeshServicePhoneDeliveryTest
|
|
{
|
|
public:
|
|
static void deliver(const meshtastic_MeshPacket &p) { service->handleFromRadio(&p); }
|
|
};
|
|
|
|
static void test_handleFromRadio_remotePacketReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket rx = meshtastic_MeshPacket_init_zero;
|
|
rx.from = REMOTE_NODE;
|
|
rx.to = NODENUM_BROADCAST;
|
|
rx.id = 0x0BADF00D;
|
|
rx.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
rx.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(rx);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet we originated, coming back around, must not be echoed to the client that sent it.
|
|
static void test_handleFromRadio_ownPacketIsNotEchoedToPhone()
|
|
{
|
|
meshtastic_MeshPacket ours = meshtastic_MeshPacket_init_zero;
|
|
ours.from = LOCAL_NODE;
|
|
ours.to = NODENUM_BROADCAST;
|
|
ours.id = 0x5E1F0001;
|
|
ours.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ours.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
|
|
// Same for the from==0 spelling handleToRadio stamps on phone-originated packets.
|
|
ours.from = 0;
|
|
ours.id = 0x5E1F0002;
|
|
MeshServicePhoneDeliveryTest::deliver(ours);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// A packet from us *addressed to us* is locally-generated feedback, not an echo, and must still be
|
|
// delivered - suppressing it would silently drop every ACK/NAK the client relies on.
|
|
static void test_handleFromRadio_ownPacketAddressedToUsReachesPhone()
|
|
{
|
|
meshtastic_MeshPacket ack = meshtastic_MeshPacket_init_zero;
|
|
ack.from = LOCAL_NODE;
|
|
ack.to = LOCAL_NODE;
|
|
ack.id = 0x5E1F0003;
|
|
ack.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
ack.decoded.portnum = meshtastic_PortNum_ROUTING_APP;
|
|
ack.decoded.request_id = 0x0C0FFEE0;
|
|
|
|
MeshServicePhoneDeliveryTest::deliver(ack);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0C0FFEE0, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// sendAckNak stamps from == our nodenum and to == us, and sendLocal defaults to RX_SRC_RADIO, so the
|
|
// loopback gate never applies and only handleFromRadio's filter gates the implicit ACK / NAK path.
|
|
static void test_localAckNak_reachesPhoneViaRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0xFEEDBEEF, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_ROUTING_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->from);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// #10767: the implicit ACK for an overheard rebroadcast of our own packet carries that copy's
|
|
// relaying node and the link metrics we heard it at, so the phone can attribute them to the relayer.
|
|
// rx_rssi has explicit presence, so has_rx_rssi has to travel with it or the reading never encodes.
|
|
static void test_localAck_carriesRelaySourceToPhone()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket overheard = meshtastic_MeshPacket_init_zero;
|
|
overheard.from = LOCAL_NODE;
|
|
overheard.to = REMOTE_NODE;
|
|
overheard.id = 0xFEEDBEEF;
|
|
overheard.relay_node = 0xAB;
|
|
overheard.has_rx_rssi = true;
|
|
overheard.rx_rssi = -93;
|
|
overheard.rx_snr = 4.75f;
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, overheard.id, 0, /*hopLimit=*/0,
|
|
/*ackWantsAck=*/false, &overheard);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0xFEEDBEEF, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(0xAB, toPhone->relay_node);
|
|
TEST_ASSERT_TRUE(toPhone->has_rx_rssi);
|
|
TEST_ASSERT_EQUAL_INT32(-93, toPhone->rx_rssi);
|
|
TEST_ASSERT_EQUAL_FLOAT(4.75f, toPhone->rx_snr);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// Every other ACK/NAK passes no relay source and must reach the phone with the relay fields clear,
|
|
// so the client is never told a relayer we did not hear.
|
|
static void test_localAck_withoutRelaySource_leavesRelayFieldsUnset()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
realRoutingModule->sendAckNak(meshtastic_Routing_Error_NONE, LOCAL_NODE, 0x0BADF00D, 0);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(0x0BADF00D, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_HEX8(NO_RELAY_NODE, toPhone->relay_node);
|
|
TEST_ASSERT_FALSE(toPhone->has_rx_rssi);
|
|
mockService->releaseToPool(toPhone);
|
|
}
|
|
|
|
// The mirror of the above: a broadcast we originated, heard back off the mesh, must not reach the
|
|
// phone even though it travels the same RoutingModule path.
|
|
static void test_ownBroadcastEcho_isDroppedByRealRoutingModule()
|
|
{
|
|
installRealRoutingModule();
|
|
|
|
meshtastic_MeshPacket echo = meshtastic_MeshPacket_init_zero;
|
|
echo.from = LOCAL_NODE;
|
|
echo.to = NODENUM_BROADCAST;
|
|
echo.id = 0x5E1F0004;
|
|
echo.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
echo.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
|
|
|
MeshModule::callModules(echo, RX_SRC_RADIO);
|
|
|
|
TEST_ASSERT_NULL(mockService->getForPhone());
|
|
}
|
|
|
|
// Full loop: a phone-originated want_response request (from == 0, RX_SRC_USER) dispatched
|
|
// through the real router must produce a module reply that reaches the phone queue.
|
|
static void test_phoneRequest_replyReachesPhone()
|
|
{
|
|
auto *replyOwner = registerDispatchModule(
|
|
new SyntheticReplyModule("private-owner", meshtastic_PortNum_PRIVATE_APP, meshtastic_PortNum_PRIVATE_APP));
|
|
|
|
meshtastic_MeshPacket request = meshtastic_MeshPacket_init_zero;
|
|
request.from = 0; // phone-originated, as MeshService::handleToRadio stamps it
|
|
request.to = LOCAL_NODE;
|
|
request.id = 0x5EED0001;
|
|
request.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
request.decoded.portnum = meshtastic_PortNum_PRIVATE_APP;
|
|
request.decoded.want_response = true;
|
|
|
|
service->sendToMesh(packetPool.allocCopy(request), RX_SRC_USER);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(1, replyOwner->allocReplyCalls);
|
|
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL(meshtastic_PortNum_PRIVATE_APP, toPhone->decoded.portnum);
|
|
TEST_ASSERT_EQUAL_UINT32(0x5EED0001, toPhone->decoded.request_id);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the request itself must not echo back
|
|
|
|
// One QueueStatus for the request, one for the reply, both reporting success
|
|
uint32_t statuses = 0;
|
|
while (auto *qs = mockService->getQueueStatusForPhone()) {
|
|
TEST_ASSERT_EQUAL(ERRNO_OK, qs->res);
|
|
mockService->releaseQueueStatusToPool(qs);
|
|
statuses++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(2, statuses);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_EQUAL_UINT32(0, mockRoutingModule->ackNaks.size());
|
|
}
|
|
|
|
// A module that fans out a self-addressed reply and a local broadcast from inside callModules()
|
|
// must not re-enter handleReceived(): the sends are deferred and drained flat (max depth 1).
|
|
static void test_nestedLocalSend_isDeferred_notReentrant()
|
|
{
|
|
auto *mod = registerDispatchModule(new NestedFanoutModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// The module ran once and was never re-entered; the drain left nothing behind.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
|
|
// The self-addressed reply reached the phone exactly once (composition with #11185's ccToPhone).
|
|
meshtastic_MeshPacket *toPhone = mockService->getForPhone();
|
|
TEST_ASSERT_NOT_NULL(toPhone);
|
|
TEST_ASSERT_EQUAL_UINT32(LOCAL_NODE, toPhone->to);
|
|
TEST_ASSERT_EQUAL_UINT32(0xBEEF, toPhone->decoded.request_id);
|
|
mockService->releaseToPool(toPhone);
|
|
TEST_ASSERT_NULL(mockService->getForPhone()); // the broadcast did not also go to the phone
|
|
|
|
// The broadcast still reached the radio TX path exactly once.
|
|
TEST_ASSERT_EQUAL_UINT32(1, mockRouter->sentPackets.size());
|
|
TEST_ASSERT_TRUE(isBroadcast(mockRouter->sentPackets[0].to));
|
|
}
|
|
|
|
// A drained packet whose own module sends again is picked up by the same drain pass, so a chain of
|
|
// local sends is processed breadth-first with the stack held flat (max depth 1).
|
|
static void test_deferredChain_drainsBreadthFirst()
|
|
{
|
|
auto *mod = registerDispatchModule(new ChainSendModule());
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP, 1); // generation 1
|
|
|
|
// Generations 1 -> 2 -> 3 were all processed in the single outermost drain, never nesting.
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->handleCalls);
|
|
TEST_ASSERT_EQUAL_UINT32(3, mod->maxGeneration);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending());
|
|
}
|
|
|
|
// Overflowing the fixed deferred queue drops the excess deferrals gracefully: no crash, no leak,
|
|
// depth still capped, and every send's phone cc still happens (return codes unchanged).
|
|
static void test_deferredQueueOverflow_dropsGracefully()
|
|
{
|
|
const uint32_t burst = 6; // deferredLocalCapacity (4) + 2 - keep in sync with Router.h
|
|
|
|
registerDispatchModule(new BurstSendModule(burst));
|
|
|
|
dispatchTrigger(meshtastic_PortNum_PRIVATE_APP);
|
|
|
|
// Two deferrals were dropped (queue full) and nothing re-entered handleReceived().
|
|
TEST_ASSERT_EQUAL_UINT32(2, mockRouter->deferredLocalDropped);
|
|
TEST_ASSERT_EQUAL_UINT8(1, mockRouter->maxHandleDepthObserved);
|
|
TEST_ASSERT_EQUAL_UINT8(0, mockRouter->deferredLocalPending()); // drained clean
|
|
|
|
// Return codes were unchanged: every reply still cc'd to the phone, dropped deferral or not.
|
|
uint32_t delivered = 0;
|
|
while (auto *toPhone = mockService->getForPhone()) {
|
|
mockService->releaseToPool(toPhone);
|
|
delivered++;
|
|
}
|
|
TEST_ASSERT_EQUAL_UINT32(burst, delivered);
|
|
}
|
|
|
|
// NeighborInfo: relays forward the payload unmodified (rewriting it breaks the sender's XEdDSA signature), so a
|
|
// receiver attributes each copy to the node it heard it from using the packet header, not last_sent_by_id.
|
|
class NeighborInfoModuleTestShim : public NeighborInfoModule
|
|
{
|
|
public:
|
|
using MeshModule::alterReceived;
|
|
using NeighborInfoModule::collectNeighborInfo;
|
|
using NeighborInfoModule::updateNeighbors;
|
|
};
|
|
|
|
static constexpr NodeNum NI_NEIGHBOR = 0x33333333;
|
|
|
|
static NeighborInfoModuleTestShim *makeNeighborInfoModule()
|
|
{
|
|
moduleConfig.neighbor_info.enabled = true;
|
|
moduleConfig.neighbor_info.update_interval = 3600;
|
|
auto *module = new NeighborInfoModuleTestShim();
|
|
realNeighborInfoModule = module; // tearDown() deletes it
|
|
return module;
|
|
}
|
|
|
|
static meshtastic_MeshPacket makeNeighborInfoPacket(NodeNum lastSentBy, uint8_t hopStart, uint8_t hopLimit, uint8_t relayNode,
|
|
meshtastic_NeighborInfo *ni)
|
|
{
|
|
*ni = meshtastic_NeighborInfo_init_zero;
|
|
ni->node_id = REMOTE_NODE;
|
|
ni->last_sent_by_id = lastSentBy;
|
|
ni->node_broadcast_interval_secs = 3600;
|
|
ni->neighbors_count = 1;
|
|
ni->neighbors[0].node_id = NI_NEIGHBOR;
|
|
ni->neighbors[0].snr = 5.0f;
|
|
|
|
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
|
|
p.from = REMOTE_NODE;
|
|
p.to = NODENUM_BROADCAST;
|
|
p.id = 0x0A0B0C0D;
|
|
p.hop_start = hopStart;
|
|
p.hop_limit = hopLimit;
|
|
p.relay_node = relayNode;
|
|
p.rx_snr = 7.0f;
|
|
p.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
|
p.decoded.portnum = meshtastic_PortNum_NEIGHBORINFO_APP;
|
|
p.decoded.payload.size =
|
|
pb_encode_to_bytes(p.decoded.payload.bytes, sizeof(p.decoded.payload.bytes), &meshtastic_NeighborInfo_msg, ni);
|
|
return p;
|
|
}
|
|
|
|
// Our recorded neighbours, as we would broadcast them.
|
|
static meshtastic_NeighborInfo collectedNeighbors(NeighborInfoModuleTestShim *module)
|
|
{
|
|
meshtastic_NeighborInfo out = meshtastic_NeighborInfo_init_zero;
|
|
module->collectNeighborInfo(&out);
|
|
return out;
|
|
}
|
|
|
|
static void test_neighborInfo_relayedPayloadIsNotModified()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(REMOTE_NODE, 3, 2, 0x44, &ni);
|
|
const meshtastic_Data_payload_t original = p.decoded.payload;
|
|
|
|
module->alterReceived(p);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(original.size, p.decoded.payload.size);
|
|
TEST_ASSERT_EQUAL_MEMORY(original.bytes, p.decoded.payload.bytes, original.size);
|
|
}
|
|
|
|
static void test_neighborInfo_directCopyRecordsSenderAndOursOmitsLastSentBy()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(0, 3, 3, 0x22, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(REMOTE_NODE, out.neighbors[0].node_id);
|
|
TEST_ASSERT_EQUAL_HEX32(LOCAL_NODE, out.node_id);
|
|
TEST_ASSERT_EQUAL_HEX32(0, out.last_sent_by_id);
|
|
}
|
|
|
|
static void test_neighborInfo_olderRelayRewriteMatchingHeaderIsTrusted()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
const NodeNum oldRelay = 0x55555544;
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(oldRelay, 3, 2, 0x44, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(oldRelay, out.neighbors[0].node_id);
|
|
}
|
|
|
|
static void test_neighborInfo_relayResolvedFromHeaderByte()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
const NodeNum relay = 0x77777766;
|
|
meshtastic_NodeInfoLite *node = nodeDB->getOrCreateMeshNode(relay);
|
|
TEST_ASSERT_NOT_NULL(node);
|
|
node->has_hops_away = true;
|
|
node->hops_away = 0;
|
|
node->last_heard = getTime();
|
|
|
|
// Unmodified payload from an older sender: last_sent_by_id still names the originator.
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(REMOTE_NODE, 3, 2, 0x66, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
meshtastic_NeighborInfo out = collectedNeighbors(module);
|
|
TEST_ASSERT_EQUAL_UINT32(1, out.neighbors_count);
|
|
TEST_ASSERT_EQUAL_HEX32(relay, out.neighbors[0].node_id);
|
|
}
|
|
|
|
static void test_neighborInfo_unresolvableRelayRecordsNothing()
|
|
{
|
|
auto *module = makeNeighborInfoModule();
|
|
meshtastic_NeighborInfo ni;
|
|
meshtastic_MeshPacket p = makeNeighborInfoPacket(0, 3, 2, 0x99, &ni);
|
|
|
|
module->updateNeighbors(p, &ni);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(0, collectedNeighbors(module).neighbors_count);
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_zeroHopBroadcast_isAllowed);
|
|
RUN_TEST(test_multiHopBroadcast_isBlocked);
|
|
RUN_TEST(test_directMessage_isAllowed);
|
|
RUN_TEST(test_fullyRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_noCurrentRequest_isAllowed);
|
|
RUN_TEST(test_legacyPacket_zeroHopStart_isAllowed);
|
|
RUN_TEST(test_singleHopRelayedBroadcast_isBlocked);
|
|
RUN_TEST(test_replyPortMatches_ownPort);
|
|
RUN_TEST(test_replyPortMatches_neighborInfoVsTelemetry);
|
|
RUN_TEST(test_replyPortMatches_positionRequest);
|
|
RUN_TEST(test_replyPortMatches_unknownModulePort);
|
|
RUN_TEST(test_replyPortMatches_unknownRequestPort);
|
|
RUN_TEST(test_dispatch_foreignPortOffenderCannotShadowOwner);
|
|
RUN_TEST(test_dispatch_ownerPortStillReplies);
|
|
RUN_TEST(test_dispatch_crossPortReplyUsesRequestOwner);
|
|
RUN_TEST(test_dispatch_foreignPortObserverCanSuppressNak);
|
|
RUN_TEST(test_dispatch_noResponderSendsNak);
|
|
RUN_TEST(test_dispatch_refusalWithoutWantResponse_sendsNak);
|
|
RUN_TEST(test_dispatch_refusalWithoutWantAck_isDiscarded);
|
|
RUN_TEST(test_phoneErrorResponse_ignoresForgedFrom);
|
|
RUN_TEST(test_dispatch_ignoreRequestIsClearedPerPacket);
|
|
RUN_TEST(test_dispatch_realNeighborInfoCannotShadowTelemetryOwner);
|
|
RUN_TEST(test_localReplyToSelf_isDeliveredToPhone);
|
|
RUN_TEST(test_handleFromRadio_remotePacketReachesPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketIsNotEchoedToPhone);
|
|
RUN_TEST(test_handleFromRadio_ownPacketAddressedToUsReachesPhone);
|
|
RUN_TEST(test_localAckNak_reachesPhoneViaRealRoutingModule);
|
|
RUN_TEST(test_localAck_carriesRelaySourceToPhone);
|
|
RUN_TEST(test_localAck_withoutRelaySource_leavesRelayFieldsUnset);
|
|
RUN_TEST(test_ownBroadcastEcho_isDroppedByRealRoutingModule);
|
|
RUN_TEST(test_phoneRequest_replyReachesPhone);
|
|
RUN_TEST(test_nestedLocalSend_isDeferred_notReentrant);
|
|
RUN_TEST(test_deferredChain_drainsBreadthFirst);
|
|
RUN_TEST(test_deferredQueueOverflow_dropsGracefully);
|
|
RUN_TEST(test_neighborInfo_relayedPayloadIsNotModified);
|
|
RUN_TEST(test_neighborInfo_directCopyRecordsSenderAndOursOmitsLastSentBy);
|
|
RUN_TEST(test_neighborInfo_olderRelayRewriteMatchingHeaderIsTrusted);
|
|
RUN_TEST(test_neighborInfo_relayResolvedFromHeaderByte);
|
|
RUN_TEST(test_neighborInfo_unresolvableRelayRecordsNothing);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|