// SPDX-License-Identifier: GPL-2.0-only /* * Digital Voice Modem - Test Suite * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2026 Bryan Biedenkapp, N2PLL * */ #include #include "fne/FNETestHooks.h" #include "fne/HostFNE.h" #include "fne/network/callhandler/TagAnalogData.h" #include "fne/network/UnitRouteRegistry.h" #include "common/analog/AnalogDefines.h" #include "common/BitManipulation.h" #include "common/lookups/AffiliationLookup.h" #include "common/lookups/PeerListLookup.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- /** * @brief Test harness for FNE network tests. */ class FNEHarness { public: /** * @brief Initializes a new instance of the FNEHarness class. */ FNEHarness() : configFile("fne-test.yml"), host(configFile), radios("", 0U, true, false), talkgroups("", 0U, true, false), traffic(&host, "127.0.0.1", 62031U, 999999U, "test-password", "test-fne", false, false, false, false, true, true, true, true, true, 0U, false, true, true, 5U, 10U, 2U), metadata(&host, &traffic, "127.0.0.1", 62032U, 2U) { traffic.setLookups(&radios, nullptr, &talkgroups, nullptr, nullptr, nullptr); } /** * @brief Finalizes a instance of the FNEHarness class. */ ~FNEHarness() { FNETestHooks::clearPeerWriteCapture(traffic); FNETestHooks::clearPeers(traffic); } std::string configFile; HostFNE host; lookups::RadioIdLookup radios; lookups::TalkgroupRulesLookup talkgroups; network::TrafficNetwork traffic; network::MetadataNetwork metadata; }; TEST_CASE("FNE unit route registry timestamps learned routes", "[fne][routing][u2u][registry]") { network::UnitRouteRegistry registry(1000U); REQUIRE(registry.update(2001U, 1001U, 1001U, true, 100U)); CHECK(registry.resolve(2001U, 1099U).valid); CHECK_FALSE(registry.resolve(2001U, 1100U).valid); const std::vector expired = registry.expire(1100U); REQUIRE(expired.size() == 1U); CHECK(expired[0U].unitId == 2001U); CHECK_FALSE(registry.resolve(2001U, 1100U).valid); } TEST_CASE("FNE explicit unit registrations do not expire", "[fne][routing][u2u][registry]") { network::UnitRouteRegistry registry(1000U); REQUIRE(registry.update(2001U, 1001U, 1001U, false, 100U)); CHECK(registry.expire(100000U).empty()); const network::UnitRoute route = registry.resolve(2001U, 100000U); REQUIRE(route.valid); CHECK(route.peerId == 1001U); CHECK_FALSE(route.learned); } TEST_CASE("FNE unit route registry validates, refreshes, and moves learned routes", "[fne][routing][u2u][registry]") { network::UnitRouteRegistry registry(1000U); CHECK_FALSE(registry.update(0U, 1001U, 1001U, true, 100U)); CHECK_FALSE(registry.update(2001U, 0U, 1001U, true, 100U)); REQUIRE(registry.update(2001U, 1001U, 0U, true, 100U)); CHECK(registry.resolve(2001U, 100U).ssrc == 1001U); REQUIRE(registry.update(2001U, 1001U, 3001U, true, 900U)); CHECK(registry.resolve(2001U, 1899U).valid); CHECK_FALSE(registry.resolve(2001U, 1900U).valid); REQUIRE(registry.update(2001U, 1002U, 3002U, true, 2000U)); const network::UnitRoute moved = registry.resolve(2001U, 2000U); REQUIRE(moved.valid); CHECK(moved.peerId == 1002U); CHECK(moved.ssrc == 3002U); } TEST_CASE("FNE explicit unit routes resist traffic learning and support removal", "[fne][routing][u2u][registry]") { network::UnitRouteRegistry registry(1000U); REQUIRE(registry.update(2001U, 1001U, 3001U, false, 100U)); CHECK_FALSE(registry.update(2001U, 1002U, 3002U, true, 200U)); REQUIRE(registry.update(2001U, 1001U, 3003U, true, 300U)); const network::UnitRoute route = registry.resolve(2001U, 5000U); REQUIRE(route.valid); CHECK(route.peerId == 1001U); CHECK(route.ssrc == 3001U); CHECK_FALSE(route.learned); CHECK_FALSE(registry.erase(2001U, 1002U)); CHECK(registry.erase(2001U, 1001U)); CHECK_FALSE(registry.resolve(2001U, 5000U).valid); } TEST_CASE("FNE unit route snapshots replace only the selected peer", "[fne][routing][u2u][registry]") { network::UnitRouteRegistry registry(1000U); REQUIRE(registry.update(2001U, 1001U, 1001U, false, 100U)); REQUIRE(registry.update(2002U, 1001U, 1001U, false, 100U)); REQUIRE(registry.update(2003U, 1002U, 1002U, false, 100U)); registry.replacePeer(1001U, std::vector{ 2002U, 2004U, 0U }, 3001U, 200U); CHECK_FALSE(registry.resolve(2001U, 200U).valid); CHECK(registry.resolve(2002U, 200U).peerId == 1001U); CHECK(registry.resolve(2003U, 200U).peerId == 1002U); CHECK(registry.resolve(2004U, 200U).ssrc == 3001U); std::vector removed = registry.erasePeer(1001U); std::sort(removed.begin(), removed.end()); CHECK(removed == std::vector{ 2002U, 2004U }); CHECK(registry.resolve(2003U, 200U).valid); } TEST_CASE("FNE orders each digital protocol stream without blocking independent calls", "[fne][ordering]") { FNEHarness harness; FNETestHooks::startTrafficWorkerPool(harness.traffic); const std::vector protocols{ FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN }; for (const FNETestHooks::ParrotProtocol protocol : protocols) { std::mutex mutex; std::condition_variable condition; bool firstStarted = false; bool releaseFirst = false; bool secondStartedEarly = false; bool independentCompleted = false; uint32_t completed = 0U; std::vector streamOrder; FNETestHooks::setOrderedFrameHandler(harness.traffic, protocol, [&](const uint8_t*, uint32_t, uint32_t peerId, uint32_t, uint16_t, uint32_t streamId, bool) { std::unique_lock lock(mutex); if (peerId == 1002U && streamId == 3002U) { independentCompleted = true; completed++; condition.notify_all(); return; } if (streamOrder.empty()) { firstStarted = true; condition.notify_all(); condition.wait(lock, [&]() { return releaseFirst; }); streamOrder.push_back(1U); } else { secondStartedEarly = !releaseFirst; streamOrder.push_back(2U); } completed++; condition.notify_all(); }); const auto enqueueTime = std::chrono::steady_clock::now(); const bool firstQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1001U, 2001U, 3001U); bool firstRanPromptly = false; { std::unique_lock lock(mutex); firstRanPromptly = condition.wait_for(lock, std::chrono::milliseconds(250), [&]() { return firstStarted; }); } const auto firstDispatchDelay = std::chrono::steady_clock::now() - enqueueTime; const bool secondQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1001U, 2001U, 3001U); const bool independentQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1002U, 2002U, 3002U); bool independentRanWhileBlocked = false; { std::unique_lock lock(mutex); independentRanWhileBlocked = condition.wait_for(lock, std::chrono::seconds(2), [&]() { return independentCompleted; }); releaseFirst = true; condition.notify_all(); condition.wait_for(lock, std::chrono::seconds(2), [&]() { return completed == 3U; }); } FNETestHooks::clearOrderedFrameHandler(harness.traffic, protocol); CHECK(firstQueued); CHECK(secondQueued); CHECK(independentQueued); CHECK(firstRanPromptly); CHECK(firstDispatchDelay < std::chrono::milliseconds(250)); CHECK(independentRanWhileBlocked); CHECK_FALSE(secondStartedEarly); CHECK(completed == 3U); CHECK(streamOrder == std::vector{ 1U, 2U }); } FNETestHooks::stopTrafficWorkerPool(harness.traffic); } TEST_CASE("FNE ordered protocol queues preserve burst FIFO and allow stream reuse", "[fne][ordering][fifo]") { constexpr uint16_t BURST_LENGTH = 32U; FNEHarness harness; FNETestHooks::startTrafficWorkerPool(harness.traffic); const std::vector protocols{ FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN }; for (const FNETestHooks::ParrotProtocol protocol : protocols) { std::mutex mutex; std::condition_variable condition; std::vector sequences; FNETestHooks::setOrderedFrameHandler(harness.traffic, protocol, [&](const uint8_t*, uint32_t, uint32_t, uint32_t, uint16_t pktSeq, uint32_t, bool) { std::lock_guard lock(mutex); sequences.push_back(pktSeq); condition.notify_all(); }); for (uint16_t sequence = 1U; sequence <= BURST_LENGTH; sequence++) { REQUIRE(FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1101U, 2101U, 3101U, sequence)); } bool burstCompleted = false; { std::unique_lock lock(mutex); burstCompleted = condition.wait_for(lock, std::chrono::seconds(2), [&]() { return sequences.size() == BURST_LENGTH; }); } REQUIRE(burstCompleted); std::vector expected; for (uint16_t sequence = 1U; sequence <= BURST_LENGTH; sequence++) expected.push_back(sequence); CHECK(sequences == expected); // The first burst drains and removes the stream state. Reusing the exact // key must schedule a fresh task instead of leaving the stream stranded. REQUIRE(FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1101U, 2101U, 3101U, BURST_LENGTH + 1U)); bool reusedStreamCompleted = false; { std::unique_lock lock(mutex); reusedStreamCompleted = condition.wait_for(lock, std::chrono::milliseconds(250), [&]() { return sequences.size() == BURST_LENGTH + 1U; }); } CHECK(reusedStreamCompleted); REQUIRE(sequences.size() == BURST_LENGTH + 1U); CHECK(sequences.back() == BURST_LENGTH + 1U); FNETestHooks::clearOrderedFrameHandler(harness.traffic, protocol); } FNETestHooks::stopTrafficWorkerPool(harness.traffic); } TEST_CASE("FNE ordered protocol queues do not wait for or normalize packet sequences", "[fne][ordering][late-entry]") { FNEHarness harness; FNETestHooks::startTrafficWorkerPool(harness.traffic); const std::vector protocols{ FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN }; const std::vector arrivals{ 25U, 23U, 23U, 24U }; for (const FNETestHooks::ParrotProtocol protocol : protocols) { std::mutex mutex; std::condition_variable condition; std::vector delivered; FNETestHooks::setOrderedFrameHandler(harness.traffic, protocol, [&](const uint8_t*, uint32_t, uint32_t, uint32_t, uint16_t pktSeq, uint32_t, bool) { std::lock_guard lock(mutex); delivered.push_back(pktSeq); condition.notify_all(); }); for (const uint16_t sequence : arrivals) { REQUIRE(FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1151U, 2151U, 3151U, sequence)); } bool allDelivered = false; { std::unique_lock lock(mutex); allDelivered = condition.wait_for(lock, std::chrono::milliseconds(250), [&]() { return delivered.size() == arrivals.size(); }); } CHECK(allDelivered); CHECK(delivered == arrivals); FNETestHooks::clearOrderedFrameHandler(harness.traffic, protocol); } FNETestHooks::stopTrafficWorkerPool(harness.traffic); } TEST_CASE("FNE ordered protocol keys isolate every traffic origin dimension", "[fne][ordering][keys]") { FNEHarness harness; FNETestHooks::startTrafficWorkerPool(harness.traffic); const std::vector protocols{ FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN }; for (const FNETestHooks::ParrotProtocol protocol : protocols) { std::mutex mutex; std::condition_variable condition; bool blockedFrameStarted = false; bool releaseBlockedFrame = false; uint32_t independentCompleted = 0U; uint32_t totalCompleted = 0U; FNETestHooks::setOrderedFrameHandler(harness.traffic, protocol, [&](const uint8_t*, uint32_t, uint32_t, uint32_t, uint16_t pktSeq, uint32_t, bool) { std::unique_lock lock(mutex); if (pktSeq == 1U) { blockedFrameStarted = true; condition.notify_all(); condition.wait(lock, [&]() { return releaseBlockedFrame; }); } else { independentCompleted++; } totalCompleted++; condition.notify_all(); }); const bool blockedQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1201U, 2201U, 3201U, 1U, false); bool blockedRanPromptly = false; { std::unique_lock lock(mutex); blockedRanPromptly = condition.wait_for(lock, std::chrono::milliseconds(250), [&]() { return blockedFrameStarted; }); } // Each frame differs from the blocked key in exactly one field. const bool peerVariantQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1202U, 2201U, 3201U, 2U, false); const bool ssrcVariantQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1201U, 2202U, 3201U, 3U, false); const bool streamVariantQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1201U, 2201U, 3202U, 4U, false); const bool directionVariantQueued = FNETestHooks::enqueueOrderedFrame(harness.traffic, protocol, 1201U, 2201U, 3201U, 5U, true); bool keysRanIndependently = false; { std::unique_lock lock(mutex); keysRanIndependently = condition.wait_for(lock, std::chrono::seconds(2), [&]() { return independentCompleted == 4U; }); releaseBlockedFrame = true; condition.notify_all(); condition.wait_for(lock, std::chrono::seconds(2), [&]() { return totalCompleted == 5U; }); } CHECK(blockedQueued); CHECK(blockedRanPromptly); CHECK(peerVariantQueued); CHECK(ssrcVariantQueued); CHECK(streamVariantQueued); CHECK(directionVariantQueued); CHECK(keysRanIndependently); CHECK(totalCompleted == 5U); FNETestHooks::clearOrderedFrameHandler(harness.traffic, protocol); } FNETestHooks::stopTrafficWorkerPool(harness.traffic); } TEST_CASE("FNE protocol dispatch validates peers before ordered delivery", "[fne][ordering][dispatch]") { constexpr uint32_t AUTHORIZED_PEER = 1301U; constexpr uint32_t UNAUTHORIZED_PEER = 1302U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, AUTHORIZED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::startTrafficWorkerPool(harness.traffic); struct ProtocolCase { FNETestHooks::ParrotProtocol protocol; network::NET_SUBFUNC::ENUM subFunction; }; const std::vector protocols{ { FNETestHooks::ParrotProtocol::DMR, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, { FNETestHooks::ParrotProtocol::P25, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, { FNETestHooks::ParrotProtocol::P25P2, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_P25_P2 }, { FNETestHooks::ParrotProtocol::NXDN, network::NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN } }; for (const ProtocolCase& protocolCase : protocols) { std::mutex mutex; std::condition_variable condition; uint32_t deliveries = 0U; uint32_t deliveredPeerId = 0U; FNETestHooks::setOrderedFrameHandler(harness.traffic, protocolCase.protocol, [&](const uint8_t*, uint32_t, uint32_t peerId, uint32_t, uint16_t, uint32_t, bool) { std::lock_guard lock(mutex); deliveries++; deliveredPeerId = peerId; condition.notify_all(); }); const std::vector packet{ 0U }; FNETestHooks::dispatchTraffic(harness.traffic, network::NET_FUNC::PROTOCOL, protocolCase.subFunction, UNAUTHORIZED_PEER, packet); { std::unique_lock lock(mutex); CHECK_FALSE(condition.wait_for(lock, std::chrono::milliseconds(50), [&]() { return deliveries != 0U; })); } FNETestHooks::dispatchTraffic(harness.traffic, network::NET_FUNC::PROTOCOL, protocolCase.subFunction, AUTHORIZED_PEER, packet); bool authorizedDelivered = false; { std::unique_lock lock(mutex); authorizedDelivered = condition.wait_for(lock, std::chrono::milliseconds(250), [&]() { return deliveries == 1U; }); } CHECK(authorizedDelivered); CHECK(deliveries == 1U); CHECK(deliveredPeerId == AUTHORIZED_PEER); FNETestHooks::clearOrderedFrameHandler(harness.traffic, protocolCase.protocol); } FNETestHooks::stopTrafficWorkerPool(harness.traffic); } TEST_CASE("FNE parrot playback honors affiliated-required talkgroups", "[fne][parrot][affiliations]") { constexpr uint32_t PARROT_TG = 3100U; constexpr uint32_t OTHER_TG = 3200U; constexpr uint32_t AFFILIATED_PEER = 1001U; constexpr uint32_t UNAFFILIATED_PEER = 1002U; constexpr uint32_t OTHER_TG_PEER = 1003U; FNEHarness harness; harness.talkgroups.addEntry(PARROT_TG, 0U, true, true); FNETestHooks::addPeer(harness.traffic, AFFILIATED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, UNAFFILIATED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_TG_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addGroupAffiliation(harness.traffic, AFFILIATED_PEER, 2001U, PARROT_TG); FNETestHooks::addGroupAffiliation(harness.traffic, OTHER_TG_PEER, 2003U, OTHER_TG); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); std::vector destinations; FNETestHooks::capturePeerWrites(harness.traffic, destinations); FNETestHooks::playbackParrotFrame(harness.traffic, protocol, UNAFFILIATED_PEER, 2002U, PARROT_TG); REQUIRE(destinations == std::vector{ AFFILIATED_PEER }); } } TEST_CASE("FNE call handlers enforce group peer-routing precedence", "[fne][callhandler][routing]") { constexpr uint32_t TALK_GROUP = 3100U; constexpr uint32_t INCLUDED_PEER = 1001U; constexpr uint32_t OTHER_PEER = 1002U; constexpr uint32_t REPLICA_PEER = 1003U; constexpr uint32_t SYSVIEW_PEER = 1004U; FNEHarness harness; harness.talkgroups.addEntry(TALK_GROUP, 1U, true, true); lookups::TalkgroupRuleGroupVoice talkgroup = harness.talkgroups.find(TALK_GROUP, 1U); lookups::TalkgroupRuleConfig config = talkgroup.config(); config.inclusion({ INCLUDED_PEER }); config.exclusion({ INCLUDED_PEER }); talkgroup.config(config); harness.talkgroups.addEntry(talkgroup); FNETestHooks::addPeer(harness.traffic, INCLUDED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addGroupAffiliation(harness.traffic, INCLUDED_PEER, 2001U, TALK_GROUP); FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); network::FNEPeerConnection& replica = FNETestHooks::addPeer( harness.traffic, REPLICA_PEER, network::NET_STAT_RUNNING, true); replica.isReplica(true); network::FNEPeerConnection& sysview = FNETestHooks::addPeer( harness.traffic, SYSVIEW_PEER, network::NET_STAT_RUNNING, true); sysview.peerClass(network::PEER_CONN_CLASS_SYSVIEW); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, INCLUDED_PEER, TALK_GROUP)); CHECK_FALSE(FNETestHooks::isPeerPermitted( harness.traffic, protocol, OTHER_PEER, TALK_GROUP)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, REPLICA_PEER, TALK_GROUP)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, SYSVIEW_PEER, TALK_GROUP)); } } TEST_CASE("FNE call handlers apply affiliation routing exceptions", "[fne][callhandler][routing]") { constexpr uint32_t TALK_GROUP = 3100U; constexpr uint32_t NORMAL_PEER = 1001U; constexpr uint32_t CONSOLE_PEER = 1002U; constexpr uint32_t CONVENTIONAL_PEER = 1003U; constexpr uint32_t VOICE_CHANNEL_PEER = 1004U; constexpr uint32_t CONTROL_CHANNEL_PEER = 2004U; FNEHarness harness; harness.talkgroups.addEntry(TALK_GROUP, 1U, true, true); FNETestHooks::addPeer(harness.traffic, NORMAL_PEER, network::NET_STAT_RUNNING, true); network::FNEPeerConnection& console = FNETestHooks::addPeer( harness.traffic, CONSOLE_PEER, network::NET_STAT_RUNNING, true); console.peerClass(network::PEER_CONN_CLASS_CONSOLE); network::FNEPeerConnection& conventional = FNETestHooks::addPeer( harness.traffic, CONVENTIONAL_PEER, network::NET_STAT_RUNNING, true); conventional.peerClass(network::PEER_CONN_CLASS_STANDARD); conventional.isConventional(true); network::FNEPeerConnection& voiceChannel = FNETestHooks::addPeer( harness.traffic, VOICE_CHANNEL_PEER, network::NET_STAT_RUNNING, true); voiceChannel.ccPeerId(CONTROL_CHANNEL_PEER); FNETestHooks::addGroupAffiliation( harness.traffic, CONTROL_CHANNEL_PEER, 2001U, TALK_GROUP); FNETestHooks::allowConventionalAffiliationOverride(harness.traffic, true); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK_FALSE(FNETestHooks::isPeerPermitted( harness.traffic, protocol, NORMAL_PEER, TALK_GROUP)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, NORMAL_PEER, TALK_GROUP, true, true)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, CONSOLE_PEER, TALK_GROUP)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, CONVENTIONAL_PEER, TALK_GROUP)); CHECK(FNETestHooks::isPeerPermitted( harness.traffic, protocol, VOICE_CHANNEL_PEER, TALK_GROUP)); } } TEST_CASE("FNE call handlers reject globally disabled unit-to-unit traffic", "[fne][callhandler][routing][u2u]") { constexpr uint32_t PEER_ID = 1001U; constexpr uint32_t DESTINATION_ID = 2001U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); FNETestHooks::disallowU2U(harness.traffic, true); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK_FALSE(FNETestHooks::isPeerPermitted( harness.traffic, protocol, PEER_ID, DESTINATION_ID, false)); } } TEST_CASE("FNE call handlers enforce per-peer unit-to-unit authorization", "[fne][callhandler][routing][u2u]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t TARGET_PEER = 1002U; constexpr uint32_t DESTINATION_ID = 2001U; FNEHarness harness; lookups::PeerListLookup peers("", 0U, true, false); lookups::PeerId sourceEntry(SOURCE_PEER, "source", "", false); sourceEntry.allowUnitToUnit(false); peers.addEntry(SOURCE_PEER, sourceEntry); harness.traffic.setLookups(&harness.radios, nullptr, &harness.talkgroups, &peers, nullptr, nullptr); FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::registerP25Unit(harness.traffic, TARGET_PEER, DESTINATION_ID); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK_FALSE(FNETestHooks::isPeerPermitted( harness.traffic, protocol, SOURCE_PEER, DESTINATION_ID, false)); } } TEST_CASE("FNE private-call unknown destinations reject or flood by policy", "[fne][routing][u2u][policy]") { FNEHarness harness; FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "reject", true, 300000U); CHECK_FALSE(FNETestHooks::isUnitRouteAvailable(harness.traffic, 2001U)); CHECK_FALSE(FNETestHooks::isUnitRouteAvailable(harness.traffic, 0U)); FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "flood", true, 300000U); CHECK(FNETestHooks::isUnitRouteAvailable(harness.traffic, 2001U)); CHECK_FALSE(FNETestHooks::isUnitRouteAvailable(harness.traffic, 0U)); FNETestHooks::configurePrivateCallRouting(harness.traffic, false, "flood", true, 300000U); CHECK_FALSE(FNETestHooks::isUnitRouteAvailable(harness.traffic, 2001U)); } TEST_CASE("FNE private-call traffic learning honors policy and peer authorization", "[fne][routing][u2u][learning]") { constexpr uint32_t ALLOWED_PEER = 1001U; constexpr uint32_t DENIED_PEER = 1002U; constexpr uint32_t UNIT_ID = 2001U; FNEHarness harness; lookups::PeerListLookup peers("", 0U, true, false); lookups::PeerId allowed(ALLOWED_PEER, "allowed", "", false); allowed.allowUnitToUnit(true); peers.addEntry(ALLOWED_PEER, allowed); lookups::PeerId denied(DENIED_PEER, "denied", "", false); denied.allowUnitToUnit(false); peers.addEntry(DENIED_PEER, denied); harness.traffic.setLookups(&harness.radios, nullptr, &harness.talkgroups, &peers, nullptr, nullptr); FNETestHooks::registerP25Unit(harness.traffic, ALLOWED_PEER, 2999U); FNETestHooks::registerP25Unit(harness.traffic, DENIED_PEER, 2998U); FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "reject", false, 300000U); FNETestHooks::learnUnitRoute(harness.traffic, ALLOWED_PEER, UNIT_ID, 3001U); CHECK(FNETestHooks::findUnitRoute(harness.traffic, UNIT_ID) == 0U); FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "reject", true, 300000U); FNETestHooks::learnUnitRoute(harness.traffic, DENIED_PEER, UNIT_ID, 3002U); CHECK(FNETestHooks::findUnitRoute(harness.traffic, UNIT_ID) == 0U); FNETestHooks::learnUnitRoute(harness.traffic, ALLOWED_PEER, UNIT_ID, 3001U); CHECK(FNETestHooks::findUnitRoute(harness.traffic, UNIT_ID) == ALLOWED_PEER); CHECK(FNETestHooks::isUnitRouteAvailable(harness.traffic, UNIT_ID)); } TEST_CASE("FNE private-call registration snapshots and peer teardown clean routes", "[fne][routing][u2u][lifecycle]") { constexpr uint32_t PEER_ID = 1001U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); FNETestHooks::registerP25Unit(harness.traffic, PEER_ID, 2001U); REQUIRE(FNETestHooks::replaceUnitRegistrations( harness.traffic, PEER_ID, std::vector{ 2002U, 2003U }, 3001U)); CHECK(FNETestHooks::findUnitRoute(harness.traffic, 2001U) == 0U); CHECK(FNETestHooks::findUnitRoute(harness.traffic, 2002U) == PEER_ID); CHECK(FNETestHooks::findUnitRoute(harness.traffic, 2003U) == PEER_ID); FNETestHooks::teardownPeer(harness.traffic, PEER_ID); CHECK(FNETestHooks::findUnitRoute(harness.traffic, 2002U) == 0U); CHECK(FNETestHooks::findUnitRoute(harness.traffic, 2003U) == 0U); } TEST_CASE("FNE private-call grants target only the registered next hop", "[fne][callhandler][routing][u2u][grants]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t TARGET_PEER = 1002U; constexpr uint32_t OTHER_PEER = 1003U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::NXDN }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, TARGET_PEER, DESTINATION_ID); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); bool accepted = false; if (protocol == FNETestHooks::ParrotProtocol::DMR) { accepted = FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } else if (protocol == FNETestHooks::ParrotProtocol::P25) { accepted = FNETestHooks::processP25GrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } else { accepted = FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } REQUIRE(accepted); CHECK(writes == std::vector{ TARGET_PEER }); } } TEST_CASE("FNE private-call flood reaches every authorized transit peer", "[fne][callhandler][routing][u2u][flood]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t ALLOWED_PEER_A = 1002U; constexpr uint32_t ALLOWED_PEER_B = 1003U; constexpr uint32_t DENIED_PEER = 1004U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::NXDN }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); FNEHarness harness; lookups::PeerListLookup peers("", 0U, true, false); for (uint32_t peerId : { SOURCE_PEER, ALLOWED_PEER_A, ALLOWED_PEER_B }) { lookups::PeerId peer(peerId, "allowed", "", false); peer.allowUnitToUnit(true); peers.addEntry(peerId, peer); } lookups::PeerId denied(DENIED_PEER, "denied", "", false); denied.allowUnitToUnit(false); peers.addEntry(DENIED_PEER, denied); harness.traffic.setLookups(&harness.radios, nullptr, &harness.talkgroups, &peers, nullptr, nullptr); FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "flood", true, 300000U); FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, ALLOWED_PEER_A, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, ALLOWED_PEER_B, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, DENIED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::registerP25Unit(harness.traffic, ALLOWED_PEER_A, 2901U); FNETestHooks::registerP25Unit(harness.traffic, ALLOWED_PEER_B, 2902U); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); bool accepted = false; if (protocol == FNETestHooks::ParrotProtocol::DMR) { accepted = FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } else if (protocol == FNETestHooks::ParrotProtocol::P25) { accepted = FNETestHooks::processP25GrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } else { accepted = FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } REQUIRE(accepted); std::sort(writes.begin(), writes.end()); CHECK(writes == std::vector{ ALLOWED_PEER_A, ALLOWED_PEER_B }); } } TEST_CASE("FNE P25 Phase 2 private voice honors known routes and flood policy", "[fne][callhandler][routing][u2u][flood][p25p2]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t TARGET_PEER = 1002U; constexpr uint32_t OTHER_PEER = 1003U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; SECTION("known destination selects only its registered peer") { FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, TARGET_PEER, DESTINATION_ID); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processP25P2PrivateFrame( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, 201U)); CHECK(writes == std::vector{ TARGET_PEER }); } SECTION("unknown destination floods to authorized peers") { FNEHarness harness; FNETestHooks::configurePrivateCallRouting(harness.traffic, true, "flood", true, 300000U); FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processP25P2PrivateFrame( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, 202U)); std::sort(writes.begin(), writes.end()); CHECK(writes == std::vector{ TARGET_PEER, OTHER_PEER }); } } TEST_CASE("FNE private-call routing denies an unauthorized destination peer", "[fne][callhandler][routing][u2u][egress]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t TARGET_PEER = 1002U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; FNEHarness harness; lookups::PeerListLookup peers("", 0U, true, false); lookups::PeerId source(SOURCE_PEER, "source", "", false); source.allowUnitToUnit(true); peers.addEntry(SOURCE_PEER, source); lookups::PeerId target(TARGET_PEER, "target", "", false); target.allowUnitToUnit(false); peers.addEntry(TARGET_PEER, target); harness.traffic.setLookups(&harness.radios, nullptr, &harness.talkgroups, &peers, nullptr, nullptr); FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, TARGET_PEER, DESTINATION_ID); CHECK_FALSE(FNETestHooks::isUnitRouteAvailable(harness.traffic, DESTINATION_ID)); CHECK_FALSE(FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); CHECK_FALSE(FNETestHooks::processP25GrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); CHECK_FALSE(FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); } TEST_CASE("FNE P25 uses the explicit U2U flag for late-entry voice", "[fne][callhandler][routing][u2u]") { constexpr uint32_t PEER_ID = 1001U; constexpr uint32_t SOURCE_ID = 5519007U; constexpr uint32_t DESTINATION_ID = 5519001U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, PEER_ID, DESTINATION_ID); CHECK(FNETestHooks::validateP25IndividualCall( harness.traffic, PEER_ID, SOURCE_ID, DESTINATION_ID)); FNETestHooks::disallowU2U(harness.traffic, true); CHECK_FALSE(FNETestHooks::validateP25IndividualCall( harness.traffic, PEER_ID, SOURCE_ID, DESTINATION_ID)); } TEST_CASE("FNE DMR individual grant demand targets the called unit site", "[fne][callhandler][routing][u2u][dmr]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t SOURCE_SITE = 1002U; constexpr uint32_t TARGET_SITE = 1003U; constexpr uint32_t OTHER_SITE = 1004U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SOURCE_SITE, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_SITE, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_SITE, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, SOURCE_SITE, SOURCE_ID); FNETestHooks::registerP25Unit(harness.traffic, TARGET_SITE, DESTINATION_ID); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); CHECK(writes == std::vector{ TARGET_SITE }); } TEST_CASE("FNE DMR group grant demand retains group peer routing", "[fne][callhandler][routing][group][dmr]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t SITE_A = 1002U; constexpr uint32_t SITE_B = 1003U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t TALK_GROUP = 3100U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SITE_A, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SITE_B, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false); FNETestHooks::addGroupAffiliation(harness.traffic, SITE_A, SOURCE_ID, TALK_GROUP); FNETestHooks::addGroupAffiliation(harness.traffic, SITE_B, SOURCE_ID, TALK_GROUP); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false)); std::sort(writes.begin(), writes.end()); CHECK(writes == std::vector{ SITE_A, SITE_B }); } TEST_CASE("FNE NXDN individual grant demand targets the called unit site", "[fne][callhandler][routing][u2u][nxdn]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t SOURCE_SITE = 1002U; constexpr uint32_t TARGET_SITE = 1003U; constexpr uint32_t OTHER_SITE = 1004U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SOURCE_SITE, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, TARGET_SITE, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_SITE, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.radios.addEntry(DESTINATION_ID, true, "test destination"); FNETestHooks::registerP25Unit(harness.traffic, SOURCE_SITE, SOURCE_ID); FNETestHooks::registerP25Unit(harness.traffic, TARGET_SITE, DESTINATION_ID); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); CHECK(writes == std::vector{ TARGET_SITE }); } TEST_CASE("FNE NXDN group grant demand retains group peer routing", "[fne][callhandler][routing][group][nxdn]") { constexpr uint32_t SOURCE_PEER = 1001U; constexpr uint32_t SITE_A = 1002U; constexpr uint32_t SITE_B = 1003U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t TALK_GROUP = 3100U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SITE_A, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, SITE_B, network::NET_STAT_RUNNING, true); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false); FNETestHooks::addGroupAffiliation(harness.traffic, SITE_A, SOURCE_ID, TALK_GROUP); FNETestHooks::addGroupAffiliation(harness.traffic, SITE_B, SOURCE_ID, TALK_GROUP); std::vector writes; FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false)); std::sort(writes.begin(), writes.end()); CHECK(writes == std::vector{ SITE_A, SITE_B }); } TEST_CASE("FNE call handlers validate source radios and group state", "[fne][callhandler][validation]") { constexpr uint32_t PEER_ID = 1001U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t TALK_GROUP = 3100U; constexpr uint32_t INVALID_TALK_GROUP = 3200U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); std::vector rejectedWrites; FNETestHooks::capturePeerWrites(harness.traffic, rejectedWrites); harness.radios.addEntry(SOURCE_ID, true, "test source"); harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK(FNETestHooks::validateGroupCall( harness.traffic, protocol, PEER_ID, SOURCE_ID, TALK_GROUP)); CHECK_FALSE(FNETestHooks::validateGroupCall( harness.traffic, protocol, PEER_ID, SOURCE_ID, INVALID_TALK_GROUP)); } harness.talkgroups.addEntry(TALK_GROUP, 1U, false, false); harness.radios.addEntry(SOURCE_ID, false, "disabled source"); for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK_FALSE(FNETestHooks::validateGroupCall( harness.traffic, protocol, PEER_ID, SOURCE_ID, TALK_GROUP)); } } TEST_CASE("FNE analog handler preserves the individual-call flag", "[fne][callhandler][analog][u2u]") { constexpr uint32_t PEER_ID = 1001U; constexpr uint32_t SOURCE_ID = 2001U; constexpr uint32_t DESTINATION_ID = 2002U; constexpr uint32_t STREAM_ID = 100U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); FNETestHooks::disallowU2U(harness.traffic, true); std::vector frame(network::ANALOG_PACKET_LENGTH, 0U); SET_UINT24(SOURCE_ID, frame.data(), 5U); SET_UINT24(DESTINATION_ID, frame.data(), 8U); frame[15U] = 0x40U | analog::defines::AudioFrameType::VOICE; CHECK_FALSE(harness.traffic.analogTrafficHandler()->processFrame( frame.data(), (uint32_t)frame.size(), PEER_ID, PEER_ID, 1U, STREAM_ID)); } TEST_CASE("FNE call handlers reject truncated protocol frames", "[fne][callhandler][validation][security]") { constexpr uint32_t PEER_ID = 1001U; FNEHarness harness; FNETestHooks::addPeer(harness.traffic, PEER_ID, network::NET_STAT_RUNNING, true); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); CHECK_FALSE(FNETestHooks::processTruncatedFrame(harness.traffic, protocol, PEER_ID)); } } TEST_CASE("FNE parrot playback permits always-send and console peers", "[fne][parrot][routing]") { constexpr uint32_t PARROT_TG = 3100U; constexpr uint32_t ALWAYS_SEND_PEER = 1001U; constexpr uint32_t CONSOLE_PEER = 1002U; constexpr uint32_t UNAFFILIATED_PEER = 1003U; FNEHarness harness; harness.talkgroups.addEntry(PARROT_TG, 0U, true, true); lookups::TalkgroupRuleGroupVoice talkgroup = harness.talkgroups.find(PARROT_TG); lookups::TalkgroupRuleConfig config = talkgroup.config(); config.alwaysSend({ ALWAYS_SEND_PEER }); talkgroup.config(config); harness.talkgroups.addEntry(talkgroup); FNETestHooks::addPeer(harness.traffic, ALWAYS_SEND_PEER, network::NET_STAT_RUNNING, true); network::FNEPeerConnection& console = FNETestHooks::addPeer( harness.traffic, CONSOLE_PEER, network::NET_STAT_RUNNING, true); console.peerClass(network::PEER_CONN_CLASS_CONSOLE); FNETestHooks::addPeer(harness.traffic, UNAFFILIATED_PEER, network::NET_STAT_RUNNING, true); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); std::vector destinations; FNETestHooks::capturePeerWrites(harness.traffic, destinations); FNETestHooks::playbackParrotFrame(harness.traffic, protocol, UNAFFILIATED_PEER, 2003U, PARROT_TG); std::sort(destinations.begin(), destinations.end()); REQUIRE(destinations == std::vector{ ALWAYS_SEND_PEER, CONSOLE_PEER }); } } TEST_CASE("FNE parrot playback blocks excluded peers", "[fne][parrot][routing]") { constexpr uint32_t PARROT_TG = 3100U; constexpr uint32_t PERMITTED_PEER = 1001U; constexpr uint32_t EXCLUDED_PEER = 1002U; FNEHarness harness; harness.talkgroups.addEntry(PARROT_TG, 0U, true, false); lookups::TalkgroupRuleGroupVoice talkgroup = harness.talkgroups.find(PARROT_TG); lookups::TalkgroupRuleConfig config = talkgroup.config(); config.exclusion({ EXCLUDED_PEER }); config.alwaysSend({ EXCLUDED_PEER }); talkgroup.config(config); harness.talkgroups.addEntry(talkgroup); FNETestHooks::addPeer(harness.traffic, PERMITTED_PEER, network::NET_STAT_RUNNING, true); network::FNEPeerConnection& excluded = FNETestHooks::addPeer( harness.traffic, EXCLUDED_PEER, network::NET_STAT_RUNNING, true); excluded.peerClass(network::PEER_CONN_CLASS_CONSOLE); FNETestHooks::addGroupAffiliation(harness.traffic, PERMITTED_PEER, 2001U, PARROT_TG); FNETestHooks::addGroupAffiliation(harness.traffic, EXCLUDED_PEER, 2002U, PARROT_TG); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); std::vector destinations; FNETestHooks::capturePeerWrites(harness.traffic, destinations); FNETestHooks::playbackParrotFrame(harness.traffic, protocol, PERMITTED_PEER, 2001U, PARROT_TG); REQUIRE(destinations == std::vector{ PERMITTED_PEER }); } } TEST_CASE("FNE parrot playback honors peer inclusion lists", "[fne][parrot][routing]") { constexpr uint32_t PARROT_TG = 3100U; constexpr uint32_t INCLUDED_PEER = 1001U; constexpr uint32_t OTHER_PEER = 1002U; FNEHarness harness; harness.talkgroups.addEntry(PARROT_TG, 0U, true, false); lookups::TalkgroupRuleGroupVoice talkgroup = harness.talkgroups.find(PARROT_TG); lookups::TalkgroupRuleConfig config = talkgroup.config(); config.inclusion({ INCLUDED_PEER }); config.alwaysSend({ OTHER_PEER }); talkgroup.config(config); harness.talkgroups.addEntry(talkgroup); FNETestHooks::addPeer(harness.traffic, INCLUDED_PEER, network::NET_STAT_RUNNING, true); FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); const std::vector protocols = { FNETestHooks::ParrotProtocol::DMR, FNETestHooks::ParrotProtocol::P25, FNETestHooks::ParrotProtocol::P25P2, FNETestHooks::ParrotProtocol::NXDN, FNETestHooks::ParrotProtocol::ANALOG }; for (FNETestHooks::ParrotProtocol protocol : protocols) { CAPTURE(static_cast(protocol)); std::vector destinations; FNETestHooks::capturePeerWrites(harness.traffic, destinations); FNETestHooks::playbackParrotFrame(harness.traffic, protocol, OTHER_PEER, 2002U, PARROT_TG); REQUIRE(destinations == std::vector{ INCLUDED_PEER }); } } TEST_CASE("FNE P25 filtered terminators honor peer routing lists", "[fne][callhandler][p25][routing]") { constexpr uint32_t TALK_GROUP = 3100U; constexpr uint32_t INCLUDED_PEER = 1001U; constexpr uint32_t EXCLUDED_PEER = 1002U; constexpr uint32_t OTHER_PEER = 1003U; FNEHarness harness; harness.talkgroups.addEntry(TALK_GROUP, 0U, true, false); lookups::TalkgroupRuleGroupVoice talkgroup = harness.talkgroups.find(TALK_GROUP); lookups::TalkgroupRuleConfig config = talkgroup.config(); config.inclusion({ INCLUDED_PEER }); config.exclusion({ EXCLUDED_PEER }); talkgroup.config(config); harness.talkgroups.addEntry(talkgroup); FNETestHooks::filterTerminators(harness.traffic, true); CHECK(FNETestHooks::isP25TerminatorPeerPermitted( harness.traffic, INCLUDED_PEER, TALK_GROUP)); CHECK_FALSE(FNETestHooks::isP25TerminatorPeerPermitted( harness.traffic, EXCLUDED_PEER, TALK_GROUP)); CHECK_FALSE(FNETestHooks::isP25TerminatorPeerPermitted( harness.traffic, OTHER_PEER, TALK_GROUP)); } TEST_CASE("Affiliation snapshots replace prior state", "[fne][affiliations]") { lookups::AffiliationLookup affiliations("test-affiliations", nullptr, false); affiliations.groupAff(1001U, 3100U); affiliations.groupAff(1002U, 3200U); std::unordered_map replacement = { { 1002U, 3300U }, { 1003U, 3400U } }; affiliations.replaceGroupAffiliations(replacement); REQUIRE(affiliations.grpAffTable() == replacement); affiliations.replaceGroupAffiliations({}); REQUIRE(affiliations.grpAffTable().empty()); } TEST_CASE("Unit registration snapshots replace prior state", "[fne][registrations]") { lookups::AffiliationLookup registrations("test-registrations", nullptr, false); registrations.unitReg(1001U); registrations.unitReg(1002U); registrations.replaceUnitRegistrations({ 1002U, 1003U, 1003U }); REQUIRE(registrations.unitRegTable() == std::vector{ 1002U, 1003U }); registrations.replaceUnitRegistrations({}); REQUIRE(registrations.unitRegTable().empty()); } TEST_CASE("FNE test hooks expose peer connection state", "[fne][hooks]") { FNEHarness harness; FNETestHooks::addPeer(harness.traffic, 1001U, network::NET_STAT_WAITING_AUTHORISATION); REQUIRE(FNETestHooks::hasPeer(harness.traffic, 1001U)); REQUIRE(FNETestHooks::peerCount(harness.traffic) == 1U); REQUIRE(FNETestHooks::peerState(harness.traffic, 1001U) == network::NET_STAT_WAITING_AUTHORISATION); } TEST_CASE("FNE peer references remain valid after registry removal", "[fne][hooks][concurrency]") { FNEHarness harness; FNETestHooks::addPeer(harness.traffic, 1001U, network::NET_STAT_RUNNING, true); std::shared_ptr retained = FNETestHooks::retainPeer(harness.traffic, 1001U); REQUIRE(retained != nullptr); FNETestHooks::removePeer(harness.traffic, 1001U); REQUIRE_FALSE(FNETestHooks::hasPeer(harness.traffic, 1001U)); REQUIRE(FNETestHooks::peerCount(harness.traffic) == 0U); REQUIRE(retained->id() == 1001U); REQUIRE(retained->connectionState() == network::NET_STAT_RUNNING); } TEST_CASE("FNE replicated peer snapshots are stable copies", "[fne][replication]") { FNEHarness harness; json::array peers; json::object peer; uint32_t replicatedPeerId = 2001U; peer["peerId"].set(replicatedPeerId); peers.push_back(json::value(peer)); FNETestHooks::updateReplicatedPeers(harness.traffic, 1001U, peers); std::unordered_map snapshot = FNETestHooks::replicatedPeersSnapshot(harness.traffic); REQUIRE(snapshot.size() == 1U); REQUIRE(snapshot.at(1001U).size() == 1U); FNETestHooks::eraseReplicatedPeers(harness.traffic, 1001U); REQUIRE(FNETestHooks::replicatedPeersSnapshot(harness.traffic).empty()); REQUIRE(snapshot.at(1001U).size() == 1U); } TEST_CASE("FNE traffic dispatcher rejects truncated pre-authentication packets", "[fne][hooks][security]") { FNEHarness harness; FNETestHooks::addPeer(harness.traffic, 1001U, network::NET_STAT_WAITING_AUTHORISATION); REQUIRE_NOTHROW(FNETestHooks::dispatchTraffic(harness.traffic, network::NET_FUNC::RPTK, network::NET_SUBFUNC::NOP, 1001U, std::vector(8U))); REQUIRE(FNETestHooks::peerState(harness.traffic, 1001U) == network::NET_STAT_WAITING_AUTHORISATION); REQUIRE_NOTHROW(FNETestHooks::dispatchTraffic(harness.traffic, network::NET_FUNC::RPTC, network::NET_SUBFUNC::NOP, 1001U, std::vector(8U))); REQUIRE(FNETestHooks::peerState(harness.traffic, 1001U) == network::NET_STAT_WAITING_AUTHORISATION); } TEST_CASE("FNE metadata dispatcher rejects truncated log transfers", "[fne][hooks][security]") { FNEHarness harness; FNETestHooks::addPeer(harness.traffic, 1001U, network::NET_STAT_RUNNING, true); REQUIRE_NOTHROW(FNETestHooks::dispatchMetadata(harness.traffic, harness.metadata, network::NET_FUNC::TRANSFER, network::NET_SUBFUNC::TRANSFER_SUBFUNC_ACTIVITY, 1001U, std::vector(11U))); REQUIRE_NOTHROW(FNETestHooks::dispatchMetadata(harness.traffic, harness.metadata, network::NET_FUNC::TRANSFER, network::NET_SUBFUNC::TRANSFER_SUBFUNC_DIAG, 1001U, std::vector(10U))); REQUIRE(FNETestHooks::peerState(harness.traffic, 1001U) == network::NET_STAT_RUNNING); }