#include #include #include #include #include #include #include #include class TraceTestClock : public mesh::MillisecondClock { public: unsigned long now = 0; unsigned long getMillis() override { return now; } }; class TraceTestRTC : public mesh::RTCClock { public: uint32_t now = 0; uint32_t getCurrentTime() override { return now; } void setCurrentTime(uint32_t time) override { now = time; } }; class TraceTestRNG : public mesh::RNG { public: uint32_t value = 0; void random(uint8_t* dest, size_t sz) override { for (size_t offset = 0; offset < sz; offset++) { dest[offset] = (uint8_t)(value >> (8 * (offset % sizeof(value)))); } } }; class TraceTestRadio : public mesh::Radio { public: bool sending = false; bool complete = false; int recvRaw(uint8_t*, int) override { return 0; } uint32_t getEstAirtimeFor(int) override { return 10; } float packetScore(float, int) override { return 0; } bool startSendRaw(const uint8_t*, int) override { sending = true; return true; } bool isSendComplete() override { return complete; } void onSendFinished() override { sending = false; complete = false; } bool isInRecvMode() const override { return !sending; } }; class TraceTestTables : public mesh::MeshTables { public: bool wasSeen(const mesh::Packet*) override { return false; } void markSeen(const mesh::Packet*) override { } void markSent(const mesh::Packet*) override { } void clear(const mesh::Packet*) override { } }; class ForwardingTestTables : public mesh::MeshTables { public: bool seen = false; int mark_seen_calls = 0; bool wasSeen(const mesh::Packet*) override { return seen; } void markSeen(const mesh::Packet*) override { seen = true; mark_seen_calls++; } void markSent(const mesh::Packet*) override { } void clear(const mesh::Packet*) override { } }; class TraceTestMesh : public mesh::Mesh { public: bool forwardFloods = false; bool floodRetriesAllowed = true; bool groupPacketObserved = false; bool tempRadioActive = false; bool rejectFloods = false; TraceTestMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, mgr, tables) { } uint8_t airtimeFactor(const mesh::Packet* packet) const { return getDirectRetryPacketAirtimeFactor(packet); } uint8_t floodPathGate(const mesh::Packet* packet, uint8_t general_gate, uint8_t group_data_gate) const { return applyGroupDataFloodRetryPathGate(packet, general_gate, group_data_gate); } uint8_t floodAttemptLimit(const mesh::Packet* packet, uint8_t role_max_attempts) const { return applyFloodRetryAttemptPolicy(packet, role_max_attempts); } uint32_t floodAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx = 0) { return getFloodRetryAttemptDelay(packet, attempt_idx); } uint32_t otaRelayDelay(const mesh::Packet* packet) { return getOtaRetransmitDelay(packet); } int receiveDelay(const mesh::Packet* packet, float score, uint32_t air_time) { return calcRxDelayForPacket(packet, score, air_time); } uint32_t cadRetryDelay() const { return getCADFailRetryDelay(); } void completePacketSend(mesh::Packet* packet) { onSendComplete(packet); } void trackMessageRetry(const mesh::Packet* packet, const uint8_t message_key[MAX_HASH_SIZE], uint32_t message_timestamp) { replaceActiveMessageRetries(packet, message_key, message_timestamp); } mesh::DispatcherAction receivePacket(mesh::Packet* packet) { return onRecvPacket(packet); } mesh::DispatcherAction routePacket(mesh::Packet* packet) { return routeRecvPacket(packet); } bool allowPacketForward(const mesh::Packet*) override { return forwardFloods; } bool filterRecvFloodPacket(mesh::Packet*) override { return rejectFloods; } bool isTempRadioActive() const override { return tempRadioActive; } bool canTransmit(const mesh::Packet* packet) const { return allowPacketTransmit(packet); } bool allowFloodRetry(const mesh::Packet*) const override { return floodRetriesAllowed; } void onGroupPacketRecv(mesh::Packet*) override { groupPacketObserved = true; } }; static mesh::Packet makeFloodPacket(uint8_t payload_type) { mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD | (payload_type << PH_TYPE_SHIFT); packet.setPathHashSizeAndCount(1, 0); packet.payload_len = 1; packet.payload[0] = 0x42; return packet; } class RetryCodingRateRadio : public TraceTestRadio { public: uint8_t cr = 5; std::vector transmitted_crs; bool setCodingRate(uint8_t value) override { cr = value; return true; } bool startSendRaw(const uint8_t* bytes, int length) override { transmitted_crs.push_back(cr); return TraceTestRadio::startSendRaw(bytes, length); } }; class RetryCodingRateMesh : public TraceTestMesh { public: using TraceTestMesh::TraceTestMesh; uint8_t base_cr = 5; uint8_t flood_attempts = 15; uint8_t direct_attempts = 15; uint8_t getDefaultTxCodingRate() const override { return base_cr; } uint8_t getFloodRetryMaxAttempts(const mesh::Packet*) const override { return flood_attempts; } uint8_t getDirectRetryMaxAttempts(const mesh::Packet*) const override { return direct_attempts; } bool allowDirectRetry(const mesh::Packet*, const uint8_t*, uint8_t) const override { return true; } uint8_t floodCR(const mesh::Packet& packet, uint8_t attempt) { mesh::Packet retry = packet; configureFloodRetryPacket(&retry, &packet, attempt); return retry.tx_cr; } uint8_t directCR(uint8_t attempt) { mesh::Packet original, retry; configureDirectRetryPacket(&retry, &original, attempt); return retry.tx_cr; } void disableFloodRetries() { flood_attempts = 0; floodRetriesAllowed = false; cancelAllFloodRetries(); } }; class RetryCodingRateTest : public testing::Test { protected: TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; RetryCodingRateRadio radio; TraceTestTables tables; StaticPoolPacketManager manager{12}; RetryCodingRateMesh node{radio, clock, rng, rtc, manager, tables}; void SetUp() override { node.begin(); } void transmitNext() { clock.now += 10000; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; ++clock.now; node.loop(); EXPECT_FALSE(radio.sending); EXPECT_EQ(node.base_cr, radio.cr); // the next packet and RX use the normal CR } void queueFlood(bool scoped = false) { auto* packet = manager.allocNew(); ASSERT_NE(nullptr, packet); *packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT); if (scoped) { uint16_t codes[] = {0x1234, 0x5678}; ASSERT_TRUE(node.sendFlood(packet, codes, 0, 3)); } else { ASSERT_TRUE(node.sendFlood(packet)); } } }; TEST_F(RetryCodingRateTest, HopZeroMatchesDirectLadderFromEveryRadioCR) { for (uint8_t cr : {4, 5, 6, 7, 8}) { node.base_cr = cr; for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) { auto packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT); packet.header = route | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); packet.tx_cr = 8; // a previous retry must not advance the ladder twice for (uint8_t hash_size : {1, 2, 3}) { packet.setPathHashSizeAndCount(hash_size, 0); for (uint8_t attempt = 1; attempt <= 15; ++attempt) { SCOPED_TRACE(testing::Message() << "CR" << int(cr) << " retry " << int(attempt)); EXPECT_EQ(node.directCR(attempt), node.floodCR(packet, attempt)); } } } } } TEST_F(RetryCodingRateTest, ForwardedFloodsKeepActiveCRAtEveryAttempt) { auto packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT); packet.tx_cr = 8; for (uint8_t cr : {5, 6, 7, 8}) { node.base_cr = cr; for (uint8_t hash_size : {1, 2, 3}) { for (uint8_t hops : {1, 2, 8}) { packet.setPathHashSizeAndCount(hash_size, hops); for (uint8_t attempt = 1; attempt <= 15; ++attempt) { EXPECT_EQ(cr, node.floodCR(packet, attempt)); } } } } } TEST_F(RetryCodingRateTest, HopZeroCR5ScheduleReachesTheRadioForEachPresetBudget) { // infra, rooftop, mobile hop-zero budgets; the production preset/role // calculations are exercised separately by test_retry_cr_presets.py. for (uint8_t attempts : {2, 6, 15}) { for (bool scoped : {false, true}) { node.begin(); node.flood_attempts = attempts; radio.transmitted_crs.clear(); queueFlood(scoped); std::vector expected{5}; // initial transmission for (uint8_t i = 0; i <= attempts; ++i) { if (i > 0) expected.push_back(i == 1 ? 5 : i <= 3 ? 7 : 8); transmitNext(); } EXPECT_EQ(expected, radio.transmitted_crs); EXPECT_EQ(0, manager.getOutboundTotal()); } } } TEST_F(RetryCodingRateTest, DisabledFloodRetrySendsOnlyTheInitialPacket) { node.disableFloodRetries(); queueFlood(); transmitNext(); EXPECT_EQ((std::vector{5}), radio.transmitted_crs); EXPECT_EQ(0, manager.getOutboundTotal()); } TEST_F(RetryCodingRateTest, DisablingFloodRetryMidSequenceCancelsEscalatedCopies) { queueFlood(); transmitNext(); transmitNext(); transmitNext(); ASSERT_EQ(1, manager.getOutboundTotal()); EXPECT_EQ(7, manager.getOutboundByIdx(0)->tx_cr); node.disableFloodRetries(); clock.now += 10000; node.loop(); EXPECT_FALSE(radio.sending); EXPECT_EQ(0, manager.getOutboundTotal()); EXPECT_EQ((std::vector{5, 5, 7}), radio.transmitted_crs); } TEST_F(RetryCodingRateTest, DirectTransmissionsKeepTheirScheduleWithFloodRetryOff) { for (uint8_t attempts : {4, 15}) { for (bool flood_enabled : {true, false}) { node.begin(); node.direct_attempts = attempts; node.floodRetriesAllowed = flood_enabled; node.flood_attempts = flood_enabled ? 15 : 0; radio.transmitted_crs.clear(); auto* packet = node.obtainNewPacket(); ASSERT_NE(nullptr, packet); packet->header = PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT; packet->payload_len = 4; memset(packet->payload, 0x34, packet->payload_len); const uint8_t path[] = {0x12, 0x34}; ASSERT_TRUE(node.sendDirect(packet, path, sizeof(path))); std::vector expected{5}; for (uint8_t i = 0; i <= attempts; ++i) { if (i > 0) expected.push_back(i == 1 ? 5 : i <= 3 ? 7 : 8); transmitNext(); } EXPECT_EQ(expected, radio.transmitted_crs); EXPECT_EQ(0, manager.getOutboundTotal()); } } } static mesh::Packet makeOtaManifestFragment(uint8_t format_version) { mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA); packet.payload_len = 12; packet.payload[0] = mesh::ota::OTA_MANIFEST; packet.payload[1] = 0x11; packet.payload[2] = 0x22; packet.payload[3] = 0x33; packet.payload[4] = 0x44; packet.payload[5] = 0; // fragment index packet.payload[6] = 2; // fragment count memcpy(packet.payload + 7, mesh::ota::MOTA_MAGIC, sizeof(mesh::ota::MOTA_MAGIC)); packet.payload[11] = format_version; return packet; } TEST(RepeaterTransport, UnknownFloodPayloadIsRelayedWhenForwardingAllowsIt) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; mesh::Packet packet = makeFloodPacket(0x0D); // deliberately unassigned payload type mesh::DispatcherAction action = node.receivePacket(&packet); EXPECT_NE(ACTION_RELEASE, action); EXPECT_EQ(1, packet.getPathHashCount()); EXPECT_EQ(1, tables.mark_seen_calls); } TEST(RepeaterTransport, UnknownFloodPayloadHonorsReceiveAndForwardingRejections) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet filtered = makeFloodPacket(0x0D); node.forwardFloods = true; node.rejectFloods = true; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&filtered)); EXPECT_EQ(0, tables.mark_seen_calls); mesh::Packet forwarding_disabled = makeFloodPacket(0x0D); node.rejectFloods = false; node.forwardFloods = false; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&forwarding_disabled)); EXPECT_EQ(1, tables.mark_seen_calls); } TEST(RepeaterTransport, OtaDiscoveryRelaysInBackgroundOnlyDuringTempRadio) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA); packet.payload[0] = mesh::ota::OTA_ADV; EXPECT_FALSE(node.canTransmit(&packet)); EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet)); EXPECT_EQ(0, tables.mark_seen_calls); EXPECT_EQ(0, packet.getPathHashCount()); node.tempRadioActive = true; EXPECT_TRUE(node.canTransmit(&packet)); mesh::DispatcherAction action = node.receivePacket(&packet); EXPECT_NE(ACTION_RELEASE, action); EXPECT_EQ(OTA_TX_PRIORITY, (action >> 24) - 1); EXPECT_EQ(1, tables.mark_seen_calls); EXPECT_EQ(1, packet.getPathHashCount()); node.tempRadioActive = false; EXPECT_FALSE(node.canTransmit(&packet)); // queued-near-expiry packets cannot leak onto the normal channel } TEST(RepeaterTransport, AppV2AndBootV3ShareOta0cTempRadioRelayPolicy) { // The package format is metadata inside an OTA_MANIFEST fragment. It must // never select a second mesh payload type or bypass the repeater default // `flood.filter.1 0x0C all suspend=tempradio` policy. ASSERT_EQ(0x0C, PAYLOAD_TYPE_OTA); const uint8_t formats[] = { mesh::ota::MOTA_APP_FORMAT_VER, mesh::ota::MOTA_BOOT_FORMAT_VER, }; for (uint8_t format : formats) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; mesh::Packet packet = makeOtaManifestFragment(format); ASSERT_EQ(PAYLOAD_TYPE_OTA, packet.getPayloadType()); ASSERT_EQ(format, packet.payload[11]); EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet)); EXPECT_EQ(0, tables.mark_seen_calls); node.tempRadioActive = true; mesh::DispatcherAction action = node.receivePacket(&packet); EXPECT_NE(ACTION_RELEASE, action); EXPECT_EQ(OTA_TRANSFER_TX_PRIORITY, (action >> 24) - 1); EXPECT_EQ(1, tables.mark_seen_calls); EXPECT_EQ(1, packet.getPathHashCount()); } } TEST(RepeaterTransport, OtaTransferRelaysAsPrimaryTrafficWithoutOtaManager) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager(OTA_FWD_MIN_FREE); // exactly at the discovery-shedding threshold TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; node.tempRadioActive = true; mesh::Packet request = makeFloodPacket(PAYLOAD_TYPE_OTA); request.payload[0] = mesh::ota::OTA_REQ; ASSERT_EQ(OTA_FWD_MIN_FREE, manager.getFreeCount()); mesh::DispatcherAction action = node.receivePacket(&request); EXPECT_NE(ACTION_RELEASE, action); EXPECT_EQ(OTA_TRANSFER_TX_PRIORITY, (action >> 24) - 1); EXPECT_EQ(1, request.getPathHashCount()); } TEST(RepeaterTransport, OtaDiscoveryRelayKeepsCollisionJitter) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; // fixed 10 ms packet airtime ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA); packet.payload[0] = mesh::ota::OTA_ADV; rng.value = 0; EXPECT_EQ(node.otaRelayDelay(&packet), 3u); // ceil(0.25 * 10 ms) rng.value = 2; // selects the last value in [3, 5] EXPECT_EQ(node.otaRelayDelay(&packet), 5u); // 0.5 * 10 ms } TEST(RepeaterTransport, OtaTransferRelayKeepsConfiguredCollisionJitter) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; // fixed 10 ms packet airtime ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.tempRadioActive = true; node.forwardFloods = true; auto make_request = []() { mesh::Packet request = makeFloodPacket(PAYLOAD_TYPE_OTA); request.payload_len = 9; request.payload[0] = 0x06; // OTA_REQ for (uint8_t i = 1; i < request.payload_len; i++) request.payload[i] = i; return request; }; mesh::Packet request = make_request(); node.receivePacket(&request); // first request establishes the key rng.value = 3; EXPECT_EQ(node.otaRelayDelay(&request), 15u); for (uint32_t retry = 1; retry <= 3; retry++) { clock.now = retry * 3000; request = make_request(); // a fresh origin copy has the same zero-hop request key node.receivePacket(&request); // frequent repeats raise one level each } rng.value = 3; EXPECT_EQ(node.otaRelayDelay(&request), 15u); // request pressure does not widen primary traffic clock.now = 100000; rng.value = 1; EXPECT_EQ(node.otaRelayDelay(&request), 5u); } TEST(RepeaterTransport, TempRadioOtaBypassesReceiveHoldoffAndUsesFastCadRetry) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; // fixed 10 ms full-packet airtime ForwardingTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA); packet.payload[0] = mesh::ota::OTA_DATA; EXPECT_GT(node.receiveDelay(&packet, 0.0f, 10), 0); EXPECT_EQ(node.cadRetryDelay(), 120u); node.tempRadioActive = true; EXPECT_EQ(node.receiveDelay(&packet, 0.0f, 10), 0); EXPECT_EQ(node.cadRetryDelay(), 5u); } TEST(RepeaterTransport, OpaqueKnownFloodPayloadsAreRelayed) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; StaticPoolPacketManager manager(12); { ForwardingTestTables tables; TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; mesh::Packet custom = makeFloodPacket(PAYLOAD_TYPE_RAW_CUSTOM); EXPECT_NE(ACTION_RELEASE, node.receivePacket(&custom)); EXPECT_EQ(1, custom.getPathHashCount()); } { ForwardingTestTables tables; TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.forwardFloods = true; mesh::Packet multipart = makeFloodPacket(PAYLOAD_TYPE_MULTIPART); multipart.payload_len = 3; multipart.payload[0] = PAYLOAD_TYPE_TXT_MSG; EXPECT_NE(ACTION_RELEASE, node.receivePacket(&multipart)); EXPECT_EQ(1, multipart.getPathHashCount()); } } TEST(RTCClock, UniqueSequenceCanFollowAnIntentionalBackwardCorrection) { TraceTestRTC rtc; rtc.now = 100; EXPECT_EQ(100U, rtc.getCurrentTimeUnique()); EXPECT_EQ(101U, rtc.getCurrentTimeUnique()); rtc.setCurrentTime(50); rtc.resetUniqueTime(50); EXPECT_EQ(50U, rtc.getCurrentTimeUnique()); } TEST(ClockSyncConsensus, EightVsEightSplitDoesNotChooseTheUpperMedian) { uint32_t values[16]; for (int i = 0; i < 8; i++) values[i] = 1000; for (int i = 8; i < 16; i++) values[i] = 5000; mesh::ClockSyncConsensusResult result = mesh::evaluateClockSyncConsensus(values, 16, 9, 600); EXPECT_FALSE(result.consensus); EXPECT_EQ(16, result.fresh_count); EXPECT_EQ(8, result.agreeing_count); EXPECT_EQ(9, result.required_count); } TEST(ClockSyncConsensus, NineVsSevenStrictMajorityIsAccepted) { uint32_t values[16]; for (int i = 0; i < 7; i++) values[i] = 1000; for (int i = 7; i < 16; i++) values[i] = 5000; mesh::ClockSyncConsensusResult result = mesh::evaluateClockSyncConsensus(values, 16, 9, 600); EXPECT_TRUE(result.consensus); EXPECT_EQ(5000U, result.estimate); EXPECT_EQ(9, result.agreeing_count); EXPECT_EQ(9, result.required_count); } TEST(ClockSyncConsensus, ConfiguredEightStillCannotAcceptAnEightVsEightSplit) { uint32_t values[16]; for (int i = 0; i < 8; i++) values[i] = 1000; for (int i = 8; i < 16; i++) values[i] = 5000; mesh::ClockSyncConsensusResult result = mesh::evaluateClockSyncConsensus(values, 16, 8, 600); EXPECT_FALSE(result.consensus); EXPECT_EQ(9, result.required_count); } TEST(ClockSyncPathPolicy, NormalModeRequiresUniquePaths) { EXPECT_TRUE(mesh::clockSyncRequiresUniquePath(false)); } TEST(ClockSyncPathPolicy, EdgeModeAllowsOnePath) { EXPECT_FALSE(mesh::clockSyncRequiresUniquePath(true)); } TEST(ClockSyncDefaults, DriftCorrectionThresholdIsTenMinutes) { EXPECT_EQ(600U, mesh::CLOCK_SYNC_DRIFT_DEFAULT_SECONDS); } TEST(MeshReceiveHooks, GroupPacketIsObservedWhenForwardingIsDisabled) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); packet.payload_len = 1 + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE; memset(packet.payload, 0, packet.payload_len); ASSERT_FALSE(node.forwardFloods); node.receivePacket(&packet); EXPECT_TRUE(node.groupPacketObserved); } 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); EXPECT_NE(packet, nullptr); if (packet == nullptr) return nullptr; EXPECT_TRUE(node.sendDirect(packet, route, route_len)); return packet; } static mesh::Packet* makeDirectText(TraceTestMesh& node, uint8_t payload_marker, const uint8_t* route, uint8_t route_len) { mesh::Packet* packet = node.obtainNewPacket(); EXPECT_NE(packet, nullptr); if (packet == nullptr) return nullptr; packet->header = PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT; packet->payload_len = 3; packet->payload[0] = 0xA1; packet->payload[1] = 0xB2; packet->payload[2] = payload_marker; EXPECT_TRUE(node.sendDirect(packet, route, route_len)); return packet; } static mesh::Packet* makeFloodText(TraceTestMesh& node, uint8_t payload_marker) { mesh::Packet* packet = node.obtainNewPacket(); EXPECT_NE(packet, nullptr); if (packet == nullptr) return nullptr; packet->header = PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT; packet->payload_len = 3; packet->payload[0] = 0xA1; packet->payload[1] = 0xB2; packet->payload[2] = payload_marker; EXPECT_TRUE(node.sendFlood(packet)); return packet; } static void finishCurrentSend(TraceTestMesh& node, TraceTestClock& clock, TraceTestRadio& radio) { clock.now++; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now++; node.loop(); ASSERT_FALSE(radio.sending); } static void initSelfAdvert(TraceTestMesh& node, mesh::Packet* packet, uint8_t marker) { ASSERT_NE(packet, nullptr); if (packet == nullptr) return; packet->header = PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT; packet->payload_len = PUB_KEY_SIZE + sizeof(uint32_t) + SIGNATURE_SIZE; memset(packet->payload, 0, packet->payload_len); memcpy(packet->payload, node.self_id.pub_key, PUB_KEY_SIZE); packet->payload[packet->payload_len - 1] = marker; } TEST(TraceRetry, TraceAndAnonymousRequestsUseThreeAirtimes) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet trace; trace.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); mesh::Packet anon; anon.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ANON_REQ << PH_TYPE_SHIFT); mesh::Packet text; text.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT); mesh::Packet other; other.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_REQ << PH_TYPE_SHIFT); EXPECT_EQ(3, node.airtimeFactor(&trace)); EXPECT_EQ(3, node.airtimeFactor(&anon)); EXPECT_EQ(7, node.airtimeFactor(&text)); EXPECT_EQ(6, node.airtimeFactor(&other)); } TEST(FloodRetry, GroupDataUsesTheStricterPathGate) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet group_data; group_data.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT); mesh::Packet group_text; group_text.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); EXPECT_EQ(1, node.floodPathGate(&group_data, 2, 1)); EXPECT_EQ(1, node.floodPathGate(&group_data, FLOOD_RETRY_PATH_GATE_DISABLED, 1)); EXPECT_EQ(1, node.floodPathGate(&group_data, 1, 3)); EXPECT_EQ(2, node.floodPathGate(&group_data, 2, FLOOD_RETRY_PATH_GATE_DISABLED)); EXPECT_EQ(0, node.floodPathGate(&group_data, 0, FLOOD_RETRY_PATH_GATE_DISABLED)); EXPECT_EQ(2, node.floodPathGate(&group_text, 2, 1)); } TEST(FloodRetry, PayloadAndPathPolicyCapsEveryFloodType) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); for (uint8_t type = 0; type <= PH_TYPE_MASK; type++) { SCOPED_TRACE(static_cast(type)); mesh::Packet packet; packet.header = ROUTE_TYPE_FLOOD | (type << PH_TYPE_SHIFT); packet.setPathHashSizeAndCount(1, 0); uint8_t origin_limit; if (type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_OTA) { origin_limit = 0; } else if (type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_ANON_REQ || type == PAYLOAD_TYPE_PATH) { origin_limit = 15; } else { origin_limit = 1; } EXPECT_EQ(origin_limit, node.floodAttemptLimit(&packet, 15)); EXPECT_EQ(0, node.floodAttemptLimit(&packet, 0)); packet.setPathHashSizeAndCount(1, 1); uint8_t transit_limit; if (type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_OTA) { transit_limit = 0; } else if (type == PAYLOAD_TYPE_GRP_TXT) { transit_limit = 15; } else if (type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_ANON_REQ || type == PAYLOAD_TYPE_PATH) { transit_limit = 2; } else { transit_limit = 1; } EXPECT_EQ(transit_limit, node.floodAttemptLimit(&packet, 15)); } } TEST(FloodRetry, PayloadPolicyOnlyCapsAndNeverRaisesRoleCount) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet login_response; login_response.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_RESPONSE << PH_TYPE_SHIFT); login_response.setPathHashSizeAndCount(1, 0); EXPECT_EQ(7, node.floodAttemptLimit(&login_response, 7)); EXPECT_EQ(15, node.floodAttemptLimit(&login_response, 255)); login_response.setPathHashSizeAndCount(1, 3); EXPECT_EQ(1, node.floodAttemptLimit(&login_response, 1)); EXPECT_EQ(2, node.floodAttemptLimit(&login_response, 7)); mesh::Packet group_text; group_text.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); group_text.setPathHashSizeAndCount(1, 3); EXPECT_EQ(7, node.floodAttemptLimit(&group_text, 7)); EXPECT_EQ(15, node.floodAttemptLimit(&group_text, 255)); } TEST(FloodRetry, OriginAdvertRetryHasAnExtraOneMinuteDelay) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); mesh::Packet origin_advert; initSelfAdvert(node, &origin_advert, 0x10); origin_advert.header |= ROUTE_TYPE_FLOOD; origin_advert.setPathHashSizeAndCount(1, 0); mesh::Packet forwarded_advert = origin_advert; forwarded_advert.setPathHashSizeAndCount(1, 1); mesh::Packet foreign_origin_advert = origin_advert; foreign_origin_advert.payload[0] ^= 0xFF; mesh::Packet origin_group_text; origin_group_text.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT); origin_group_text.setPathHashSizeAndCount(1, 0); uint32_t ordinary_delay = node.floodAttemptDelay(&origin_group_text); EXPECT_EQ(ordinary_delay, node.floodAttemptDelay(&forwarded_advert)); EXPECT_EQ(ordinary_delay, node.floodAttemptDelay(&foreign_origin_advert)); EXPECT_EQ(ordinary_delay + 60000UL, node.floodAttemptDelay(&origin_advert)); } TEST(FloodRetry, NewSelfAdvertReplacesTheOlderQueuedRetry) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); mesh::Packet* old_advert = manager.allocNew(); ASSERT_NE(old_advert, nullptr); initSelfAdvert(node, old_advert, 0x11); ASSERT_TRUE(node.sendFlood(old_advert)); ASSERT_EQ(1, manager.getOutboundTotal()); clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now = 2; node.loop(); ASSERT_EQ(1, manager.getOutboundTotal()); mesh::Packet* old_retry = manager.getOutboundByIdx(0); ASSERT_NE(old_retry, nullptr); EXPECT_NE(old_advert, old_retry); mesh::Packet* group_data = manager.allocNew(); ASSERT_NE(group_data, nullptr); group_data->header = PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT; group_data->payload_len = 1; group_data->payload[0] = 0x33; ASSERT_TRUE(node.sendFlood(group_data)); clock.now = 3; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now = 4; node.loop(); ASSERT_EQ(2, manager.getOutboundTotal()); mesh::Packet* group_retry = NULL; for (int i = 0; i < manager.getOutboundTotal(); i++) { mesh::Packet* queued = manager.getOutboundByIdx(i); if (queued != old_retry) group_retry = queued; } ASSERT_NE(group_retry, nullptr); mesh::Packet* new_advert = manager.allocNew(); ASSERT_NE(new_advert, nullptr); initSelfAdvert(node, new_advert, 0x22); ASSERT_TRUE(node.sendFlood(new_advert)); ASSERT_EQ(2, manager.getOutboundTotal()); bool found_new_advert = false; bool found_group_retry = false; for (int i = 0; i < manager.getOutboundTotal(); i++) { mesh::Packet* queued = manager.getOutboundByIdx(i); found_new_advert |= queued == new_advert; found_group_retry |= queued == group_retry; EXPECT_NE(old_retry, queued); } EXPECT_TRUE(found_new_advert); EXPECT_TRUE(found_group_retry); } TEST(FloodRetry, DisabledRetryIsRecheckedAfterInitialTxAndBeforeDelayedTx) { { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); mesh::Packet* packet = manager.allocNew(); ASSERT_NE(packet, nullptr); packet->header = PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT; packet->payload_len = 1; packet->payload[0] = 0x44; ASSERT_TRUE(node.sendFlood(packet)); node.floodRetriesAllowed = false; clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now = 2; node.loop(); EXPECT_EQ(0, manager.getOutboundTotal()); } { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); mesh::Packet* packet = manager.allocNew(); ASSERT_NE(packet, nullptr); packet->header = PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT; packet->payload_len = 1; packet->payload[0] = 0x55; ASSERT_TRUE(node.sendFlood(packet)); clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now = 2; node.loop(); ASSERT_EQ(1, manager.getOutboundTotal()); node.floodRetriesAllowed = false; clock.now = 1000; node.loop(); EXPECT_FALSE(radio.sending); EXPECT_EQ(0, manager.getOutboundTotal()); } } TEST(FloodRetry, RecentForwardedAdvertWithHeardEchoIsNotForwardedAgain) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); node.forwardFloods = true; rtc.now = 100000; clock.now = 1000; mesh::Packet forwarded; forwarded.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); forwarded.setPathHashSizeAndCount(1, 1); forwarded.path[0] = 0x42; forwarded.payload_len = PUB_KEY_SIZE + sizeof(uint32_t) + SIGNATURE_SIZE; memset(forwarded.payload, 0x5A, forwarded.payload_len); uint32_t emitted_timestamp = rtc.now - 60; memcpy(&forwarded.payload[PUB_KEY_SIZE], &emitted_timestamp, sizeof(emitted_timestamp)); node.completePacketSend(&forwarded); mesh::Packet echo = forwarded; echo.setPathHashSizeAndCount(1, 2); node.receivePacket(&echo); mesh::Packet repeated = forwarded; EXPECT_EQ(ACTION_RELEASE, node.routePacket(&repeated)); rtc.now = emitted_timestamp + (6UL * 60UL * 60UL); repeated = forwarded; EXPECT_NE(ACTION_RELEASE, node.routePacket(&repeated)); } TEST(FloodRetry, ForwardedAdvertEchoMayReturnThroughAnotherBranch) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); node.forwardFloods = true; rtc.now = 100000; mesh::Packet forwarded; forwarded.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); forwarded.setPathHashSizeAndCount(1, 1); forwarded.path[0] = 0x24; forwarded.payload_len = PUB_KEY_SIZE + sizeof(uint32_t) + SIGNATURE_SIZE; memset(forwarded.payload, 0xA5, forwarded.payload_len); uint32_t emitted_timestamp = rtc.now - 60; memcpy(&forwarded.payload[PUB_KEY_SIZE], &emitted_timestamp, sizeof(emitted_timestamp)); node.completePacketSend(&forwarded); mesh::Packet other_branch = forwarded; other_branch.setPathHashSizeAndCount(1, 2); other_branch.path[0] ^= 0xFF; node.receivePacket(&other_branch); mesh::Packet repeated = forwarded; EXPECT_EQ(ACTION_RELEASE, node.routePacket(&repeated)); } TEST(TraceRetry, NewTraceReplacesQueuedRetryButAdvancedOldTraceStillQueues) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); const uint8_t route[] = {0x11, 0x22, 0x33}; mesh::Packet* old_trace = makeTrace(node, 0x11111111, 0xAAAAAAAA, route, sizeof(route)); ASSERT_NE(old_trace, nullptr); ASSERT_EQ(1, manager.getOutboundTotal()); clock.now = 1; node.loop(); ASSERT_TRUE(radio.sending); radio.complete = true; clock.now = 2; node.loop(); ASSERT_EQ(1, manager.getOutboundTotal()); // old TRACE retry mesh::Packet* new_trace = makeTrace(node, 0x22222222, 0xBBBBBBBB, route, sizeof(route)); ASSERT_NE(new_trace, nullptr); ASSERT_EQ(1, manager.getOutboundTotal()); EXPECT_EQ(new_trace, manager.getOutboundByIdx(0)); // A packet from the older run that has already advanced is a different // retry stage. It must remain queueable instead of being treated as the // stale same-hop retry that the newer run replaced. mesh::Packet* returning_old = node.createTrace(0x11111111, 0xAAAAAAAA, 0); ASSERT_NE(returning_old, nullptr); memcpy(&returning_old->payload[returning_old->payload_len], route, sizeof(route)); returning_old->payload_len += sizeof(route); returning_old->header |= ROUTE_TYPE_DIRECT; returning_old->path_len = 1; returning_old->path[0] = 4; ASSERT_TRUE(node.sendPacket(returning_old, 1)); EXPECT_EQ(2, manager.getOutboundTotal()); } TEST(MessageRetry, DifferentTimestampReplacesQueuedDirectRetry) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); const uint8_t route[] = {0x11, 0x22}; const uint8_t message_key[MAX_HASH_SIZE] = { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80 }; mesh::Packet* old_message = makeDirectText(node, 0x01, route, sizeof(route)); ASSERT_NE(old_message, nullptr); node.trackMessageRetry(old_message, message_key, 100U); finishCurrentSend(node, clock, radio); ASSERT_EQ(1, manager.getOutboundTotal()); mesh::Packet* old_retry = manager.getOutboundByIdx(0); mesh::Packet* new_message = makeDirectText(node, 0x02, route, sizeof(route)); ASSERT_NE(new_message, nullptr); ASSERT_EQ(2, manager.getOutboundTotal()); node.trackMessageRetry(new_message, message_key, 101U); ASSERT_EQ(1, manager.getOutboundTotal()); EXPECT_EQ(new_message, manager.getOutboundByIdx(0)); EXPECT_NE(old_retry, manager.getOutboundByIdx(0)); finishCurrentSend(node, clock, radio); ASSERT_EQ(1, manager.getOutboundTotal()); EXPECT_NE(old_retry, manager.getOutboundByIdx(0)); } TEST(MessageRetry, SameTimestampKeepsExistingRetrySequence) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); const uint8_t route[] = {0x31, 0x32}; const uint8_t message_key[MAX_HASH_SIZE] = { 0x81, 0x71, 0x61, 0x51, 0x41, 0x31, 0x21, 0x11 }; mesh::Packet* old_message = makeDirectText(node, 0x11, route, sizeof(route)); ASSERT_NE(old_message, nullptr); node.trackMessageRetry(old_message, message_key, 200U); finishCurrentSend(node, clock, radio); ASSERT_EQ(1, manager.getOutboundTotal()); mesh::Packet* old_retry = manager.getOutboundByIdx(0); mesh::Packet* same_timestamp = makeDirectText(node, 0x12, route, sizeof(route)); ASSERT_NE(same_timestamp, nullptr); node.trackMessageRetry(same_timestamp, message_key, 200U); ASSERT_EQ(2, manager.getOutboundTotal()); bool found_old_retry = false; bool found_new_message = false; for (int i = 0; i < manager.getOutboundTotal(); i++) { found_old_retry |= manager.getOutboundByIdx(i) == old_retry; found_new_message |= manager.getOutboundByIdx(i) == same_timestamp; } EXPECT_TRUE(found_old_retry); EXPECT_TRUE(found_new_message); } TEST(MessageRetry, ReplacementWorksAcrossFloodAndDirectRoutes) { TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; TraceTestTables tables; StaticPoolPacketManager manager(12); TraceTestMesh node(radio, clock, rng, rtc, manager, tables); node.begin(); const uint8_t message_key[MAX_HASH_SIZE] = { 0x08, 0x18, 0x28, 0x38, 0x48, 0x58, 0x68, 0x78 }; mesh::Packet* old_flood = makeFloodText(node, 0x21); ASSERT_NE(old_flood, nullptr); node.trackMessageRetry(old_flood, message_key, 300U); finishCurrentSend(node, clock, radio); ASSERT_EQ(1, manager.getOutboundTotal()); mesh::Packet* old_retry = manager.getOutboundByIdx(0); const uint8_t route[] = {0x41, 0x42}; mesh::Packet* new_direct = makeDirectText(node, 0x22, route, sizeof(route)); ASSERT_NE(new_direct, nullptr); ASSERT_EQ(2, manager.getOutboundTotal()); node.trackMessageRetry(new_direct, message_key, 301U); ASSERT_EQ(1, manager.getOutboundTotal()); EXPECT_EQ(new_direct, manager.getOutboundByIdx(0)); EXPECT_NE(old_retry, manager.getOutboundByIdx(0)); } class AdvertLimitedTestMesh : public TraceTestMesh { public: mesh::StaticFloodAdvertLimiter<4> limiter; unsigned advert_callbacks = 0; unsigned forwarding_checks = 0; using TraceTestMesh::TraceTestMesh; mesh::FloodAdvertLimiter* getFloodAdvertLimiter() override { return &limiter; } bool allowPacketForward(const mesh::Packet* packet) override { ++forwarding_checks; return TraceTestMesh::allowPacketForward(packet); } void onAdvertRecv(mesh::Packet*, const mesh::Identity&, uint32_t, const uint8_t*, size_t) override { ++advert_callbacks; } }; class AdvertReceiveLimit : public ::testing::Test { protected: TraceTestClock clock; TraceTestRTC rtc; TraceTestRNG rng; TraceTestRadio radio; ForwardingTestTables tables; StaticPoolPacketManager manager{12}; AdvertLimitedTestMesh node{radio, clock, rng, rtc, manager, tables}; void SetUp() override { node.begin(); node.forwardFloods = true; node.floodRetriesAllowed = false; g_mock_ed25519_verify_result = true; g_mock_ed25519_verify_calls = 0; } void TearDown() override { g_mock_ed25519_verify_result = true; } mesh::Packet advert(unsigned seq, uint8_t hops = 8, uint8_t hash_size = 1) { mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_ADVERT); packet.payload_len = PUB_KEY_SIZE + 4 + SIGNATURE_SIZE; memset(packet.payload, 0, packet.payload_len); packet.payload[0] = 0xBA; memcpy(packet.payload + PUB_KEY_SIZE, &seq, 4); memset(packet.path, 0x45, sizeof(packet.path)); packet.setPathHashSizeAndCount(hash_size, hops); return packet; } mesh::DispatcherAction receive(unsigned seq, uint8_t hops = 8, bool seen = false) { tables.seen = seen; auto packet = advert(seq, hops); return node.receivePacket(&packet); } }; TEST_F(AdvertReceiveLimit, StopsOnlyForwardingAndKeepsLocalAdvertCallbacks) { EXPECT_NE(ACTION_RELEASE, receive(1)); EXPECT_NE(ACTION_RELEASE, receive(2)); EXPECT_EQ(ACTION_RELEASE, receive(3)); EXPECT_EQ(3U, node.advert_callbacks); EXPECT_EQ(2U, node.forwarding_checks); // before side-effectful rule counters tables.seen = false; auto message = makeFloodPacket(PAYLOAD_TYPE_RAW_CUSTOM); EXPECT_NE(ACTION_RELEASE, node.receivePacket(&message)); } TEST_F(AdvertReceiveLimit, CliListsTheActualReceiveLimitWithoutChangingForwarding) { char reply[160]; ASSERT_NE(ACTION_RELEASE, receive(1)); ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now)); EXPECT_STREQ("> no rate-limited adverts", reply); ASSERT_NE(ACTION_RELEASE, receive(2)); ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now)); EXPECT_NE(nullptr, strstr(reply, "BA0000000000 quota wait=10800s")); ASSERT_EQ(ACTION_RELEASE, receive(3)); ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert key 1", reply, clock.now)); EXPECT_NE(nullptr, strstr(reply, "sent=2/2 hops=8")); EXPECT_EQ(3U, node.advert_callbacks); EXPECT_EQ(2U, node.forwarding_checks); // An authenticated shorter duplicate changes the actual allowance and list. ASSERT_EQ(ACTION_RELEASE, receive(1, 1, true)); ASSERT_TRUE(mesh::cli::handleFloodAdvertGet(&node.limiter, "get flood.advert", reply, clock.now)); EXPECT_STREQ("> no rate-limited adverts", reply); EXPECT_NE(ACTION_RELEASE, receive(4)); } TEST_F(AdvertReceiveLimit, VerifiedShorterDuplicateRaisesAllowanceWithoutRelayingIt) { receive(1); receive(2); EXPECT_EQ(ACTION_RELEASE, receive(1, 1, true)); EXPECT_EQ(3U, g_mock_ed25519_verify_calls); EXPECT_EQ(2U, node.advert_callbacks); for (unsigned seq = 3; seq <= 10; ++seq) EXPECT_NE(ACTION_RELEASE, receive(seq)); EXPECT_EQ(ACTION_RELEASE, receive(11)); } TEST_F(AdvertReceiveLimit, ForgedShorterDuplicateCannotRaiseAllowance) { receive(1); receive(2); g_mock_ed25519_verify_result = false; EXPECT_EQ(ACTION_RELEASE, receive(1, 0, true)); g_mock_ed25519_verify_result = true; EXPECT_EQ(ACTION_RELEASE, receive(3)); EXPECT_EQ(3U, node.advert_callbacks); } TEST_F(AdvertReceiveLimit, InvalidSelfAndMalformedAdvertsCannotCreateAbuseHistory) { g_mock_ed25519_verify_result = false; for (unsigned seq = 0; seq < 20; ++seq) EXPECT_EQ(ACTION_RELEASE, receive(seq)); g_mock_ed25519_verify_result = true; auto self = advert(30); memcpy(self.payload, node.self_id.pub_key, PUB_KEY_SIZE); tables.seen = false; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&self)); auto malformed = advert(31); malformed.payload_len = PUB_KEY_SIZE; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&malformed)); EXPECT_NE(ACTION_RELEASE, receive(40)); EXPECT_NE(ACTION_RELEASE, receive(41)); EXPECT_EQ(2U, node.advert_callbacks); } TEST_F(AdvertReceiveLimit, AdvertDuplicatesStaySuppressedAfterGeneralSeenCacheEviction) { receive(1); for (unsigned i = 0; i < 300; ++i) EXPECT_EQ(ACTION_RELEASE, receive(1)); EXPECT_NE(ACTION_RELEASE, receive(2)); EXPECT_EQ(ACTION_RELEASE, receive(3)); } TEST_F(AdvertReceiveLimit, UnsignedTrailingDataCannotManufactureDistinctAdverts) { for (unsigned seq = 0; seq < 20; ++seq) { auto packet = advert(1); packet.payload_len += MAX_ADVERT_DATA_SIZE + 1; memset(packet.payload + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE, 0, MAX_ADVERT_DATA_SIZE + 1); packet.payload[packet.payload_len - 1] = seq; tables.seen = false; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet)); } EXPECT_EQ(0U, g_mock_ed25519_verify_calls); EXPECT_EQ(0U, node.advert_callbacks); EXPECT_NE(ACTION_RELEASE, receive(2)); EXPECT_NE(ACTION_RELEASE, receive(3)); // The maximum supported signed app data remains valid. auto largest = advert(4); largest.payload_len += MAX_ADVERT_DATA_SIZE; memset(largest.payload + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE, 0, MAX_ADVERT_DATA_SIZE); tables.seen = false; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&largest)); // quota, not parser rejection EXPECT_EQ(3U, node.advert_callbacks); } TEST_F(AdvertReceiveLimit, UsesHopCountNotPathBytesAndIgnoresRtcJumps) { for (uint8_t size = 1; size <= 3; ++size) { node.limiter.reset(); for (unsigned seq = 1; seq <= 3; ++seq) { auto packet = advert(seq, 8, size); tables.seen = false; rtc.now = seq == 2 ? UINT32_MAX : 1; auto action = node.receivePacket(&packet); if (seq <= 2) EXPECT_NE(ACTION_RELEASE, action); else EXPECT_EQ(ACTION_RELEASE, action); } } } TEST_F(AdvertReceiveLimit, DirectAdvertsAreNotFloodQuotaOrAbuseEvidence) { for (unsigned seq = 0; seq < 20; ++seq) { auto packet = advert(seq, 0); packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); tables.seen = false; EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet)); } EXPECT_NE(ACTION_RELEASE, receive(30)); EXPECT_NE(ACTION_RELEASE, receive(31)); } TEST_F(AdvertReceiveLimit, SuppressedTrafficStillEscalatesAndRemainsLocallyVisible) { for (unsigned seq = 1; seq <= 3; ++seq) receive(seq); clock.now = mesh::FloodAdvertLimiter::WINDOW_MS; for (unsigned seq = 4; seq <= 6; ++seq) receive(seq); auto packet = advert(7); ASSERT_TRUE(node.limiter.isBad(packet.payload, clock.now)); EXPECT_EQ(ACTION_RELEASE, receive(7)); EXPECT_EQ(7U, node.advert_callbacks); node.begin(); // reboot clears both abuse and ordinary quota history EXPECT_FALSE(node.limiter.isBad(packet.payload, clock.now)); EXPECT_NE(ACTION_RELEASE, receive(8)); EXPECT_NE(ACTION_RELEASE, receive(9)); } TEST_F(AdvertReceiveLimit, SeenVerifiedDuplicateRefreshesLastHeardWithoutSignatureWork) { for (unsigned source = 0; source < 4; ++source) { auto packet = advert(1); packet.payload[0] += source; tables.seen = false; clock.now = source; ASSERT_NE(ACTION_RELEASE, node.receivePacket(&packet)); } clock.now = 10; ASSERT_EQ(ACTION_RELEASE, receive(1, 8, true)); EXPECT_EQ(4U, g_mock_ed25519_verify_calls); auto new_source = advert(1); new_source.payload[0] += 4; tables.seen = false; clock.now = 11; EXPECT_NE(ACTION_RELEASE, node.receivePacket(&new_source)); auto oldest = advert(1); oldest.payload[0] += 1; uint8_t hash[MAX_HASH_SIZE]; oldest.calculatePacketHash(hash); EXPECT_EQ(mesh::FloodAdvertLimiter::Decision::Capacity, node.limiter.check(oldest.payload, hash, clock.now)); auto refreshed = advert(1); refreshed.calculatePacketHash(hash); EXPECT_EQ(mesh::FloodAdvertLimiter::Decision::Duplicate, node.limiter.check(refreshed.payload, hash, clock.now)); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } #include "../fixtures/radio_profiles/mesh_tests.h"