From d2ac635b9dec2f2d0e280c7b637d8d3b2429fc6c Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Tue, 10 Nov 2026 09:58:08 -0500 Subject: [PATCH] correct P25 group affiliation query handling, correlate and retry affiliation queries, refresh WUID validity only for acknowledged valid transactions and complete group affiliation query support; --- src/common/lookups/AffiliationLookup.cpp | 15 +++ src/common/lookups/AffiliationLookup.h | 5 + src/common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.cpp | 9 +- src/common/p25/lc/tsbk/OSP_GRP_AFF_Q.cpp | 11 +- src/host/p25/Control.cpp | 1 + src/host/p25/packet/ControlSignaling.cpp | 106 ++++++++++++++++--- src/host/p25/packet/ControlSignaling.h | 36 ++++++- tests/p25/TSBK_Tests.cpp | 54 ++++++++++ 8 files changed, 218 insertions(+), 19 deletions(-) diff --git a/src/common/lookups/AffiliationLookup.cpp b/src/common/lookups/AffiliationLookup.cpp index fd7a9908..09abd5b1 100644 --- a/src/common/lookups/AffiliationLookup.cpp +++ b/src/common/lookups/AffiliationLookup.cpp @@ -149,6 +149,21 @@ void AffiliationLookup::touchUnitReg(uint32_t srcId) } } +/* Restarts only the source ID registration timer. */ + +void AffiliationLookup::touchUnitRegTimer(uint32_t srcId) +{ + if (srcId == 0U) { + return; + } + + __spinlock(); + + if (isUnitReg(srcId)) { + m_unitRegTimers[srcId].start(); + } +} + /* Gets the current timer timeout for this unit registration. */ uint32_t AffiliationLookup::unitRegTimeout(uint32_t srcId) diff --git a/src/common/lookups/AffiliationLookup.h b/src/common/lookups/AffiliationLookup.h index c6092aa3..c80f53dc 100644 --- a/src/common/lookups/AffiliationLookup.h +++ b/src/common/lookups/AffiliationLookup.h @@ -87,6 +87,11 @@ namespace lookups * @param srcId Source Radio ID. */ virtual void touchUnitReg(uint32_t srcId); + /** + * @brief Restarts only the source ID registration timer. + * @param srcId Source Radio ID. + */ + virtual void touchUnitRegTimer(uint32_t srcId); /** * @brief Gets the current timer timeout for this unit registration. * @param srcId Source Radio ID. diff --git a/src/common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.cpp b/src/common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.cpp index 3294e542..31be43f0 100644 --- a/src/common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.cpp +++ b/src/common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.cpp @@ -44,6 +44,7 @@ bool ISP_GRP_AFF_Q_RSP::decode(const uint8_t* data, bool rawTSBK) ulong64_t tsbkValue = TSBK::toValue(tsbk); + m_emergency = ((tsbkValue >> 56) & 0x80U) != 0U; // Emergency Flag m_announceGroup = (uint32_t)((tsbkValue >> 40) & 0xFFFFU); // Announcement Group Address m_dstId = (uint32_t)((tsbkValue >> 24) & 0xFFFFU); // Talkgroup Address m_srcId = (uint32_t)(tsbkValue & 0xFFFFFFU); // Source Radio Address @@ -57,7 +58,13 @@ void ISP_GRP_AFF_Q_RSP::encode(uint8_t* data, bool rawTSBK, bool noTrellis) { assert(data != nullptr); - /* stub */ + ulong64_t tsbkValue = m_emergency ? 0x80U : 0x00U; // Emergency Flag + tsbkValue = (tsbkValue << 16) + m_announceGroup; // Announcement Group Address + tsbkValue = (tsbkValue << 16) + m_dstId; // Talkgroup Address + tsbkValue = (tsbkValue << 24) + m_srcId; // Source Radio Address + + std::unique_ptr tsbk = TSBK::fromValue(tsbkValue); + TSBK::encode(data, tsbk.get(), rawTSBK, noTrellis); } /* Returns a string that represents the current TSBK. */ diff --git a/src/common/p25/lc/tsbk/OSP_GRP_AFF_Q.cpp b/src/common/p25/lc/tsbk/OSP_GRP_AFF_Q.cpp index 6c30410d..f90ca756 100644 --- a/src/common/p25/lc/tsbk/OSP_GRP_AFF_Q.cpp +++ b/src/common/p25/lc/tsbk/OSP_GRP_AFF_Q.cpp @@ -34,7 +34,16 @@ bool OSP_GRP_AFF_Q::decode(const uint8_t* data, bool rawTSBK) { assert(data != nullptr); - /* stub */ + uint8_t tsbk[P25_TSBK_LENGTH_BYTES + 1U]; + ::memset(tsbk, 0x00U, P25_TSBK_LENGTH_BYTES); + + bool ret = TSBK::decode(data, tsbk, rawTSBK); + if (!ret) + return false; + + ulong64_t tsbkValue = TSBK::toValue(tsbk); + m_dstId = (uint32_t)((tsbkValue >> 24) & 0xFFFFFFU); // Target Radio Address + m_srcId = (uint32_t)(tsbkValue & 0xFFFFFFU); // Source Radio Address return true; } diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 412b2fb5..2b30078e 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -225,6 +225,7 @@ void Control::reset() if (m_control != nullptr) { m_control->resetU2UAvailabilityTransactions(); + m_control->resetGroupAffQueryTransactions(); } m_txImmQueue.clear(); diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 544dd211..6c64a2af 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -153,6 +153,8 @@ const uint32_t TSDU_CTRL_BURST_COUNT = 2U; const uint32_t TSBK_MBF_CNT = 3U; const uint32_t GRANT_TIMER_TIMEOUT = 15U; const uint32_t U2U_AVAILABILITY_TIMER_TIMEOUT = 15U; +const uint32_t GROUP_AFF_QUERY_TIMER_TIMEOUT = 4U; +const uint8_t GROUP_AFF_QUERY_MAX_RETRIES = 4U; const uint8_t CONV_FALLBACK_PACKET_DELAY = 8U; // --------------------------------------------------------------------------- @@ -230,7 +232,6 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrm_affiliations->touchUnitReg(srcId); m_lastMFID = tsbk->getMFId(); // handle standard P25 reference opcodes @@ -763,14 +764,10 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrtoString(true), TSBKO::ISP_GRP_AFF_Q_RSP, srcId); // validate the source RID - VALID_SRCID(tsbk->toString(true), TSBKO::IOSP_ACK_RSP, srcId); - - // validate the target RID - VALID_DSTID(tsbk->toString(true), TSBKO::IOSP_ACK_RSP, srcId, dstId); + VALID_SRCID(tsbk->toString(true), TSBKO::ISP_GRP_AFF_Q_RSP, srcId); - if (m_p25->m_ackTSBKRequests) { - writeRF_TSDU_ACK_FNE(srcId, TSBKO::ISP_GRP_AFF_Q_RSP, true, true); - } + // verify the source RID is registered + VERIFY_SRCID_REG(tsbk->toString(true), TSBKO::ISP_GRP_AFF_Q_RSP, srcId); ISP_GRP_AFF_Q_RSP* isp = static_cast(tsbk.get()); if (m_verbose) { @@ -778,15 +775,44 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrtoString(true).c_str(), srcId, dstId, isp->getAnnounceGroup()); } - ::ActivityLog("P25", true, "group affiliation query response from %u to %s %u", srcId, "TG ", dstId); + // look for a pending group affiliation query transaction for this source ID + auto query = m_groupAffQueryTransactions.find(srcId); + if (query == m_groupAffQueryTransactions.end()) { + LogWarning(LOG_RF, P25_TSDU_STR ", unsolicited group affiliation query response, srcId = %u", srcId); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } - if (!m_p25->m_affiliations->isGroupAff(srcId, dstId)) { - // update dynamic affiliation table - m_p25->m_affiliations->groupAff(srcId, dstId); + m_groupAffQueryTransactions.erase(query); - if (m_p25->m_network != nullptr) - m_p25->m_network->announceGroupAffiliation(srcId, dstId); + // check if the announced group matches the expected announcement group and if the source is affiliated with the target group + if (isp->getAnnounceGroup() != m_announcementGroup || !m_p25->m_affiliations->isGroupAff(srcId, dstId)) { + LogWarning(LOG_RF, P25_TSDU_STR ", affiliation query mismatch, srcId = %u, dstId = %u, anncId = %u", srcId, dstId, isp->getAnnounceGroup()); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::ISP_GRP_AFF_Q_RSP); + writeRF_TSDU_U_Reg_Cmd(srcId); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } + + // preserve the normal affiliation path's null-group exception + if (dstId != 0U) { + // validate the talkgroup ID + VALID_TGID(tsbk->toString(true), TSBKO::ISP_GRP_AFF_Q_RSP, srcId, dstId); + + // check if the target group ID is non-preferred + if (acl::AccessControl::tgidNonPreferred(dstId)) { + LogWarning(LOG_RF, P25_TSDU_STR ", %s non-preferred on this site, TGID rejection, dstId = %u", tsbk->toString(true).c_str(), dstId); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::ISP_GRP_AFF_Q_RSP); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } } + + writeRF_TSDU_ACK_FNE(srcId, TSBKO::ISP_GRP_AFF_Q_RSP, false, true); + + m_p25->m_affiliations->touchUnitReg(srcId); + + ::ActivityLog("P25", true, "group affiliation query response from %u to %s %u", srcId, "TG ", dstId); } break; case TSBKO::ISP_U_DEREG_REQ: @@ -1500,6 +1526,19 @@ void ControlSignaling::writeRF_TSDU_Ext_Func(uint32_t func, uint32_t arg, uint32 /* Helper to write a group affiliation query packet. */ void ControlSignaling::writeRF_TSDU_Grp_Aff_Q(uint32_t dstId) +{ + if (dstId == 0U) { + return; + } + + m_groupAffQueryTransactions.erase(dstId); + m_groupAffQueryTransactions.emplace(dstId, GroupAffQueryTransaction(GROUP_AFF_QUERY_TIMER_TIMEOUT)); + writeRF_TSDU_Grp_Aff_Q_Once(dstId); +} + +/* Emits a group affiliation query without changing transaction state. */ + +void ControlSignaling::writeRF_TSDU_Grp_Aff_Q_Once(uint32_t dstId) { std::unique_ptr osp = std::make_unique(); osp->setSrcId(WUID_FNE); @@ -1680,6 +1719,25 @@ void ControlSignaling::clock(uint32_t ms) ++it; } } + + // TIA-102.AABD-B 6.7.3 permits the RFSS to repeat the query while waiting + // for the response. Nretry is four per Table 17-1. + for (auto it = m_groupAffQueryTransactions.begin(); it != m_groupAffQueryTransactions.end(); ) { + it->second.responseTimer.clock(ms); + if (it->second.responseTimer.isRunning() && it->second.responseTimer.hasExpired()) { + if (it->second.retries >= GROUP_AFF_QUERY_MAX_RETRIES) { + LogWarning(LOG_RF, P25_TSDU_STR ", group affiliation query response expired, dstId = %u", it->first); + it = m_groupAffQueryTransactions.erase(it); + continue; + } + + ++it->second.retries; + writeRF_TSDU_Grp_Aff_Q_Once(it->first); + it->second.responseTimer.start(); + } + + ++it; + } } /* Clears all pending unit-to-unit availability transactions. */ @@ -1689,6 +1747,13 @@ void ControlSignaling::resetU2UAvailabilityTransactions() m_u2uAvailabilityTransactions.clear(); } +/* Clears all pending group-affiliation query transactions. */ + +void ControlSignaling::resetGroupAffQueryTransactions() +{ + m_groupAffQueryTransactions.clear(); +} + /* Write data processed from RF to the network. */ void ControlSignaling::writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset) @@ -3056,6 +3121,10 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ m_p25->m_rfTGHang.start(); } + if (!net) { + m_p25->m_affiliations->touchUnitRegTimer(srcId); + } + return true; } @@ -3265,6 +3334,7 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3 } } + m_p25->m_affiliations->touchUnitRegTimer(srcId); return true; } @@ -3283,7 +3353,7 @@ void ControlSignaling::writeRF_TSDU_UU_Ans_Req(uint32_t srcId, uint32_t dstId) /* Helper to write a acknowledge packet. */ -void ControlSignaling::writeRF_TSDU_ACK_FNE(uint32_t dstId, uint32_t service, bool extended, bool noNetwork) +void ControlSignaling::writeRF_TSDU_ACK_FNE(uint32_t dstId, uint32_t service, bool extended, bool noNetwork, bool refreshRegistration) { std::unique_ptr iosp = std::make_unique(); iosp->setAIV(true); @@ -3303,6 +3373,10 @@ void ControlSignaling::writeRF_TSDU_ACK_FNE(uint32_t dstId, uint32_t service, bo } writeRF_TSDU_SBF_Imm(iosp.get(), noNetwork); + + if (refreshRegistration) { + m_p25->m_affiliations->touchUnitRegTimer(dstId); + } } /* Helper to write a deny packet. */ @@ -3327,7 +3401,7 @@ void ControlSignaling::writeRF_TSDU_Deny(uint32_t srcId, uint32_t dstId, uint8_t // are deny responses disabled? if (!m_p25->m_dedicatedControl && !m_p25->m_controlOnly && m_p25->m_disableDenyResponse) { // at least ACK the request to try to silence the sender - writeRF_TSDU_ACK_FNE(srcId, service, aiv, false); + writeRF_TSDU_ACK_FNE(srcId, service, aiv, false, false); return; } diff --git a/src/host/p25/packet/ControlSignaling.h b/src/host/p25/packet/ControlSignaling.h index b737040e..3c81e0a8 100644 --- a/src/host/p25/packet/ControlSignaling.h +++ b/src/host/p25/packet/ControlSignaling.h @@ -209,6 +209,26 @@ namespace p25 } }; + /** + * @brief Pending group-affiliation query transaction. + * @ingroup host_p25 + */ + struct GroupAffQueryTransaction { + uint8_t retries; //!< Number of retransmissions already sent. + Timer responseTimer; //!< TACK response timer. + + /** + * @brief Initializes a pending group-affiliation query transaction. + * @param timeoutSeconds Response timeout in seconds. + */ + explicit GroupAffQueryTransaction(uint32_t timeoutSeconds) : + retries(0U), + responseTimer(1000U, timeoutSeconds) + { + responseTimer.start(); + } + }; + Control* m_p25; uint32_t m_patchSuperGroup; @@ -243,6 +263,7 @@ namespace p25 bool m_telephoneInterconnect; std::unordered_set m_telIntGrants; std::unordered_map m_u2uAvailabilityTransactions; + std::unordered_map m_groupAffQueryTransactions; uint8_t m_convFallbackPacketDelay; bool m_convFallback; @@ -291,6 +312,11 @@ namespace p25 */ void resetU2UAvailabilityTransactions(); + /** + * @brief Clears all pending group-affiliation query transactions. + */ + void resetGroupAffQueryTransactions(); + /** * @brief Write data processed from RF to the network. * @param tsbk TSBK to write to network. @@ -417,8 +443,16 @@ namespace p25 * @param service Service being acknowledged. * @param extended Flag indicating this is an extended acknowledgement. * @param noNetwork Flag indicating the acknowledgement must not be forwarded to the network. + * @param refreshRegistration Flag indicating the acknowledgement confirms a valid WUID transaction. + */ + void writeRF_TSDU_ACK_FNE(uint32_t dstId, uint32_t service, bool extended, bool noNetwork, + bool refreshRegistration = true); + + /** + * @brief Emits a group-affiliation query without changing transaction state. + * @param dstId Destination Radio ID. */ - void writeRF_TSDU_ACK_FNE(uint32_t dstId, uint32_t service, bool extended, bool noNetwork); + void writeRF_TSDU_Grp_Aff_Q_Once(uint32_t dstId); /** * @brief Helper to write a deny packet. * @param srcId Source Radio ID. diff --git a/tests/p25/TSBK_Tests.cpp b/tests/p25/TSBK_Tests.cpp index b040ab53..f8d63e68 100644 --- a/tests/p25/TSBK_Tests.cpp +++ b/tests/p25/TSBK_Tests.cpp @@ -10,6 +10,8 @@ #include "host/Defines.h" #include "common/p25/lc/tsbk/OSP_SCCB.h" #include "common/p25/lc/tsbk/OSP_SYS_SRV_BCAST.h" +#include "common/p25/lc/tsbk/OSP_GRP_AFF_Q.h" +#include "common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.h" #include "common/p25/lc/tsbk/OSP_TSBK_RAW.h" #include "common/p25/lc/tsbk/TSBKFactory.h" #include "common/p25/data/DataBlock.h" @@ -64,6 +66,58 @@ TEST_CASE("TSBK", "[p25][tsbk]") { TSBK::setSiteData(previousSiteData); } + SECTION("Group_Affiliation_Query_Known_Bytes") { + OSP_GRP_AFF_Q query; + query.setDstId(0x123456U); + query.setSrcId(WUID_FNE); + + uint8_t raw[P25_TSBK_LENGTH_BYTES]; + ::memset(raw, 0x00U, sizeof(raw)); + query.encode(raw, true, true); + + CHECK(raw[2U] == 0x00U); + CHECK(raw[3U] == 0x00U); + CHECK(raw[4U] == 0x12U); + CHECK(raw[5U] == 0x34U); + CHECK(raw[6U] == 0x56U); + CHECK(raw[7U] == 0xFFU); + CHECK(raw[8U] == 0xFFU); + CHECK(raw[9U] == 0xFCU); + + OSP_GRP_AFF_Q decoded; + REQUIRE(decoded.decode(raw, true)); + CHECK(decoded.getDstId() == 0x123456U); + CHECK(decoded.getSrcId() == WUID_FNE); + } + + SECTION("Group_Affiliation_Query_Response_Known_Bytes") { + ISP_GRP_AFF_Q_RSP response; + response.setEmergency(true); + response.setAnnounceGroup(0x2468U); + response.setDstId(0x1357U); + response.setSrcId(0x123456U); + + uint8_t raw[P25_TSBK_LENGTH_BYTES]; + ::memset(raw, 0x00U, sizeof(raw)); + response.encode(raw, true, true); + + CHECK(raw[2U] == 0x80U); + CHECK(raw[3U] == 0x24U); + CHECK(raw[4U] == 0x68U); + CHECK(raw[5U] == 0x13U); + CHECK(raw[6U] == 0x57U); + CHECK(raw[7U] == 0x12U); + CHECK(raw[8U] == 0x34U); + CHECK(raw[9U] == 0x56U); + + ISP_GRP_AFF_Q_RSP decoded; + REQUIRE(decoded.decode(raw, true)); + CHECK(decoded.getEmergency()); + CHECK(decoded.getAnnounceGroup() == 0x2468U); + CHECK(decoded.getDstId() == 0x1357U); + CHECK(decoded.getSrcId() == 0x123456U); + } + SECTION("Emergency_Alarm_Request_Roundtrip") { ISP_EMERG_ALRM_REQ source; source.setSpecialInfo1(0x01U);