// 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 "fne/FNETestHooks.h" #include "fne/HostFNE.h" #include "fne/network/P25OTARService.h" #include "fne/network/callhandler/TagP25Data.h" #include "fne/network/callhandler/TagDMRData.h" #include "fne/network/callhandler/TagP25P2Data.h" #include "fne/network/callhandler/TagNXDNData.h" #include "fne/network/callhandler/TagAnalogData.h" #include "fne/lookups/AffiliationLookup.h" #include "common/p25/data/Assembler.h" #include "common/Utils.h" #include #include #include using namespace network; using namespace p25::defines; using namespace p25::data; // --------------------------------------------------------------------------- // Global Variables // --------------------------------------------------------------------------- bool g_promiscuousHub = false; // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /* Tests whether a P25 frame is telephone interconnect traffic. */ bool FNETestHooks::isP25TelIntTraffic(TrafficNetwork& network, p25::lc::LC& control, DUID::E duid, const p25::lc::TSBK* tsbk) { return network.m_tagP25->isTelIntTraffic(control, duid, tsbk); } /* Tests whether telephone interconnect traffic may be routed between two peers. */ bool FNETestHooks::isP25TelIntPeerPermitted(TrafficNetwork& network, uint32_t srcPeerId, uint32_t dstPeerId, uint32_t dstId) { return network.m_tagP25->isTelIntPeerPermitted(srcPeerId, dstPeerId, network.findPeer(dstPeerId), dstId); } /* Adds a unit registration owned by a site peer. */ void FNETestHooks::registerP25Unit(TrafficNetwork& network, uint32_t sitePeerId, uint32_t unitId) { std::shared_ptr affiliations = std::make_shared("test-p25-registrations", nullptr, false); affiliations->unitReg(unitId, sitePeerId); network.m_peerAffiliations[sitePeerId] = affiliations; } /* Adds a talkgroup affiliation owned by a site peer. */ void FNETestHooks::addGroupAffiliation(TrafficNetwork& network, uint32_t sitePeerId, uint32_t unitId, uint32_t talkgroupId) { std::shared_ptr affiliations = network.getPeerAffiliations(sitePeerId); if (affiliations == nullptr) { affiliations = std::make_shared( "test-parrot-affiliations", nullptr, false); network.m_peerAffiliations[sitePeerId] = affiliations; } affiliations->groupAff(unitId, talkgroupId); } /* Captures peer IDs selected by subsequent test writes. */ void FNETestHooks::capturePeerWrites(TrafficNetwork& network, std::vector& peerIds) { network.m_testPeerWriteHandler = [&peerIds](uint32_t peerId, FrameQueue::OpcodePair) { peerIds.push_back(peerId); }; } /* Clears the test peer-write capture callback. */ void FNETestHooks::clearPeerWriteCapture(TrafficNetwork& network) { network.m_testPeerWriteHandler = nullptr; } /* Queues and plays one synthetic parrot frame through a protocol handler. */ void FNETestHooks::playbackParrotFrame(TrafficNetwork& network, ParrotProtocol protocol, uint32_t sourcePeerId, uint32_t sourceId, uint32_t talkgroupId) { constexpr uint32_t FRAME_LENGTH = 16U; constexpr uint16_t PACKET_SEQUENCE = 1U; constexpr uint32_t STREAM_ID = 100U; switch (protocol) { case ParrotProtocol::DMR: { callhandler::TagDMRData::ParrotFrame frame{}; frame.buffer = new uint8_t[FRAME_LENGTH](); frame.bufferLen = FRAME_LENGTH; frame.slotNo = 1U; frame.pktSeq = PACKET_SEQUENCE; frame.streamId = STREAM_ID; frame.peerId = sourcePeerId; frame.srcId = sourceId; frame.dstId = talkgroupId; network.m_tagDMR->m_parrotFrames.push_back(frame); network.m_tagDMR->playbackParrot(); break; } case ParrotProtocol::P25: { callhandler::TagP25Data::ParrotFrame frame{}; frame.buffer = new uint8_t[FRAME_LENGTH](); frame.bufferLen = FRAME_LENGTH; frame.pktSeq = PACKET_SEQUENCE; frame.streamId = STREAM_ID; frame.peerId = sourcePeerId; frame.srcId = sourceId; frame.dstId = talkgroupId; network.m_tagP25->m_parrotFrames.push_back(frame); network.m_tagP25->playbackParrot(); break; } case ParrotProtocol::P25P2: { callhandler::TagP25P2Data::ParrotFrame frame{}; frame.buffer = new uint8_t[FRAME_LENGTH](); frame.bufferLen = FRAME_LENGTH; frame.pktSeq = PACKET_SEQUENCE; frame.streamId = STREAM_ID; frame.peerId = sourcePeerId; frame.srcId = sourceId; frame.dstId = talkgroupId; network.m_tagP25P2->m_parrotFrames.push_back(frame); network.m_tagP25P2->playbackParrot(); break; } case ParrotProtocol::NXDN: { callhandler::TagNXDNData::ParrotFrame frame{}; frame.buffer = new uint8_t[FRAME_LENGTH](); frame.bufferLen = FRAME_LENGTH; frame.pktSeq = PACKET_SEQUENCE; frame.streamId = STREAM_ID; frame.peerId = sourcePeerId; frame.srcId = sourceId; frame.dstId = talkgroupId; network.m_tagNXDN->m_parrotFrames.push_back(frame); network.m_tagNXDN->playbackParrot(); break; } case ParrotProtocol::ANALOG: { callhandler::TagAnalogData::ParrotFrame frame{}; frame.buffer = new uint8_t[FRAME_LENGTH](); frame.bufferLen = FRAME_LENGTH; frame.pktSeq = PACKET_SEQUENCE; frame.streamId = STREAM_ID; frame.peerId = sourcePeerId; frame.srcId = sourceId; frame.dstId = talkgroupId; network.m_tagAnalog->m_parrotFrames.push_back(frame); network.m_tagAnalog->playbackParrot(); break; } } } /* Evaluates a call handler's peer-routing policy. */ bool FNETestHooks::isPeerPermitted(TrafficNetwork& network, ParrotProtocol protocol, uint32_t peerId, uint32_t destinationId, bool group, bool fromUpstream, uint8_t slotNo) { constexpr uint32_t STREAM_ID = 101U; switch (protocol) { case ParrotProtocol::DMR: { dmr::data::NetData data; data.setDstId(destinationId); data.setSlotNo(slotNo); data.setFLCO(group ? dmr::defines::FLCO::GROUP : dmr::defines::FLCO::PRIVATE); return network.m_tagDMR->isPeerPermitted(peerId, data, STREAM_ID, fromUpstream); } case ParrotProtocol::P25: { p25::lc::LC control; control.setDstId(destinationId); control.setLCO(group ? p25::defines::LCO::GROUP : p25::defines::LCO::PRIVATE); control.setGroup(group); return network.m_tagP25->isPeerPermitted( peerId, control, p25::defines::DUID::LDU1, STREAM_ID, fromUpstream); } case ParrotProtocol::P25P2: return network.m_tagP25P2->isPeerPermitted( peerId, destinationId, slotNo, !group, STREAM_ID, fromUpstream); case ParrotProtocol::NXDN: { nxdn::lc::RTCH control; control.setDstId((uint16_t)destinationId); control.setGroup(group); return network.m_tagNXDN->isPeerPermitted( peerId, control, nxdn::defines::MessageType::RTCH_VCALL, STREAM_ID, fromUpstream); } case ParrotProtocol::ANALOG: { analog::data::NetData data; data.setDstId(destinationId); data.setGroup(group); return network.m_tagAnalog->isPeerPermitted(peerId, data, STREAM_ID, fromUpstream); } } return false; } /* Evaluates a call handler's group-call validation policy. */ bool FNETestHooks::validateGroupCall(TrafficNetwork& network, ParrotProtocol protocol, uint32_t peerId, uint32_t sourceId, uint32_t talkgroupId, uint8_t slotNo) { constexpr uint32_t STREAM_ID = 102U; switch (protocol) { case ParrotProtocol::DMR: { dmr::data::NetData data; data.setSrcId(sourceId); data.setDstId(talkgroupId); data.setSlotNo(slotNo); data.setFLCO(dmr::defines::FLCO::GROUP); data.setDataType(dmr::defines::DataType::VOICE_LC_HEADER); return network.m_tagDMR->validate(peerId, data, nullptr, STREAM_ID); } case ParrotProtocol::P25: { p25::lc::LC control; control.setSrcId(sourceId); control.setDstId(talkgroupId); control.setLCO(p25::defines::LCO::GROUP); return network.m_tagP25->validate( peerId, control, p25::defines::DUID::LDU1, nullptr, 0U, STREAM_ID); } case ParrotProtocol::P25P2: return network.m_tagP25P2->validate( peerId, sourceId, talkgroupId, slotNo, false, STREAM_ID); case ParrotProtocol::NXDN: { nxdn::lc::RTCH control; control.setSrcId((uint16_t)sourceId); control.setDstId((uint16_t)talkgroupId); control.setGroup(true); return network.m_tagNXDN->validate( peerId, control, nxdn::defines::MessageType::RTCH_VCALL, STREAM_ID); } case ParrotProtocol::ANALOG: { analog::data::NetData data; data.setSrcId(sourceId); data.setDstId(talkgroupId); data.setGroup(true); data.setFrameType(analog::defines::AudioFrameType::VOICE); return network.m_tagAnalog->validate(peerId, data, STREAM_ID); } } return false; } /* Validates P25 individual traffic whose current LCO is not private. */ bool FNETestHooks::validateP25IndividualCall(TrafficNetwork& network, uint32_t peerId, uint32_t sourceId, uint32_t destinationId) { constexpr uint32_t STREAM_ID = 103U; p25::lc::LC control; control.setSrcId(sourceId); control.setDstId(destinationId); control.setLCO(p25::defines::LCO::GROUP); control.setGroup(false); return network.m_tagP25->validate( peerId, control, p25::defines::DUID::LDU2, nullptr, 0U, STREAM_ID) && network.m_tagP25->isPeerPermitted( peerId, control, p25::defines::DUID::LDU2, STREAM_ID, false); } /* Processes a synthetic DMR FNE grant request. */ bool FNETestHooks::processDMRGrantRequest(TrafficNetwork& network, uint32_t peerId, uint32_t sourceId, uint32_t destinationId, bool unitToUnit) { return network.m_tagDMR->processGrantReq( sourceId, destinationId, 1U, unitToUnit, peerId, 1U, 105U); } /* Processes a synthetic NXDN FNE grant request. */ bool FNETestHooks::processNXDNGrantRequest(TrafficNetwork& network, uint32_t peerId, uint32_t sourceId, uint32_t destinationId, bool unitToUnit) { return network.m_tagNXDN->processGrantReq( sourceId, destinationId, unitToUnit, peerId, 1U, 106U); } /* Sets whether unit-to-unit traffic is globally disabled. */ void FNETestHooks::disallowU2U(TrafficNetwork& network, bool disabled) { network.m_disallowU2U = disabled; } /* Sets whether conventional sites bypass affiliation requirements. */ void FNETestHooks::allowConventionalAffiliationOverride(TrafficNetwork& network, bool enabled) { network.m_allowConvSiteAffOverride = enabled; } /* Sets whether P25 terminators are filtered by destination policy. */ void FNETestHooks::filterTerminators(TrafficNetwork& network, bool enabled) { network.m_filterTerminators = enabled; } /* Evaluates P25 TDU peer-routing policy. */ bool FNETestHooks::isP25TerminatorPeerPermitted(TrafficNetwork& network, uint32_t peerId, uint32_t talkgroupId) { p25::lc::LC control; control.setDstId(talkgroupId); control.setLCO(p25::defines::LCO::GROUP); return network.m_tagP25->isPeerPermitted( peerId, control, p25::defines::DUID::TDU, 103U, false); } /* Passes a deliberately truncated frame to a protocol handler. */ bool FNETestHooks::processTruncatedFrame(TrafficNetwork& network, ParrotProtocol protocol, uint32_t peerId) { constexpr uint32_t STREAM_ID = 104U; std::vector frame(1U, 0U); switch (protocol) { case ParrotProtocol::DMR: return network.m_tagDMR->processFrame( frame.data(), (uint32_t)frame.size(), peerId, peerId, 1U, STREAM_ID); case ParrotProtocol::P25: frame.resize(24U, 0U); frame[22U] = p25::defines::DUID::LDU1; return network.m_tagP25->processFrame( frame.data(), (uint32_t)frame.size(), peerId, peerId, 1U, STREAM_ID); case ParrotProtocol::P25P2: return network.m_tagP25P2->processFrame( frame.data(), (uint32_t)frame.size(), peerId, peerId, 1U, STREAM_ID); case ParrotProtocol::NXDN: return network.m_tagNXDN->processFrame( frame.data(), (uint32_t)frame.size(), peerId, peerId, 1U, STREAM_ID); case ParrotProtocol::ANALOG: return network.m_tagAnalog->processFrame( frame.data(), (uint32_t)frame.size(), peerId, peerId, 1U, STREAM_ID); } return false; } /** * @brief Initializes an instance of the HostFNE class. */ HostFNE::HostFNE(const std::string& confFile) : m_confFile(confFile), m_conf(), m_network(nullptr), m_mdNetwork(nullptr), m_vtunEnabled(false), m_packetDataMode(PacketDataMode::PROJECT25), #if !defined(_WIN32) m_tun(nullptr), #endif m_dmrEnabled(false), m_p25Enabled(false), m_p25P2Enabled(false), m_nxdnEnabled(false), m_analogEnabled(false), m_ridLookup(nullptr), m_tidLookup(nullptr), m_peerListLookup(nullptr), m_adjSiteMapLookup(nullptr), m_cryptoLookup(nullptr), m_peerNetworks(), m_pingTime(5U), m_maxMissedPings(5U), m_updateLookupTime(10U), m_peerReplicaSavesACL(false), m_allowActivityTransfer(false), m_allowDiagnosticTransfer(false), m_RESTAPI(nullptr) { /* stub */ } /** * @brief Finalizes an instance of the HostFNE class. */ HostFNE::~HostFNE() = default; /** * @brief Adds a peer to the specified TrafficNetwork. * @param network The TrafficNetwork instance. * @param peerId The ID of the peer to add. * @param state The connection state of the peer. * @param connected Whether the peer is connected. * @return Reference to the added FNEPeerConnection. */ FNEPeerConnection& FNETestHooks::addPeer(TrafficNetwork& network, uint32_t peerId, NET_CONN_STATUS state, bool connected) { TrafficNetwork::PeerConnectionPtr connection = std::make_shared(); connection->m_id = peerId; connection->m_connectionState = state; connection->m_connected = connected; network.storePeer(peerId, connection); return *connection; } /** * @brief Passes an encoded KMM through the FNE OTAR message dispatcher. * @param network The TrafficNetwork instance. * @param packet The KMM packet data. * @param llId The logical link ID. * @param payloadSize The size of the processed payload. * @return A unique pointer to the processed KMM frame, or nullptr if processing failed. */ std::unique_ptr FNETestHooks::processOTARKMM(TrafficNetwork& network, const std::vector& packet, uint32_t llId, uint32_t& payloadSize, uint8_t outerAlgoId, uint16_t outerKId, bool dataLinkIndependent) { payloadSize = 0U; if (packet.empty() || network.m_p25OTARService == nullptr) return nullptr; return network.m_p25OTARService->processKMM(packet.data(), (uint32_t)packet.size(), llId, false, &payloadSize, outerAlgoId, outerKId, nullptr, dataLinkIndependent); } /** * @brief Builds an OTAR rekey command for the specified logical link ID and KMM RSI. * @param network The TrafficNetwork instance. * @param llId The logical link ID. * @param kmmRSI The KMM RSI value. * @return A vector of vectors containing the OTAR rekey frames. */ std::vector> FNETestHooks::buildOTARRekey(TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI) { std::vector> frames; if (network.m_p25OTARService == nullptr) return frames; P25OTARService::KMMAuthContext auth; uint32_t firstLength = 0U; std::vector> additional; const bool previousAllowClear = network.m_p25OTARService->m_allowNoUKEKRekey; network.m_p25OTARService->m_allowNoUKEKRekey = true; UInt8Array first = network.m_p25OTARService->write_KMM_Rekey_Command(llId, kmmRSI, KMM_HelloFlag::REKEY_REQUEST_NO_UKEK, &firstLength, auth, &additional); network.m_p25OTARService->m_allowNoUKEKRekey = previousAllowClear; if (first == nullptr || firstLength == 0U) return frames; frames.emplace_back(first.get(), first.get() + firstLength); for (std::vector& frame : additional) frames.push_back(std::move(frame)); return frames; } /** * @brief Builds an OTAR Changeover Command without dispatching it. * @param network The TrafficNetwork instance. * @param llId The logical link ID. * @param kmmRSI The KMM RSI value. * @param supersededKeysetId The ID of the superseded keyset. * @param activeKeysetId The ID of the active keyset. * @return A vector containing the encoded OTAR Changeover Command. */ std::vector FNETestHooks::buildOTARChangeover(TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI, uint8_t supersededKeysetId, uint8_t activeKeysetId) { if (network.m_p25OTARService == nullptr) return {}; uint32_t length = 0U; UInt8Array encoded = network.m_p25OTARService->write_KMM_Changeover_Command(llId, kmmRSI, supersededKeysetId, activeKeysetId, &length); if (encoded == nullptr || length == 0U) return {}; return std::vector(encoded.get(), encoded.get() + length); } /** * @brief Builds an OTAR Warm Start Command without dispatching it. * @param network The TrafficNetwork instance. * @param llId The logical link ID. * @param kmmRSI The KMM RSI value. * @return A vector containing the encoded OTAR Warm Start Command. */ std::vector FNETestHooks::buildOTARWarmStart(TrafficNetwork& network, uint32_t llId, uint32_t kmmRSI) { if (network.m_p25OTARService == nullptr) return {}; uint32_t length = 0U; UInt8Array encoded = network.m_p25OTARService->write_KMM_WarmStart_Command(llId, kmmRSI, &length); if (encoded == nullptr || length == 0U) return {}; return std::vector(encoded.get(), encoded.get() + length); } /** * @brief Retrieves the temporary TEK for an OTAR Warm Start transaction. * @param network The TrafficNetwork instance. * @param rsi The RSI associated with the Warm Start transaction. * @param kid Output parameter for the key ID. * @param key Output parameter for the temporary TEK. * @return True if the temporary TEK was successfully retrieved, false otherwise. */ bool FNETestHooks::getOTARWarmStartTEK(TrafficNetwork& network, uint32_t rsi, uint16_t& kid, std::vector& key) { if (network.m_p25OTARService == nullptr) return false; std::lock_guard lock(network.m_p25OTARService->m_warmStartMutex); auto it = network.m_p25OTARService->m_warmStartTransactions.find(rsi); if (it == network.m_p25OTARService->m_warmStartTransactions.end()) return false; kid = it->second.temporaryKId; key.assign(it->second.temporaryTEK.begin(), it->second.temporaryTEK.end()); return true; } /** * @brief Checks if there is an active OTAR Warm Start transaction for the given RSI. * @param network The TrafficNetwork instance. * @param rsi The RSI to check. * @return True if there is an active Warm Start transaction, false otherwise. */ bool FNETestHooks::hasOTARWarmStart(TrafficNetwork& network, uint32_t rsi) { if (network.m_p25OTARService == nullptr) return false; std::lock_guard lock(network.m_p25OTARService->m_warmStartMutex); return network.m_p25OTARService->m_warmStartTransactions.find(rsi) != network.m_p25OTARService->m_warmStartTransactions.end(); } /** * @brief Injects and resolves an upstream TEK response through the OTAR cache. * @param network The TrafficNetwork instance. * @param key The KMM key item representing the TEK. * @param algId The algorithm ID used for the TEK. * @param keyLength The length of the TEK. * @return True if the TEK was successfully cached and resolved, false otherwise. */ bool FNETestHooks::cacheOTARTEK(TrafficNetwork& network, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength) { if (network.m_p25OTARService == nullptr) return false; network.m_p25OTARService->cacheUpstreamTEK(key, algId, keyLength); const uint32_t id = ((uint32_t)algId << 16U) | key.kId(); auto it = network.m_p25OTARService->m_upstreamTEKs.find(id); return it != network.m_p25OTARService->m_upstreamTEKs.end() && it->second.algorithmId == algId && it->second.keyLength == keyLength; } /** * @brief Injects and resolves an upstream UKEK response through the OTAR cache. * @param network The TrafficNetwork instance. * @param rsi The RSI associated with the UKEK. * @param key The KMM key item representing the UKEK. * @param algId The algorithm ID used for the UKEK. * @param keyLength The length of the UKEK. * @return True if the UKEK was successfully cached and resolved, false otherwise. */ bool FNETestHooks::cacheOTARUKEK(TrafficNetwork& network, uint32_t rsi, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength) { if (network.m_p25OTARService == nullptr) return false; network.m_p25OTARService->cacheUpstreamUKEK(rsi, key, algId, keyLength); auto it = network.m_p25OTARService->m_upstreamUKEKs.find(rsi); return it != network.m_p25OTARService->m_upstreamUKEKs.end() && it->second.algorithmId == algId && it->second.keyLength == keyLength; } /** * @brief Resolves the security parameters of an OTAR response packet. * @param network The TrafficNetwork instance. * @param packet The OTAR response packet data. * @param encrypted Output flag indicating if the packet is encrypted. * @param algoId Output parameter for the algorithm ID used for encryption. * @param kid Output parameter for the key ID used for encryption. * @return True if the security parameters were successfully resolved, false otherwise. */ bool FNETestHooks::resolveOTARResponseSecurity(TrafficNetwork& network, const std::vector& packet, bool& encrypted, uint8_t& algoId, uint16_t& kid) { if (packet.empty() || network.m_p25OTARService == nullptr) return false; return network.m_p25OTARService->resolveResponseSecurity(packet.data(), (uint32_t)packet.size(), encrypted, algoId, kid); } /** * @brief Passes a DLD KMM through the public P25 OTAR bearer entry point. * @param network The TrafficNetwork instance. * @param packet The DLD packet data. * @param llId The logical link ID. * @param n The sequence number. * @param encrypted Whether the packet is encrypted. * @param algoId The algorithm ID used for encryption. * @param kid The key ID used for encryption. * @param mi The message integrity value. * @return True if the packet was successfully processed, false otherwise. */ bool FNETestHooks::processOTARDLD(TrafficNetwork& network, const std::vector& packet, uint32_t llId, uint8_t n, bool encrypted, uint8_t algoId, uint16_t kid, const uint8_t* mi) { if (packet.empty() || network.m_p25OTARService == nullptr) return false; return network.m_p25OTARService->processDLD(packet.data(), (uint32_t)packet.size(), llId, n, encrypted, algoId, kid, mi); } /** * @brief Encodes and injects a complete DLD KMM through the P25 PDU assembler path. * @param network The TrafficNetwork instance. * @param packet The DLD packet data. * @param llId The logical link ID. * @param encrypted Whether the packet is encrypted. * @param algoId The algorithm ID used for encryption. * @param kid The key ID used for encryption. * @param mi The message integrity value. * @return True if the packet was successfully processed, false otherwise. */ bool FNETestHooks::processOTARDLDPDU(TrafficNetwork& network, const std::vector& packet, uint32_t llId, bool encrypted, uint8_t algoId, uint16_t kid, const uint8_t* mi) { if (packet.empty() || network.m_tagP25 == nullptr) return false; p25::data::DataHeader header; header.setFormat(PDUFormatType::CONFIRMED); header.setMFId(MFG_STANDARD); header.setAckNeeded(true); header.setOutbound(false); header.setSAP(encrypted ? PDUSAP::ENC_USER_DATA : PDUSAP::UNENC_KMM); header.setLLId(llId); header.setFullMessage(true); header.setBlocksToFollow(1U); if (encrypted) { if (mi == nullptr) return false; header.setEXSAP(PDUSAP::UNENC_KMM); header.setAlgId(algoId); header.setKId(kid); header.setMI(mi); } header.calculateLength((uint32_t)packet.size()); p25::data::Assembler assembler; uint32_t bitLength = 0U; UInt8Array assembled = assembler.assemble(header, false, encrypted, packet.data(), &bitLength); if (assembled == nullptr || bitLength <= P25_PREAMBLE_LENGTH_BITS) return false; const uint32_t blockCount = (bitLength - P25_PREAMBLE_LENGTH_BITS) / P25_PDU_FEC_LENGTH_BITS; auto* packetData = network.m_tagP25->packetData(); for (uint32_t block = 0U; block < blockCount; ++block) { uint8_t envelope[24U + P25_PDU_FEC_LENGTH_BYTES] = { 0U }; SET_UINT24(P25_PDU_FEC_LENGTH_BYTES, envelope, 8U); envelope[20U] = (uint8_t)(blockCount - 1U); envelope[21U] = (uint8_t)block; Utils::getBitRange(assembled.get(), envelope + 24U, P25_PREAMBLE_LENGTH_BITS + block * P25_PDU_FEC_LENGTH_BITS, P25_PDU_FEC_LENGTH_BITS); if (!packetData->processFrame(envelope, sizeof(envelope), 1U, (uint16_t)block, 1U)) return false; } return true; } /** Injects a one-block PDU response through the FNE packet-data receiver. */ bool FNETestHooks::processP25PDUResponse(TrafficNetwork& network, uint32_t llId, uint8_t responseStatus) { if (network.m_tagP25 == nullptr) return false; p25::data::DataHeader header; header.setFormat(PDUFormatType::RSP); header.setMFId(MFG_STANDARD); header.setOutbound(false); header.setResponseClass(PDUAckClass::ACK); header.setResponseType(PDUAckType::ACK); header.setResponseStatus(responseStatus); header.setLLId(llId); header.setFullMessage(true); header.setBlocksToFollow(0U); uint8_t envelope[24U + P25_PDU_FEC_LENGTH_BYTES] = { 0U }; SET_UINT24(P25_PDU_FEC_LENGTH_BYTES, envelope, 8U); envelope[20U] = 0U; envelope[21U] = 0U; header.encode(envelope + 24U); return network.m_tagP25->packetData()->processFrame(envelope, sizeof(envelope), 1U, 0U, 1U); } /** * @brief Passes a complete Version-0 DLI datagram through the network receive task. * @param network The TrafficNetwork instance. * @param datagram The DLI packet data. * @return True if the packet was successfully processed, false otherwise. */ void FNETestHooks::processOTARDLI(TrafficNetwork& network, const std::vector& datagram) { if (datagram.empty() || network.m_p25OTARService == nullptr) return; OTARPacketRequest* req = new OTARPacketRequest(); req->obj = network.m_p25OTARService; req->address = {}; req->addrLen = 0U; req->length = (int)datagram.size(); req->buffer = new uint8_t[datagram.size()]; ::memcpy(req->buffer, datagram.data(), datagram.size()); P25OTARService::taskNetworkRx(req); } /** * @brief Opens the real DLI UDP endpoint for an integration test. * @param network The TrafficNetwork instance. * @param address The IP address of the DLI UDP endpoint. * @param port The port number of the DLI UDP endpoint. * @return True if the endpoint was successfully opened, false otherwise. */ bool FNETestHooks::openOTARDLI(TrafficNetwork& network, const std::string& address, uint16_t port) { return network.m_p25OTARService != nullptr && network.m_p25OTARService->open(address, port); } /** * @brief Polls the real DLI UDP endpoint. * @param network The TrafficNetwork instance. * @param ms The number of milliseconds to wait. */ void FNETestHooks::clockOTARDLI(TrafficNetwork& network, uint32_t ms) { if (network.m_p25OTARService != nullptr) network.m_p25OTARService->clock(ms); } /** * @brief Closes the real DLI UDP endpoint. * @param network The TrafficNetwork instance. */ void FNETestHooks::closeOTARDLI(TrafficNetwork& network) { if (network.m_p25OTARService != nullptr) network.m_p25OTARService->close(); } /** * @brief Applies the OTAR outer encryption/decryption implementation. * @param network The TrafficNetwork instance. * @param algoId The algorithm ID used for encryption/decryption. * @param kid The key ID used for encryption/decryption. * @param mi The message integrity value. * @param packet The packet data to be encrypted/decrypted. * @param encrypt True to encrypt, false to decrypt. * @return The resulting encrypted/decrypted packet. */ std::vector FNETestHooks::cryptOTARKMM(TrafficNetwork& network, uint8_t algoId, uint16_t kid, uint8_t* mi, const std::vector& packet, bool encrypt) { if (packet.empty() || network.m_p25OTARService == nullptr) return {}; UInt8Array result = network.m_p25OTARService->cryptKMM(algoId, kid, mi, packet.data(), (uint32_t)packet.size(), encrypt); if (result == nullptr) return {}; return std::vector(result.get(), result.get() + packet.size()); } /** * @brief Checks if the specified inbound message number exists for the given RSI. * @param network The TrafficNetwork instance. * @param rsi The RSI to check. * @param mn The message number to check. * @return True if the inbound message number exists and matches, false otherwise. */ bool FNETestHooks::hasOTARInboundMessageNumber(TrafficNetwork& network, uint32_t rsi, uint16_t mn) { if (network.m_p25OTARService == nullptr) return false; auto it = network.m_p25OTARService->m_rsiInboundMessageNumber.find(rsi); return it != network.m_p25OTARService->m_rsiInboundMessageNumber.end() && it->second == mn; } /** * @brief Enables or disables KMF services for the specified TrafficNetwork. * @param network The TrafficNetwork instance. * @param enabled True to enable KMF services, false to disable. */ void FNETestHooks::setKMFServicesEnabled(TrafficNetwork& network, bool enabled) { network.m_kmfServicesEnabled = enabled; } /** * @brief Adds a cryptographic key to the network's crypto lookup. * @param network The TrafficNetwork instance. * @param key The cryptographic key to add. */ void FNETestHooks::addCryptoKey(TrafficNetwork& network, const EKCKeyItem& key) { network.m_cryptoLookup->addEntry(key); } /** * @brief Adds a UKEK to the test crypto container. * @param network The TrafficNetwork instance. * @param key The UKEK key item to add. */ void FNETestHooks::addCryptoUKEK(TrafficNetwork& network, const EKCKeyItem& key) { network.m_cryptoLookup->addUKEK(key); } // --------------------------------------------------------------------------- // Static Class Members // --------------------------------------------------------------------------- /** * @brief Checks if the specified peer exists in the TrafficNetwork. * @param network The TrafficNetwork instance. * @param peerId The ID of the peer to check. * @return True if the peer exists, false otherwise. */ bool FNETestHooks::hasPeer(const TrafficNetwork& network, uint32_t peerId) { return network.findPeer(peerId) != nullptr; } /** * @brief Retrieves the number of peers in the TrafficNetwork. * @param network The TrafficNetwork instance. * @return The number of peers. */ size_t FNETestHooks::peerCount(const TrafficNetwork& network) { return network.peerCount(); } /** * @brief Retrieves the connection state of the specified peer. * @param network The TrafficNetwork instance. * @param peerId The ID of the peer. * @return The connection state of the peer. */ NET_CONN_STATUS FNETestHooks::peerState(const TrafficNetwork& network, uint32_t peerId) { TrafficNetwork::PeerConnectionPtr peer = network.findPeer(peerId); if (peer == nullptr) throw std::out_of_range("peer does not exist"); return peer->connectionState(); } /** * @brief Retains a shared reference to the specified peer connection. * @param network The TrafficNetwork instance. * @param peerId The ID of the peer to retain. * @return Shared peer connection, or nullptr if the peer does not exist. */ std::shared_ptr FNETestHooks::retainPeer( const TrafficNetwork& network, uint32_t peerId) { return network.findPeer(peerId); } /** * @brief Removes a peer from the specified TrafficNetwork. * @param network The TrafficNetwork instance. * @param peerId The ID of the peer to remove. */ void FNETestHooks::removePeer(TrafficNetwork& network, uint32_t peerId) { network.removePeer(peerId); } /** * @brief Clears all peers from the specified TrafficNetwork. * @param network The TrafficNetwork instance. */ void FNETestHooks::clearPeers(TrafficNetwork& network) { for (const auto& peer : network.peersSnapshot()) network.removePeer(peer.first); } /** * @brief Replaces a replicated active peer list for testing. * @param network The TrafficNetwork instance. * @param peerId The replica peer ID. * @param peers The active peer list to store. */ void FNETestHooks::updateReplicatedPeers(TrafficNetwork& network, uint32_t peerId, const json::array& peers) { network.updateReplicatedPeers(peerId, peers); } /** * @brief Gets a snapshot of the replicated active peer lists. * @param network The TrafficNetwork instance. * @return Map of replica peer IDs to active peer lists. */ std::unordered_map FNETestHooks::replicatedPeersSnapshot(const TrafficNetwork& network) { return network.replicatedPeersSnapshot(); } /** * @brief Removes a replicated active peer list for testing. * @param network The TrafficNetwork instance. * @param peerId The replica peer ID. */ void FNETestHooks::eraseReplicatedPeers(TrafficNetwork& network, uint32_t peerId) { network.eraseReplicatedPeers(peerId); } /** * @brief Creates a NetPacketRequest for the specified network, metadata, function, subFunction, peerId, and packet. * @param network The TrafficNetwork instance. * @param metadata The MetadataNetwork instance (can be nullptr). * @param function The network function. * @param subFunction The network sub-function. * @param peerId The ID of the peer. * @param packet The packet data. * @return Pointer to the created NetPacketRequest. */ static NetPacketRequest* makeRequest(TrafficNetwork& network, MetadataNetwork* metadata, NET_FUNC::ENUM function, NET_SUBFUNC::ENUM subFunction, uint32_t peerId, const std::vector& packet) { NetPacketRequest* request = new NetPacketRequest(); request->obj = &network; request->metadataObj = metadata; request->peerId = peerId; request->fneHeader.setFunction(function); request->fneHeader.setSubFunction(subFunction); request->fneHeader.setPeerId(peerId); request->fneHeader.setStreamId(1U); request->rtpHeader.setSSRC(peerId); request->pktRxTime = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); request->length = static_cast(packet.size()); if (!packet.empty()) { request->buffer = new uint8_t[packet.size()]; ::memcpy(request->buffer, packet.data(), packet.size()); } return request; } /** * @brief Dispatches a traffic packet to the specified TrafficNetwork. * @param network The TrafficNetwork instance. * @param function The network function. * @param subFunction The network sub-function. * @param peerId The ID of the peer. * @param packet The packet data. */ void FNETestHooks::dispatchTraffic(TrafficNetwork& network, NET_FUNC::ENUM function, NET_SUBFUNC::ENUM subFunction, uint32_t peerId, const std::vector& packet) { TrafficNetwork::taskNetworkRx(makeRequest(network, nullptr, function, subFunction, peerId, packet)); } /** * @brief Dispatches a metadata packet to the specified TrafficNetwork. * @param network The TrafficNetwork instance. * @param metadata The MetadataNetwork instance. * @param function The network function. * @param subFunction The network sub-function. * @param peerId The ID of the peer. * @param packet The packet data. */ void FNETestHooks::dispatchMetadata(TrafficNetwork& network, MetadataNetwork& metadata, NET_FUNC::ENUM function, NET_SUBFUNC::ENUM subFunction, uint32_t peerId, const std::vector& packet) { MetadataNetwork::taskNetworkRx(makeRequest(network, &metadata, function, subFunction, peerId, packet)); } /* Starts the shared traffic worker pool for ordered-task tests. */ void FNETestHooks::startTrafficWorkerPool(TrafficNetwork& network) { network.m_threadPool.start(); } /* Stops and joins the shared traffic worker pool after an ordered-task test. */ void FNETestHooks::stopTrafficWorkerPool(TrafficNetwork& network) { network.m_threadPool.stop(); network.m_threadPool.wait(); } /* Installs a callback invoked for ordered protocol frames instead of decoding them. */ void FNETestHooks::setOrderedFrameHandler(TrafficNetwork& network, ParrotProtocol protocol, std::function handler) { switch (protocol) { case ParrotProtocol::DMR: network.m_tagDMR->m_orderedFrames.setTestFrameHandler(std::move(handler)); break; case ParrotProtocol::P25: network.m_tagP25->m_orderedFrames.setTestFrameHandler(std::move(handler)); break; case ParrotProtocol::P25P2: network.m_tagP25P2->m_orderedFrames.setTestFrameHandler(std::move(handler)); break; case ParrotProtocol::NXDN: network.m_tagNXDN->m_orderedFrames.setTestFrameHandler(std::move(handler)); break; case ParrotProtocol::ANALOG: break; } } /* Clears an ordered protocol frame callback. */ void FNETestHooks::clearOrderedFrameHandler(TrafficNetwork& network, ParrotProtocol protocol) { setOrderedFrameHandler(network, protocol, nullptr); } /* Enqueues a synthetic ordered protocol frame. */ bool FNETestHooks::enqueueOrderedFrame(TrafficNetwork& network, ParrotProtocol protocol, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint16_t pktSeq, bool fromUpstream) { const uint8_t frame[1U] = { 0U }; switch (protocol) { case ParrotProtocol::DMR: return network.m_tagDMR->enqueueFrame(frame, sizeof(frame), peerId, ssrc, pktSeq, streamId, fromUpstream); case ParrotProtocol::P25: return network.m_tagP25->enqueueFrame(frame, sizeof(frame), peerId, ssrc, pktSeq, streamId, fromUpstream); case ParrotProtocol::P25P2: return network.m_tagP25P2->enqueueFrame(frame, sizeof(frame), peerId, ssrc, pktSeq, streamId, fromUpstream); case ParrotProtocol::NXDN: return network.m_tagNXDN->enqueueFrame(frame, sizeof(frame), peerId, ssrc, pktSeq, streamId, fromUpstream); case ParrotProtocol::ANALOG: return false; } return false; }