diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 6f70dfe286f..dc244055567 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -192,7 +192,9 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], } RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { - if (packet->transport_codes[0] == 0) return &wildcard; + if (packet->transport_codes[0] == 0) { // code 0 = un-scoped: same rules as the wildcard (config + runtime gate) + return (wildcard.effectiveFlags() & mask) == 0 ? &wildcard : NULL; + } for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if ((region->effectiveFlags() & mask) == 0) { // does region allow this? (config + runtime gate) diff --git a/src/helpers/RoutingPolicy.h b/src/helpers/RoutingPolicy.h index 5251a590a42..896ae513f8a 100644 --- a/src/helpers/RoutingPolicy.h +++ b/src/helpers/RoutingPolicy.h @@ -4,11 +4,20 @@ namespace mesh { +/** + * \brief Is this an un-scoped flood? ROUTE_TYPE_FLOOD, or a TRANSPORT_FLOOD carrying transport + * code 0, which RegionMap::findMatch() treats the same as un-scoped. + */ +inline bool isUnscopedFlood(const Packet* packet) { + if (packet->getRouteType() == ROUTE_TYPE_FLOOD) return true; + return packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD && packet->transport_codes[0] == 0; +} + /** * \brief Test a flood packet against the configured hop limits. * \param packet inbound flood packet (caller has already checked isRouteFlood()) * \param flood_max max hops for any flood packet - * \param flood_max_unscoped max hops for ROUTE_TYPE_FLOOD (ie. un-scoped) packets + * \param flood_max_unscoped max hops for un-scoped packets (see isUnscopedFlood()) * \param flood_max_advert max hops for ADVERT packets * \returns true if the packet has exceeded a limit, and must not be forwarded */ @@ -16,7 +25,7 @@ inline bool isFloodHopLimitExceeded(const Packet* packet, uint8_t flood_max, uint8_t flood_max_unscoped, uint8_t flood_max_advert) { uint8_t hops = packet->getPathHashCount(); if (hops >= flood_max) return true; - if (packet->getRouteType() == ROUTE_TYPE_FLOOD && hops >= flood_max_unscoped) return true; + if (isUnscopedFlood(packet) && hops >= flood_max_unscoped) return true; if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && hops >= flood_max_advert) return true; return false; } diff --git a/test/test_region_gating/test_region_gating.cpp b/test/test_region_gating/test_region_gating.cpp index 1d6703ce968..db6c627fb50 100644 --- a/test/test_region_gating/test_region_gating.cpp +++ b/test/test_region_gating/test_region_gating.cpp @@ -1,5 +1,6 @@ #include #include "helpers/RegionMap.h" +#include // Unit tests for duty-cycle region gating (issue #2747). // These exercise the pure hierarchy/level logic; no crypto or filesystem is touched. @@ -223,6 +224,55 @@ TEST_F(NlChainGating, RecoveryRestoresConfiguredBehaviour) { EXPECT_FALSE(eu->effectiveFlags() & REGION_DENY_FLOOD); // flood allowed again } +// ---------- transport code 0 (upstream #3568: 'treat code 0 the same as un-scoped') ---------- +// Code 0 maps to the wildcard, so it must obey the wildcard's flags (config denyf and the +// runtime duty gate) exactly like a plain un-scoped flood, instead of bypassing them. + +static mesh::Packet makeTransportFlood(uint16_t code0, uint16_t code1 = 0) { + mesh::Packet p; + p.header = ROUTE_TYPE_TRANSPORT_FLOOD | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT); + p.payload_len = 4; + memcpy(p.payload, "abcd", 4); + p.transport_codes[0] = code0; + p.transport_codes[1] = code1; + return p; +} + +TEST_F(NlChainGating, ZeroCodeMatchesWildcardWhenWildcardAllowsFlood) { + auto pkt = makeTransportFlood(0); + EXPECT_EQ(rm.findMatch(&pkt, REGION_DENY_FLOOD), &rm.getWildcard()); +} + +TEST_F(NlChainGating, ZeroCodeIsDeniedWhenWildcardDeniesFlood) { + rm.getWildcard().flags |= REGION_DENY_FLOOD; // 'region denyf *' + auto pkt = makeTransportFlood(0); + EXPECT_EQ(rm.findMatch(&pkt, REGION_DENY_FLOOD), nullptr); +} + +TEST_F(NlChainGating, ZeroCodeIsDeniedWhileDutyGateHoldsWildcard) { + rm.applyDutyGate(1); // runtime gate on '*', config still allows flood + auto pkt = makeTransportFlood(0); + EXPECT_EQ(rm.findMatch(&pkt, REGION_DENY_FLOOD), nullptr); +} + +TEST_F(NlChainGating, ZeroCodeWithReturnRegionHintIsStillUnscoped) { + rm.getWildcard().flags |= REGION_DENY_FLOOD; + auto pkt = makeTransportFlood(0, 0x1234); // codes[1] carries a hint, codes[0] is still 0 + EXPECT_EQ(rm.findMatch(&pkt, REGION_DENY_FLOOD), nullptr); +} + +TEST_F(NlChainGating, NamedRegionStillMatchesWhenWildcardDeniesFlood) { + rm.getWildcard().flags |= REGION_DENY_FLOOD; + RegionEntry* nl = rm.findByName("nl"); + nl->flags &= (uint8_t)~REGION_DENY_FLOOD; // 'region allowf nl' + + auto pkt = makeTransportFlood(0); + TransportKey key; + ASSERT_EQ(rm.getTransportKeysFor(*nl, &key, 1), 1); + pkt.transport_codes[0] = key.calcTransportCode(&pkt); + EXPECT_EQ(rm.findMatch(&pkt, REGION_DENY_FLOOD), nl); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/test/test_routing_policy/test_routing_policy.cpp b/test/test_routing_policy/test_routing_policy.cpp index bba9ec79013..f44cbfc32cf 100644 --- a/test/test_routing_policy/test_routing_policy.cpp +++ b/test/test_routing_policy/test_routing_policy.cpp @@ -3,11 +3,14 @@ using namespace mesh; -static Packet makeFlood(uint8_t route_type, uint8_t payload_type, uint8_t hops) { +// scoped packets get a real (non-zero) transport code unless the test asks for code 0 +static Packet makeFlood(uint8_t route_type, uint8_t payload_type, uint8_t hops, uint16_t code0 = 0x1234) { Packet p; p.header = route_type | (payload_type << PH_TYPE_SHIFT); p.setPathHashSizeAndCount(1, hops); p.payload_len = 1; + p.transport_codes[0] = code0; + p.transport_codes[1] = 0; return p; } @@ -50,6 +53,42 @@ TEST(FloodHopLimit, UnscopedAdvertIsAlsoDroppedAtHopZero) { EXPECT_FALSE(isFloodHopLimitExceeded(&scoped, 64, 0, 8)); } +// transport code 0 is 'un-scoped' (upstream #3568): it must not escape flood.max.unscoped +TEST(UnscopedFlood, PlainFloodIsUnscoped) { + auto pkt = makeFlood(ROUTE_TYPE_FLOOD, PAYLOAD_TYPE_TXT_MSG, 0); + EXPECT_TRUE(isUnscopedFlood(&pkt)); +} + +TEST(UnscopedFlood, TransportFloodWithZeroCodeIsUnscoped) { + auto pkt = makeFlood(ROUTE_TYPE_TRANSPORT_FLOOD, PAYLOAD_TYPE_TXT_MSG, 0, 0); + EXPECT_TRUE(isUnscopedFlood(&pkt)); +} + +TEST(UnscopedFlood, TransportFloodWithRealCodeIsScoped) { + auto pkt = makeFlood(ROUTE_TYPE_TRANSPORT_FLOOD, PAYLOAD_TYPE_TXT_MSG, 0); + EXPECT_FALSE(isUnscopedFlood(&pkt)); +} + +TEST(UnscopedFlood, DirectRoutesAreNeverUnscopedFloods) { + auto direct = makeFlood(ROUTE_TYPE_DIRECT, PAYLOAD_TYPE_TXT_MSG, 0); + EXPECT_FALSE(isUnscopedFlood(&direct)); + auto tdirect = makeFlood(ROUTE_TYPE_TRANSPORT_DIRECT, PAYLOAD_TYPE_TXT_MSG, 0, 0); + EXPECT_FALSE(isUnscopedFlood(&tdirect)); +} + +TEST(FloodHopLimit, ZeroCodeTransportFloodIsDroppedAtFirstHopWhenMaxUnscopedIsZero) { + auto pkt = makeFlood(ROUTE_TYPE_TRANSPORT_FLOOD, PAYLOAD_TYPE_TXT_MSG, 0, 0); + EXPECT_TRUE(isFloodHopLimitExceeded(&pkt, 64, 0, 8)); +} + +TEST(FloodHopLimit, ZeroCodeTransportFloodSurvivesUpToMaxUnscopedHops) { + auto ok = makeFlood(ROUTE_TYPE_TRANSPORT_FLOOD, PAYLOAD_TYPE_TXT_MSG, 2, 0); + EXPECT_FALSE(isFloodHopLimitExceeded(&ok, 64, 3, 8)); + + auto too_far = makeFlood(ROUTE_TYPE_TRANSPORT_FLOOD, PAYLOAD_TYPE_TXT_MSG, 3, 0); + EXPECT_TRUE(isFloodHopLimitExceeded(&too_far, 64, 3, 8)); +} + TEST(ReplyRoute, FloodRequestGetsAPathReturn) { EXPECT_EQ(REPLY_ROUTE_PATH_RETURN, chooseReplyRoute(true, false, false));