Files
Thomas Göttgens 3b608b8fc5 fix(mesh): keep ROUTING_APP responses when toPhoneQueue is full (#11480)
* fix(mesh): keep ROUTING_APP responses when toPhoneQueue is full

#2918 narrowed the overflow policy to evict the oldest entry only for
TEXT_MESSAGE_APP and RANGE_TEST_APP, dropping every other portnum. A
dropped ROUTING_APP response leaves the phone with no delivery
confirmation for a message it sent.

Add ROUTING_APP to the eviction list and pin the policy in
test/test_tophone_queue.

Fixes #11439

* fix(mesh): gate the queue-overflow portnum check on the decoded variant

decoded.portnum aliases encrypted.size in the payload union, so an
encrypted packet could be read as a privileged portnum by its ciphertext
length. Restore config.device.rebroadcast_mode in the test teardown.

* test: rename a test to avoid a trufflehog false positive

test_text_still_admitted_when_queue_full is "test_" followed by exactly
35 characters, which matches the Lob API key shape and fails trunk check.
2026-08-13 13:12:54 -04:00

168 lines
4.9 KiB
C++

#include "MeshTypes.h"
#include "TestUtil.h"
#include <unity.h>
#if ARCH_PORTDUINO // portduino_config.maxtophone is what sizes the queue under test
#include "configuration.h"
#include "mesh/MeshService.h"
#include "mesh/NodeDB.h"
#include "platform/portduino/PortduinoGlue.h"
#include <cstdio>
#include <cstdlib>
#include <vector>
// Queue depth for the suite. MAX_RX_TOPHONE resolves to portduino_config.maxtophone, read when
// MeshService constructs its queue.
static const int TEST_QUEUE_LEN = 4;
static MeshService *testService = nullptr;
static MeshService *savedService = nullptr;
static int savedMaxToPhone = 0;
static meshtastic_Config_DeviceConfig_RebroadcastMode savedRebroadcastMode;
static meshtastic_MeshPacket basePacket(uint32_t id)
{
meshtastic_MeshPacket p = meshtastic_MeshPacket_init_zero;
p.from = 0x11223344;
p.to = NODENUM_BROADCAST;
p.id = id;
return p;
}
static void sendPacket(const meshtastic_MeshPacket &src)
{
meshtastic_MeshPacket *p = packetPool.allocCopy(src);
TEST_ASSERT_NOT_NULL(p);
service->sendToPhone(p);
}
static void send(uint32_t id, meshtastic_PortNum portnum, uint32_t requestId = 0)
{
meshtastic_MeshPacket src = basePacket(id);
src.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
src.decoded.portnum = portnum;
src.decoded.request_id = requestId;
sendPacket(src);
}
static void fillWith(meshtastic_PortNum portnum, uint32_t firstId)
{
for (int i = 0; i < TEST_QUEUE_LEN; i++)
send(firstId + i, portnum);
}
/// Drain the queue, returning the delivered packet ids in order.
static std::vector<uint32_t> drainIds()
{
std::vector<uint32_t> ids;
while (meshtastic_MeshPacket *p = service->getForPhone()) {
ids.push_back(p->id);
service->releaseToPool(p);
}
return ids;
}
static void assertIds(const std::vector<uint32_t> &expected, const char *what)
{
const std::vector<uint32_t> actual = drainIds();
TEST_ASSERT_EQUAL_INT_MESSAGE((int)expected.size(), (int)actual.size(), what);
for (size_t i = 0; i < expected.size(); i++)
TEST_ASSERT_EQUAL_UINT32_MESSAGE(expected[i], actual[i], what);
}
// An ACK/NAK is the phone's only delivery confirmation, so it must displace the oldest packet
// rather than be dropped when sustained downlink keeps the queue full.
static void test_routing_response_admitted_when_queue_full(void)
{
fillWith(meshtastic_PortNum_TELEMETRY_APP, 1);
send(100, meshtastic_PortNum_ROUTING_APP, /*requestId=*/7);
assertIds({2, 3, 4, 100}, "oldest telemetry should have been evicted for the routing response");
}
static void test_text_evicts_oldest_when_full(void)
{
fillWith(meshtastic_PortNum_TELEMETRY_APP, 1);
send(200, meshtastic_PortNum_TEXT_MESSAGE_APP);
assertIds({2, 3, 4, 200}, "text should still evict the oldest packet");
}
static void test_low_priority_packet_still_dropped_when_full(void)
{
fillWith(meshtastic_PortNum_TEXT_MESSAGE_APP, 1);
send(200, meshtastic_PortNum_TELEMETRY_APP);
assertIds({1, 2, 3, 4}, "a low-priority arrival should still be dropped on a full queue");
}
// decoded.portnum aliases encrypted.size in the payload union, so a still-encrypted packet whose
// ciphertext length happens to equal a privileged portnum must not be read as one.
static void test_encrypted_packet_is_not_classified_by_portnum(void)
{
fillWith(meshtastic_PortNum_TELEMETRY_APP, 1);
meshtastic_MeshPacket src = basePacket(300);
src.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
src.encrypted.size = meshtastic_PortNum_ROUTING_APP;
sendPacket(src);
assertIds({1, 2, 3, 4}, "an encrypted packet must not be classified from the aliased portnum");
}
void setUp(void)
{
savedMaxToPhone = portduino_config.maxtophone;
savedRebroadcastMode = config.device.rebroadcast_mode;
portduino_config.maxtophone = TEST_QUEUE_LEN;
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL;
testService = new MeshService();
savedService = service;
service = testService;
}
void tearDown(void)
{
drainIds(); // the queue owns its pointers; a failed assertion longjmps past any in-test drain
service = savedService;
delete testService;
testService = nullptr;
portduino_config.maxtophone = savedMaxToPhone;
config.device.rebroadcast_mode = savedRebroadcastMode;
}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
printf("\n=== toPhoneQueue overflow policy ===\n");
RUN_TEST(test_routing_response_admitted_when_queue_full);
RUN_TEST(test_text_evicts_oldest_when_full);
RUN_TEST(test_low_priority_packet_still_dropped_when_full);
RUN_TEST(test_encrypted_packet_is_not_classified_by_portnum);
exit(UNITY_END());
}
void loop() {}
#else // !ARCH_PORTDUINO
void setUp(void) {}
void tearDown(void) {}
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
exit(UNITY_END());
}
void loop() {}
#endif