From 2ba4dbf97c54a62081b716c99ed22dd46468ecaa Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sun, 8 Nov 2026 12:54:07 -0500 Subject: [PATCH] correct P25 unit-to-unit routing across FNE sites; use the explicit network U2U flag for private-call validation, routing, and late-entry voice when a private LCO is unavailable; restrict network answer requests to the control site serving the target subscriber; document conventional interoperability requirements; add FNE and host regression tests; --- configs/config.example.yml | 2 ++ src/fne/network/callhandler/TagP25Data.cpp | 29 +++++++++++++--------- src/host/p25/packet/ControlSignaling.cpp | 14 +++++++---- tests/fne/FNENetwork_Tests.cpp | 19 ++++++++++++++ tests/fne/FNETestHooks.cpp | 19 ++++++++++++++ tests/fne/FNETestHooks.h | 10 ++++++++ tests/p25/HostControl_Tests.cpp | 16 ++++++++++++ 7 files changed, 92 insertions(+), 17 deletions(-) diff --git a/configs/config.example.yml b/configs/config.example.yml index 2091c47d..4bc64ba4 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -340,6 +340,8 @@ protocols: # Flag indicating whether or not the host will acknowledge message packets. noMessageAck: true # Flag indicating that unit-to-unit availiability checks should be performed for a private call. + # (Disable unitToUnitAvailCheck for trunked sites that must call conventional RIDs, as unit-to-unit availability + # checks are not applicable in such scenarios.) unitToUnitAvailCheck: false # Flag indicating telephone interconnect requests are forwarded to the network for a telephone # interconnect gateway peer to service. (If disabled, telephone interconnect requests are denied.) diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 654b2ddb..d4610a69 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -586,8 +586,9 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId TrafficNetwork::MetricsLogging::incrementActiveCalls(m_network); } - // is this a private call? - if (lco == LCO::PRIVATE) { + // The network U2U flag is authoritative across all voice + // data units, including late entry without a private LC. + if (!control.getGroup() && !isTelIntTraffic(control, duid, nullptr)) { m_statusPVCall.lock(false); m_statusPVCall[dstId].callStartTime = pktTime; m_statusPVCall[dstId].srcId = srcId; @@ -687,7 +688,7 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId privateCallInProgress = true; } } else { - if (lco == LCO::PRIVATE) { + if (!control.getGroup() && !telInt) { privateCallInProgress = true; } } @@ -754,7 +755,7 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId } // is this a private call? - if ((lco == LCO::PRIVATE) && !neighbor) { + if (!control.getGroup() && !telInt && !neighbor) { // is this a private call? if so only repeat to the peer that registered the unit auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair& x) { if (x.second.dstId == dstId) { @@ -1605,7 +1606,15 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid, if (g_promiscuousHub) return true; - if (control.getLCO() == LCO::PRIVATE) { + // telephone interconnect calls are individual calls routed by their + // gateway policy rather than the U2U subscriber policy + if (duid != DUID::TSDU && isTelIntTraffic(control, duid, nullptr)) { + return true; + } + + // use the explicit network individual-call flag; private voice LC is not + // guaranteed in every LDU, particularly during late entry + if (!control.getGroup()) { if (m_network->m_disallowU2U) return false; if (!m_network->checkU2UDroppedPeer(peerId)) @@ -1620,11 +1629,6 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid, return false; } - // telephone interconnect calls are individual calls routed by isTelIntPeerPermitted() - if (duid != DUID::TSDU && isTelIntTraffic(control, duid, nullptr)) { - return true; - } - TrafficNetwork::PeerConnectionPtr connection = peerId > 0U ? m_network->findPeer(peerId) : nullptr; if (connection != nullptr) { @@ -1868,6 +1872,8 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const } m_rejectedCallStreams.unlock(); + const bool telIntCall = (duid != DUID::TSDU) && isTelIntTraffic(control, duid, nullptr); + // validate private call in-progress bool privateCallInProgress = false; if ((control.getLCO() != LCO::PRIVATE) && !control.getGroup()) { @@ -1885,8 +1891,7 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const } // is this a private or telephone interconnect call? - bool telIntCall = (duid != DUID::TSDU) && isTelIntTraffic(control, duid, nullptr); - if ((control.getLCO() == LCO::PRIVATE) || telIntCall || privateCallInProgress) { + if ((!control.getGroup() && !telIntCall) || telIntCall || privateCallInProgress) { // is the destination ID a blacklisted ID? lookups::RadioId rid = m_network->m_ridLookup->find(control.getDstId()); if (!rid.radioDefault()) { diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index bce52dc3..dcf36995 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -1050,12 +1050,16 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr } else { VERBOSE_LOG_TSBK_NET(tsbk->toString(), srcId, dstId); - if (m_p25->m_affiliations->isUnitReg(dstId)) { - const uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + - (tsbk->getEncrypted() ? 0x40U : 0x00U) + - (tsbk->getPriority() & 0x07U); - startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, false, true); + // only the control-enabled site currently serving + // the target SU may place this request on RF + if (!m_p25->m_enableControl || !m_p25->m_affiliations->isUnitReg(dstId)) { + return true; } + + const uint8_t serviceOptions = (tsbk->getEmergency() ? 0x80U : 0x00U) + + (tsbk->getEncrypted() ? 0x40U : 0x00U) + + (tsbk->getPriority() & 0x07U); + startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, false, true); } } break; diff --git a/tests/fne/FNENetwork_Tests.cpp b/tests/fne/FNENetwork_Tests.cpp index 98d795f8..ba1549c7 100644 --- a/tests/fne/FNENetwork_Tests.cpp +++ b/tests/fne/FNENetwork_Tests.cpp @@ -570,6 +570,25 @@ TEST_CASE("FNE call handlers reject globally disabled unit-to-unit traffic", "[f } } +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"); + + 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 call handlers validate source radios and group state", "[fne][callhandler][validation]") { constexpr uint32_t PEER_ID = 1001U; diff --git a/tests/fne/FNETestHooks.cpp b/tests/fne/FNETestHooks.cpp index 251e826f..b0520227 100644 --- a/tests/fne/FNETestHooks.cpp +++ b/tests/fne/FNETestHooks.cpp @@ -201,6 +201,7 @@ bool FNETestHooks::isPeerPermitted(TrafficNetwork& network, ParrotProtocol proto 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); } @@ -280,6 +281,24 @@ bool FNETestHooks::validateGroupCall(TrafficNetwork& network, ParrotProtocol pro 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); +} + /* Sets whether unit-to-unit traffic is globally disabled. */ void FNETestHooks::disallowU2U(TrafficNetwork& network, bool disabled) diff --git a/tests/fne/FNETestHooks.h b/tests/fne/FNETestHooks.h index 28bd37fd..73b9dfca 100644 --- a/tests/fne/FNETestHooks.h +++ b/tests/fne/FNETestHooks.h @@ -164,6 +164,16 @@ public: */ static bool validateGroupCall(network::TrafficNetwork& network, ParrotProtocol protocol, uint32_t peerId, uint32_t sourceId, uint32_t talkgroupId, uint8_t slotNo = 1U); + /** + * @brief Validates P25 individual traffic whose current LCO is not private. + * @param network The TrafficNetwork instance. + * @param peerId Originating peer ID. + * @param sourceId Source subscriber ID. + * @param destinationId Target subscriber ID. + * @return True when the explicit individual-call flag is honored. + */ + static bool validateP25IndividualCall(network::TrafficNetwork& network, uint32_t peerId, + uint32_t sourceId, uint32_t destinationId); /** * @brief Sets whether unit-to-unit traffic is globally disabled. * @param network The TrafficNetwork instance. diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 3faf0307..6a9b1bd6 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -1000,6 +1000,22 @@ TEST_CASE("P25 unit-to-unit target site correlates a network answer request with CHECK(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); } +TEST_CASE("P25 network answer request is ignored outside the target unit site", + "[p25][host][control][u2u][procedure][network]") +{ + P25HostHarness harness; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + buildUUAnswerRequest(caller, called, raw); + + REQUIRE(HostTestHooks::p25ProcessNetworkRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25PendingU2UCount(*harness.m_control) == 0U); + CHECK_FALSE(HostTestHooks::p25IsU2UGranted(*harness.m_control, caller, called)); +} + TEST_CASE("P25 host preserves encrypted network call parameters", "[p25][host][control][crypto]") { P25HostHarness harness;