diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index bca46518c..9ea1c7b22 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -715,23 +715,32 @@ void Dispatcher::checkRecv() { #endif snr = _radio->getLastSNR(); rssi = _radio->getLastRSSI(); - logRxRaw(snr, rssi, raw, len); - pkt = _mgr->allocNew(); - if (pkt == NULL) { - MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime()); + // TRACE and CONTROL are direct-only. Reject a flood framing before + // raw hooks can expose it to USB logs, MQTT, ESP-NOW, or another bridge. + // The header check is deliberate: it also holds when the packet pool is + // exhausted, so an invalid frame never slips out through a raw sink. + if (Packet::violatesRoutePolicy(raw[0])) { + pkt = NULL; } else { - if (tryParsePacket(pkt, raw, len)) { - last_meshcore_recv_millis = _ms->getMillis(); - if (auto* p = _radio->profiles()) ++p->rx_packets[pkt->radio_profile]; - pkt->_snr = snr * 4.0f; - pkt->_rssi = (int16_t)rssi; - score = _radio->packetScore(snr, len); - air_time = _radio->getEstAirtimeFor(len); - rx_air_time += air_time; + logRxRaw(snr, rssi, raw, len); + + pkt = _mgr->allocNew(); + if (pkt == NULL) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime()); } else { - _mgr->free(pkt); // put back into pool - pkt = NULL; + if (tryParsePacket(pkt, raw, len)) { + last_meshcore_recv_millis = _ms->getMillis(); + if (auto* p = _radio->profiles()) ++p->rx_packets[pkt->radio_profile]; + pkt->_snr = snr * 4.0f; + pkt->_rssi = (int16_t)rssi; + score = _radio->packetScore(snr, len); + air_time = _radio->getEstAirtimeFor(len); + rx_air_time += air_time; + } else { + _mgr->free(pkt); // put back into pool + pkt = NULL; + } } } } else { @@ -978,6 +987,9 @@ void Dispatcher::releasePacket(Packet* packet) { } bool Dispatcher::queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { + if (packet->violatesRoutePolicy()) { + return false; + } if (!radio_available) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): radio unavailable", getLogDateTime()); return false; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index be7ed9e2a..754513c87 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -918,6 +918,11 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + // Trace and control payloads are direct-only. Dispatcher enforces this for + // RF input before any logging/bridge hook; retain the core check for packets + // arriving through another transport implementation. + if (pkt->violatesRoutePolicy()) return ACTION_RELEASE; + observeForwardedAdvertEcho(pkt); if (pkt->isRouteDirect()) { cancelDirectRetryOnEcho(pkt); @@ -3015,8 +3020,8 @@ bool Mesh::sendManagementData(Packet* packet, bool flood, const uint8_t* path, } bool Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) { - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); + if (packet == NULL) return false; + if (Packet::isDirectOnlyPayloadType(packet->getPayloadType())) { releasePacket(packet); return false; } @@ -3047,8 +3052,8 @@ bool Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_si } bool Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) { - if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { - MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); + if (packet == NULL) return false; + if (Packet::isDirectOnlyPayloadType(packet->getPayloadType())) { releasePacket(packet); return false; } diff --git a/src/Packet.h b/src/Packet.h index 0b20dd924..0770645a3 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -84,6 +84,20 @@ public: bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; } bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } + // TRACE gathers per-hop state and CONTROL can carry local discovery state. + // Neither has flood semantics. Keep this header-only so raw receive sinks + // can reject a prohibited packet before exporting it to USB/MQTT/bridges. + static bool isDirectOnlyPayloadType(uint8_t payload_type) { + return payload_type == PAYLOAD_TYPE_TRACE || payload_type == PAYLOAD_TYPE_CONTROL; + } + static bool violatesRoutePolicy(uint8_t packet_header) { + const uint8_t route = packet_header & PH_ROUTE_MASK; + const uint8_t payload_type = (packet_header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; + return (route == ROUTE_TYPE_FLOOD || route == ROUTE_TYPE_TRANSPORT_FLOOD) + && isDirectOnlyPayloadType(payload_type); + } + bool violatesRoutePolicy() const { return violatesRoutePolicy(header); } + bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; } /** diff --git a/src/helpers/AbstractBridge.h b/src/helpers/AbstractBridge.h index b45b002d0..961b3e8e8 100644 --- a/src/helpers/AbstractBridge.h +++ b/src/helpers/AbstractBridge.h @@ -11,7 +11,10 @@ protected: // Called on the mesh loop, before copying/marking/queuing a packet. Never // invoke the policy from a WiFi callback or the MQTT publishing task. bool allowsPacket(const mesh::Packet* packet) const { - return packet && (!_packet_filter + // Route policy is transport-independent: a flood-framed TRACE or CONTROL + // must never be relayed through a bridge even if the role has no custom + // bridge filter configured. + return packet && !packet->violatesRoutePolicy() && (!_packet_filter || _packet_filter(_packet_filter_context, packet)); } diff --git a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp index df902a7c0..47e54db2d 100644 --- a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp +++ b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp @@ -102,6 +102,12 @@ protected: completed_packet = packet; completed_packets++; } + void logRxRaw(float, float, const uint8_t[], int) override { + raw_receive_logs++; + } + void logRx(mesh::Packet*, int, float) override { + parsed_receive_logs++; + } public: mesh::Packet* failed_packet = nullptr; @@ -109,6 +115,8 @@ public: int completed_packets = 0; int free_count_during_failure = -1; int received_packets = 0; + int raw_receive_logs = 0; + int parsed_receive_logs = 0; int forced_rx_delay = -1; bool bypass_rx_delay = false; bool configured_cad_enabled = false; @@ -248,6 +256,85 @@ TEST(Dispatcher, FloodPacketWaitsForConfiguredRxDelayWithoutBypass) { EXPECT_EQ(1, dispatcher.received_packets); } +TEST(Dispatcher, FloodTraceAndControlAreDroppedBeforeLogsAndRouting) { + RxReservePacketManager manager(4, 1); + TestClock clock; + TestRadio radio; + TestDispatcher dispatcher(radio, clock, manager); + dispatcher.begin(); + + for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) { + for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) { + uint8_t raw[7] = {}; + int raw_len = 0; + raw[raw_len++] = route | (type << PH_TYPE_SHIFT); + if (route == ROUTE_TYPE_TRANSPORT_FLOOD) { + raw_len += 4; // transport codes + } + raw[raw_len++] = 0; // zero-hop path + raw[raw_len++] = 0x42; + + clock.now++; + radio.queueRx(raw, raw_len); + dispatcher.loop(); + } + } + + EXPECT_EQ(0, dispatcher.raw_receive_logs); + EXPECT_EQ(0, dispatcher.parsed_receive_logs); + EXPECT_EQ(0, dispatcher.received_packets); + EXPECT_EQ(0U, dispatcher.getLastMeshCoreRecvMillis()); + EXPECT_EQ(0U, dispatcher.getNumRecvFlood()); + EXPECT_EQ(4, manager.getFreeCount()); +} + +TEST(Dispatcher, DirectTraceAndControlRemainValid) { + RxReservePacketManager manager(4, 1); + TestClock clock; + TestRadio radio; + TestDispatcher dispatcher(radio, clock, manager); + dispatcher.begin(); + + for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) { + const uint8_t raw[] = { + static_cast(ROUTE_TYPE_DIRECT | (type << PH_TYPE_SHIFT)), 0, 0x42 + }; + clock.now++; + radio.queueRx(raw, sizeof(raw)); + dispatcher.loop(); + } + + EXPECT_EQ(2, dispatcher.raw_receive_logs); + EXPECT_EQ(2, dispatcher.parsed_receive_logs); + EXPECT_EQ(2, dispatcher.received_packets); + EXPECT_EQ(2U, dispatcher.getLastMeshCoreRecvMillis()); + EXPECT_EQ(2U, dispatcher.getNumRecvDirect()); +} + +TEST(Dispatcher, FloodTraceAndControlCannotBeQueuedForTransmit) { + RxReservePacketManager manager(4, 1); + TestClock clock; + TestRadio radio; + TestDispatcher dispatcher(radio, clock, manager); + dispatcher.begin(); + + for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) { + for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) { + mesh::Packet* packet = dispatcher.obtainNewPacket(); + ASSERT_NE(packet, nullptr); + packet->header = route | (type << PH_TYPE_SHIFT); + packet->setPathHashSizeAndCount(1, 0); + packet->payload_len = 1; + packet->payload[0] = 0x42; + EXPECT_FALSE(dispatcher.sendPacket(packet, 0)); + EXPECT_FALSE(dispatcher.hasOutbound()); + } + } + + EXPECT_EQ(4, manager.getFreeCount()); + EXPECT_EQ(0, radio.send_starts); +} + TEST(Dispatcher, RejectedRadioPacketsCannotFeedMeshCoreRxWatchdog) { RxReservePacketManager manager(4, 1); TestClock clock; diff --git a/test/test_trace_retry/test_trace_retry.cpp b/test/test_trace_retry/test_trace_retry.cpp index f0d3b90e1..c793da6f7 100644 --- a/test/test_trace_retry/test_trace_retry.cpp +++ b/test/test_trace_retry/test_trace_retry.cpp @@ -705,6 +705,26 @@ TEST(MeshReceiveHooks, GroupPacketIsObservedWhenForwardingIsDisabled) { EXPECT_TRUE(node.groupPacketObserved); } +TEST(MeshReceiveHooks, FloodTraceAndControlAreNeverForwarded) { + TraceTestClock clock; + TraceTestRTC rtc; + TraceTestRNG rng; + TraceTestRadio radio; + ForwardingTestTables tables; + StaticPoolPacketManager manager(12); + TraceTestMesh node(radio, clock, rng, rtc, manager, tables); + node.forwardFloods = true; + + for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) { + for (uint8_t type : {PAYLOAD_TYPE_TRACE, PAYLOAD_TYPE_CONTROL}) { + mesh::Packet packet = makeFloodPacket(type); + packet.header = route | (type << PH_TYPE_SHIFT); + EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet)); + EXPECT_EQ(0, tables.mark_seen_calls); + } + } +} + static mesh::Packet* makeTrace(TraceTestMesh& node, uint32_t tag, uint32_t auth, const uint8_t* route, uint8_t route_len) { mesh::Packet* packet = node.createTrace(tag, auth, 0);