From 5177685e48c0be1f569a0fe49bfb044e2b07cc92 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Tue, 10 Nov 2026 11:08:23 -0500 Subject: [PATCH] implement FNE synchronized talkgroup RID admission rules, enforce group access during affiliation, RF ingress and channel grants for P25, DMR and NXDN while excluding U2U and telephone interconnect traffic, and add round-robin and protocol controller admission tests; --- .../schema/talkgroup_rules.yaml_schema.json | 4 +- configs/talkgroup_rules.example.yml | 18 ++-- src/common/dmr/acl/AccessControl.cpp | 7 ++ src/common/dmr/acl/AccessControl.h | 8 ++ src/common/lookups/TalkgroupRulesLookup.cpp | 50 ++++++++++ src/common/lookups/TalkgroupRulesLookup.h | 16 +++- src/common/network/BaseNetwork.h | 2 +- src/common/network/Network.cpp | 2 + src/common/network/Network.h | 3 + src/common/network/RTPFNEHeader.h | 1 + src/common/network/packet/Master.cpp | 92 +++++++++++++++++++ src/common/nxdn/acl/AccessControl.cpp | 7 ++ src/common/nxdn/acl/AccessControl.h | 7 ++ src/common/p25/acl/AccessControl.cpp | 7 ++ src/common/p25/acl/AccessControl.h | 7 ++ src/fne/network/TrafficNetwork.cpp | 81 ++++++++++++++++ src/fne/network/TrafficNetwork.h | 10 ++ src/fne/network/callhandler/TagAnalogData.cpp | 7 +- src/fne/network/callhandler/TagDMRData.cpp | 12 +-- src/fne/network/callhandler/TagNXDNData.cpp | 14 +-- src/fne/network/callhandler/TagP25Data.cpp | 12 +-- src/fne/network/callhandler/TagP25P2Data.cpp | 7 +- src/host/dmr/packet/ControlSignaling.cpp | 20 +++- src/host/dmr/packet/Data.cpp | 4 +- src/host/dmr/packet/Voice.cpp | 4 +- src/host/nxdn/packet/ControlSignaling.cpp | 16 +++- src/host/nxdn/packet/Data.cpp | 4 +- src/host/nxdn/packet/Voice.cpp | 4 +- src/host/p25/packet/ControlSignaling.cpp | 51 +++++++--- src/host/p25/packet/Voice.cpp | 4 +- src/host/p25/phase2/Slot.cpp | 3 +- tests/dmr/HostControl_Tests.cpp | 58 +++++++++++- tests/nxdn/HostControl_Tests.cpp | 54 ++++++++++- tests/p25/HostControl_Tests.cpp | 71 +++++++++++++- 34 files changed, 592 insertions(+), 75 deletions(-) diff --git a/configs/schema/talkgroup_rules.yaml_schema.json b/configs/schema/talkgroup_rules.yaml_schema.json index 950b69bc..aad1fb72 100644 --- a/configs/schema/talkgroup_rules.yaml_schema.json +++ b/configs/schema/talkgroup_rules.yaml_schema.json @@ -88,7 +88,7 @@ } }, "rid_permitted": { - "description": "List of radio IDs permitted to transmit on the talkgroup.", + "description": "List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. An empty list admits all radio IDs when talkgroup ACL enforcement is enabled.", "type": "array", "items": { "description": "Radio ID.", @@ -117,4 +117,4 @@ } } } -} \ No newline at end of file +} diff --git a/configs/talkgroup_rules.example.yml b/configs/talkgroup_rules.example.yml index 555a69b6..61375ca7 100644 --- a/configs/talkgroup_rules.example.yml +++ b/configs/talkgroup_rules.example.yml @@ -53,7 +53,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration @@ -92,7 +93,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration @@ -137,7 +139,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration @@ -174,7 +177,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration @@ -211,7 +215,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration @@ -248,7 +253,8 @@ groupVoice: # sites not listed here will be non-preferred and will cause a AFF_GRP_RSP DENY, typically triggering roaming). # If this list is empty *all* peers are preferred. (Trunking Only) preferred: [] - # List of radio IDs permitted to transmit on the talkgroup. + # List of radio IDs admitted to affiliate with and originate traffic on the talkgroup. + # An empty list admits all radio IDs when talkgroup ACL enforcement is enabled. rid_permitted: [] # # Source Configuration diff --git a/src/common/dmr/acl/AccessControl.cpp b/src/common/dmr/acl/AccessControl.cpp index 2935db3f..e9915354 100644 --- a/src/common/dmr/acl/AccessControl.cpp +++ b/src/common/dmr/acl/AccessControl.cpp @@ -82,6 +82,13 @@ bool AccessControl::validateTGId(uint32_t slotNo, uint32_t id) } } +/* Validates whether a radio ID is admitted to a talkgroup. */ + +bool AccessControl::validateTGAccess(uint32_t slotNo, uint32_t srcId, uint32_t dstId) +{ + return m_tidLookup->isRIDPermitted(dstId, static_cast(slotNo), srcId); +} + /* Helper to determine if a talkgroup ID is non-preferred. */ bool AccessControl::tgidNonPreferred(uint32_t slotNo, uint32_t id) diff --git a/src/common/dmr/acl/AccessControl.h b/src/common/dmr/acl/AccessControl.h index 36f24a11..4f4f242d 100644 --- a/src/common/dmr/acl/AccessControl.h +++ b/src/common/dmr/acl/AccessControl.h @@ -58,6 +58,14 @@ namespace dmr * @returns bool True, if talkgroup ID is valid, otherwise false. */ static bool validateTGId(uint32_t slotNo, uint32_t id); + /** + * @brief Validates whether a radio ID is admitted to a talkgroup. + * @param slotNo DMR slot number. + * @param srcId Source radio ID. + * @param dstId Destination talkgroup ID. + * @returns bool True if admitted, otherwise false. + */ + static bool validateTGAccess(uint32_t slotNo, uint32_t srcId, uint32_t dstId); /** * @brief Helper to determine if a talkgroup ID is non-preferred. diff --git a/src/common/lookups/TalkgroupRulesLookup.cpp b/src/common/lookups/TalkgroupRulesLookup.cpp index 2cbac3e5..fc19b839 100644 --- a/src/common/lookups/TalkgroupRulesLookup.cpp +++ b/src/common/lookups/TalkgroupRulesLookup.cpp @@ -294,6 +294,56 @@ TalkgroupRuleGroupVoice TalkgroupRulesLookup::findByRewrite(uint32_t peerId, uin return entry; } +/* Determines whether a radio ID is admitted to a talkgroup. */ + +bool TalkgroupRulesLookup::isRIDPermitted(uint32_t id, uint8_t slot, uint32_t radioId) +{ + if (!m_acl) { + return true; + } + + TalkgroupRuleGroupVoice entry = find(id, slot); + if (entry.isInvalid()) { + return false; + } + + const std::vector permittedRIDs = entry.config().permittedRIDs(); + if (permittedRIDs.empty()) { + return true; + } + + return std::find(permittedRIDs.begin(), permittedRIDs.end(), radioId) != permittedRIDs.end(); +} + +/* Atomically replaces all network-delivered talkgroup RID admission lists. */ + +void TalkgroupRulesLookup::replaceRIDAdmissions(const std::vector& admissions) +{ + std::lock_guard lock(s_mutex); + + // clear the permitted RIDs for all existing group voice entries + for (TalkgroupRuleGroupVoice& entry : m_groupVoice) { + TalkgroupRuleConfig config = entry.config(); + config.permittedRIDs(std::vector()); + entry.config(config); + } + + // apply the new RID admissions to the existing group voice entries + for (const TalkgroupRuleGroupVoice& admission : admissions) { + const uint32_t id = admission.source().tgId(); + const uint8_t slot = admission.source().tgSlot(); + auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(), + [id, slot](const TalkgroupRuleGroupVoice& entry) { + return entry.source().tgId() == id && (slot == 0U || entry.source().tgSlot() == slot); + }); + if (it != m_groupVoice.end()) { + TalkgroupRuleConfig config = it->config(); + config.permittedRIDs(admission.config().permittedRIDs()); + it->config(config); + } + } +} + /* Saves loaded talkgroup rules. */ bool TalkgroupRulesLookup::commit(bool quiet) diff --git a/src/common/lookups/TalkgroupRulesLookup.h b/src/common/lookups/TalkgroupRulesLookup.h index 32d7c7ce..e4892e99 100644 --- a/src/common/lookups/TalkgroupRulesLookup.h +++ b/src/common/lookups/TalkgroupRulesLookup.h @@ -432,7 +432,7 @@ namespace lookups DECLARE_PROPERTY_PLAIN(std::vector, preferred); /** - * @brief List of radios IDs permitted to transmit on the talkgroup. + * @brief Radio IDs admitted to affiliate with and originate traffic on the talkgroup; empty admits all RIDs. */ DECLARE_PROPERTY_PLAIN(std::vector, permittedRIDs); @@ -629,6 +629,20 @@ namespace lookups */ virtual TalkgroupRuleGroupVoice findByRewrite(uint32_t peerId, uint32_t id, uint8_t slot = 0U); + /** + * @brief Determines whether a radio ID is admitted to a talkgroup. + * @param id Talkgroup ID. + * @param slot DMR slot this talkgroup is valid on, or zero when not applicable. + * @param radioId Source radio ID. + * @returns bool True if talkgroup ACL enforcement is disabled, the talkgroup has no RID restriction, or the RID is permitted. + */ + bool isRIDPermitted(uint32_t id, uint8_t slot, uint32_t radioId); + /** + * @brief Atomically replaces all network-delivered talkgroup RID admission lists. + * @param[in] admissions Talkgroup entries containing the replacement permitted RID lists. + */ + void replaceRIDAdmissions(const std::vector& admissions); + /** * @brief Saves loaded talkgroup rules. * @param quiet Disable logging during save operation. diff --git a/src/common/network/BaseNetwork.h b/src/common/network/BaseNetwork.h index 77040d83..7a4a7570 100644 --- a/src/common/network/BaseNetwork.h +++ b/src/common/network/BaseNetwork.h @@ -450,7 +450,7 @@ namespace network * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | Destination ID | Reserved |S| Reserverd | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Mode | Service Options| Reserved | + * | Mode | Service Opts | Reserved | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * \endcode * @param mode Digital Mode. diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index c75b207e..9074ac0f 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -51,6 +51,8 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, m_ridLookup(nullptr), m_tidLookup(nullptr), m_ridAliasLookup(nullptr), + m_tgAdmissionPkt(true, "Talkgroup RID Admission"), + m_tgAdmissionStreamId(0U), m_radioAliasSyncPkt(true, "Radio Alias Sync"), m_radioAliasSyncStreamId(0U), m_salt(nullptr), diff --git a/src/common/network/Network.h b/src/common/network/Network.h index cdd5303e..4dbbb689 100644 --- a/src/common/network/Network.h +++ b/src/common/network/Network.h @@ -377,6 +377,9 @@ namespace network lookups::TalkgroupRulesLookup* m_tidLookup; lookups::RadioAliasLookup* m_ridAliasLookup; + PacketBuffer m_tgAdmissionPkt; + uint32_t m_tgAdmissionStreamId; + PacketBuffer m_radioAliasSyncPkt; uint32_t m_radioAliasSyncStreamId; diff --git a/src/common/network/RTPFNEHeader.h b/src/common/network/RTPFNEHeader.h index 926a7b50..d5a7111c 100644 --- a/src/common/network/RTPFNEHeader.h +++ b/src/common/network/RTPFNEHeader.h @@ -103,6 +103,7 @@ namespace network MASTER_SUBFUNC_BL_RID = 0x01U, //!< Blacklist RIDs MASTER_SUBFUNC_ACTIVE_TGS = 0x02U, //!< Active TGIDs MASTER_SUBFUNC_DEACTIVE_TGS = 0x03U, //!< Deactive TGIDs + MASTER_SUBFUNC_TG_ADMIT = 0x04U, //!< Talkgroup RID Admission Lists MASTER_HA_PARAMS = 0xA3U, //!< HA Parameters KEYS_UPDATE_STATUS = 0x04U, //!< Key Update Status Response diff --git a/src/common/network/packet/Master.cpp b/src/common/network/packet/Master.cpp index 9b424000..1eaf3330 100644 --- a/src/common/network/packet/Master.cpp +++ b/src/common/network/packet/Master.cpp @@ -181,6 +181,98 @@ bool Network::PacketHandler::master(Network* network, uint32_t peerId, uint32_t } break; + case NET_SUBFUNC::MASTER_SUBFUNC_TG_ADMIT: // Talkgroup RID Admission Lists + { + if (network->m_enabled && network->m_updateLookup && network->m_tidLookup != nullptr) { + if (length != FRAG_SIZE) { + LogWarning(LOG_NET, "Ignoring invalid talkgroup RID admission fragment length %d", length); + network->m_tgAdmissionPkt.clear(); + network->m_tgAdmissionStreamId = 0U; + break; + } + + if (network->m_tgAdmissionStreamId != 0U && network->m_tgAdmissionStreamId != streamId) { + network->m_tgAdmissionPkt.clear(); + } + network->m_tgAdmissionStreamId = streamId; + + uint8_t* message = nullptr; + uint32_t messageLength = 0U; + if (!network->m_tgAdmissionPkt.decode(buffer, &message, &messageLength)) { + break; + } + + UInt8Array messagePtr(message); + if (messageLength < 4U) { + LogWarning(LOG_NET, "Ignoring truncated talkgroup RID admission dataset"); + network->m_tgAdmissionStreamId = 0U; + break; + } + + const uint32_t entryCount = GET_UINT32(message, 0U); + uint32_t offs = 4U; + std::vector entries; + bool valid = true; + + for (uint32_t i = 0U; i < entryCount; i++) { + if (offs + 9U > messageLength) { + valid = false; + break; + } + + const uint32_t tgId = GET_UINT32(message, offs); + const uint8_t slot = message[offs + 4U]; + const uint32_t ridCount = GET_UINT32(message, offs + 5U); + offs += 9U; + + if (tgId == 0U || slot > 2U || ridCount == 0U || + static_cast(offs) + static_cast(ridCount) * 4U > static_cast(messageLength)) { + valid = false; + break; + } + + lookups::TalkgroupRuleGroupVoiceSource source; + lookups::TalkgroupRuleConfig config; + std::vector permittedRIDs; + permittedRIDs.reserve(ridCount); + for (uint32_t j = 0U; j < ridCount; j++, offs += 4U) { + const uint32_t rid = GET_UINT32(message, offs); + if (rid == 0U) { + valid = false; + break; + } + permittedRIDs.push_back(rid); + } + if (!valid) { + break; + } + + source.tgId(tgId); + source.tgSlot(slot); + config.permittedRIDs(permittedRIDs); + lookups::TalkgroupRuleGroupVoice entry; + entry.source(source); + entry.config(config); + entries.push_back(entry); + } + + if (!valid || offs != messageLength) { + LogWarning(LOG_NET, "Ignoring malformed talkgroup RID admission dataset"); + network->m_tgAdmissionStreamId = 0U; + break; + } + + network->m_tidLookup->replaceRIDAdmissions(entries); + LogInfoEx(LOG_NET, "Loaded RID admission lists for %u talkgroups", entries.size()); + network->m_tgAdmissionStreamId = 0U; + + if (network->m_saveLookup) { + network->m_tidLookup->commit(); + } + } + } + break; + case NET_SUBFUNC::MASTER_HA_PARAMS: // HA Parameters { if (network->m_enabled) { diff --git a/src/common/nxdn/acl/AccessControl.cpp b/src/common/nxdn/acl/AccessControl.cpp index 49544164..bc9c1692 100644 --- a/src/common/nxdn/acl/AccessControl.cpp +++ b/src/common/nxdn/acl/AccessControl.cpp @@ -75,6 +75,13 @@ bool AccessControl::validateTGId(uint32_t id) return true; } +/* Validates whether a radio ID is admitted to a talkgroup. */ + +bool AccessControl::validateTGAccess(uint32_t srcId, uint32_t dstId) +{ + return m_tidLookup->isRIDPermitted(dstId, 0U, srcId); +} + /* Helper to determine if a talkgroup ID is non-preferred. */ bool AccessControl::tgidNonPreferred(uint32_t id) diff --git a/src/common/nxdn/acl/AccessControl.h b/src/common/nxdn/acl/AccessControl.h index 470f73fa..a2368b5c 100644 --- a/src/common/nxdn/acl/AccessControl.h +++ b/src/common/nxdn/acl/AccessControl.h @@ -57,6 +57,13 @@ namespace nxdn * @returns bool True, if talkgroup ID is valid, otherwise false. */ static bool validateTGId(uint32_t id); + /** + * @brief Validates whether a radio ID is admitted to a talkgroup. + * @param srcId Source radio ID. + * @param dstId Destination talkgroup ID. + * @returns bool True if admitted, otherwise false. + */ + static bool validateTGAccess(uint32_t srcId, uint32_t dstId); /** * @brief Helper to determine if a talkgroup ID is non-preferred. diff --git a/src/common/p25/acl/AccessControl.cpp b/src/common/p25/acl/AccessControl.cpp index aa29cde0..8e000162 100644 --- a/src/common/p25/acl/AccessControl.cpp +++ b/src/common/p25/acl/AccessControl.cpp @@ -79,6 +79,13 @@ bool AccessControl::validateTGId(uint32_t id, bool allowZero) return true; } +/* Validates whether a radio ID is admitted to a talkgroup. */ + +bool AccessControl::validateTGAccess(uint32_t srcId, uint32_t dstId) +{ + return s_tidLookup->isRIDPermitted(dstId, 0U, srcId); +} + /* Helper to determine if a talkgroup ID is non-preferred. */ bool AccessControl::tgidNonPreferred(uint32_t id) diff --git a/src/common/p25/acl/AccessControl.h b/src/common/p25/acl/AccessControl.h index c303b0f1..5d35d4b7 100644 --- a/src/common/p25/acl/AccessControl.h +++ b/src/common/p25/acl/AccessControl.h @@ -58,6 +58,13 @@ namespace p25 * @returns bool True, if talkgroup ID is valid, otherwise false. */ static bool validateTGId(uint32_t id, bool allowZero = false); + /** + * @brief Validates whether a radio ID is admitted to a talkgroup. + * @param srcId Source radio ID. + * @param dstId Destination talkgroup ID. + * @returns bool True if admitted, otherwise false. + */ + static bool validateTGAccess(uint32_t srcId, uint32_t dstId); /** * @brief Helper to determine if a talkgroup ID is non-preferred. diff --git a/src/fne/network/TrafficNetwork.cpp b/src/fne/network/TrafficNetwork.cpp index b7c6252d..2054e029 100644 --- a/src/fne/network/TrafficNetwork.cpp +++ b/src/fne/network/TrafficNetwork.cpp @@ -2481,6 +2481,7 @@ void TrafficNetwork::taskMetadataUpdate(MetadataUpdateRequest* req) network->writeBlacklistRIDs(req->peerId, streamId); network->writeTGIDs(req->peerId, streamId, false); network->writeDeactiveTGIDs(req->peerId, streamId); + network->writeTGAdmissions(req->peerId, streamId); network->writeHAParameters(req->peerId, streamId, false); } @@ -3056,6 +3057,86 @@ void TrafficNetwork::writeDeactiveTGIDs(uint32_t peerId, uint32_t streamId) payload, 4U + (tgidList.size() * 5U), streamId, true); } +/* Sends the restricted RID admission lists for talkgroups visible to a peer. */ + +void TrafficNetwork::writeTGAdmissions(uint32_t peerId, uint32_t streamId) +{ + if (!m_tidLookup->sendTalkgroups()) { + return; + } + + std::vector admissions; + uint64_t payloadLength = 4U; + + for (lookups::TalkgroupRuleGroupVoice entry : m_tidLookup->groupVoice()) { + const std::vector inclusion = entry.config().inclusion(); + const std::vector exclusion = entry.config().exclusion(); + + if (!inclusion.empty() && std::find(inclusion.begin(), inclusion.end(), peerId) == inclusion.end()) { + continue; + } + if (inclusion.empty() && std::find(exclusion.begin(), exclusion.end(), peerId) != exclusion.end()) { + continue; + } + + std::vector permittedRIDs = entry.config().permittedRIDs(); + permittedRIDs.erase(std::remove(permittedRIDs.begin(), permittedRIDs.end(), 0U), permittedRIDs.end()); + std::sort(permittedRIDs.begin(), permittedRIDs.end()); + permittedRIDs.erase(std::unique(permittedRIDs.begin(), permittedRIDs.end()), permittedRIDs.end()); + if (permittedRIDs.empty()) { + continue; + } + + lookups::TalkgroupRuleConfig config = entry.config(); + config.permittedRIDs(permittedRIDs); + entry.config(config); + admissions.push_back(entry); + payloadLength += 9U + static_cast(permittedRIDs.size()) * 4U; + } + + if (payloadLength > 8192U * 1024U) { + LogError(LOG_MASTER, "PEER %u talkgroup RID admission dataset exceeds PacketBuffer limit", peerId); + return; + } + + // build dataset + DECLARE_UINT8_ARRAY(payload, static_cast(payloadLength)); + SET_UINT32(admissions.size(), payload, 0U); + + uint32_t offs = 4U; + for (const lookups::TalkgroupRuleGroupVoice& entry : admissions) { + const std::vector permittedRIDs = entry.config().permittedRIDs(); + SET_UINT32(entry.source().tgId(), payload, offs); + payload[offs + 4U] = entry.source().tgSlot(); + + SET_UINT32(permittedRIDs.size(), payload, offs + 5U); + offs += 9U; + + for (uint32_t rid : permittedRIDs) { + SET_UINT32(rid, payload, offs); + offs += 4U; + } + } + + PacketBuffer pkt(true, "Talkgroup RID Admission"); + if (!pkt.encode(payload, static_cast(payloadLength))) { + LogError(LOG_MASTER, "PEER %u talkgroup RID admission dataset failed to encode", peerId); + return; + } + + if (m_debug) { + LogInfoEx(LOG_MASTER, "PEER %u sending RID admission lists for %u talkgroups in %u blocks", peerId, + static_cast(admissions.size()), static_cast(pkt.fragments.size())); + } + + for (auto frag : pkt.fragments) { + writePeer(peerId, m_peerId, { NET_FUNC::MASTER, NET_SUBFUNC::MASTER_SUBFUNC_TG_ADMIT }, + frag.second->data, FRAG_SIZE, 0U, streamId); + Thread::sleep(60U); + } + pkt.clear(); +} + /* Helper to send the list of radio aliases to the specified peer. */ void TrafficNetwork::writeRadioAliasList(uint32_t peerId, uint32_t streamId) diff --git a/src/fne/network/TrafficNetwork.h b/src/fne/network/TrafficNetwork.h index 64283d13..a9970047 100644 --- a/src/fne/network/TrafficNetwork.h +++ b/src/fne/network/TrafficNetwork.h @@ -1111,6 +1111,16 @@ namespace network * @param streamId Stream ID for this message. */ void writeDeactiveTGIDs(uint32_t peerId, uint32_t streamId); + /** + * @brief Sends the restricted RID admission lists for talkgroups visible to a peer. + * @details The complete dataset is compressed and fragmented with PacketBuffer. Its uncompressed + * form begins with a 32-bit entry count. Each entry contains a 32-bit TGID, an 8-bit DMR slot, + * a 32-bit RID count, and that many 32-bit RIDs. Only restricted talkgroups are present; an empty + * dataset clears all received restrictions. All multi-byte fields use network byte order. + * @param peerId Peer ID. + * @param streamId Stream ID for this message. + */ + void writeTGAdmissions(uint32_t peerId, uint32_t streamId); /** * @brief Helper to send the list of radio aliases to the specified peer. * @note This doesn't have a data layout document because it is *only* sent as a packet buffered message. diff --git a/src/fne/network/callhandler/TagAnalogData.cpp b/src/fne/network/callhandler/TagAnalogData.cpp index ff3abe8d..2bf73e4b 100644 --- a/src/fne/network/callhandler/TagAnalogData.cpp +++ b/src/fne/network/callhandler/TagAnalogData.cpp @@ -917,11 +917,7 @@ bool TagAnalogData::validate(uint32_t peerId, data::NetData& data, uint32_t stre // always peers can violate the rules...hurray if (!isAlwaysPeer) { - // does the TGID have a permitted RID list? - if (tg.config().permittedRIDs().size() > 0) { - // does the transmitting RID have permission? - std::vector permittedRIDs = tg.config().permittedRIDs(); - if (std::find(permittedRIDs.begin(), permittedRIDs.end(), data.getSrcId()) == permittedRIDs.end()) { + if (!m_network->m_tidLookup->isRIDPermitted(data.getDstId(), 0U, data.getSrcId())) { // report error event to metrics TrafficNetwork::MetricsLogging::logCallErrorEvent(m_network, peerId, streamId, data.getSrcId(), data.getDstId(), std::string(DB_ERRSTR_RID_NOT_PERMITTED)); @@ -931,7 +927,6 @@ bool TagAnalogData::validate(uint32_t peerId, data::NetData& data, uint32_t stre // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); return false; - } } } } diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index 2b546336..4092b0e7 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -772,6 +772,11 @@ bool TagDMRData::processGrantReq(uint32_t srcId, uint32_t dstId, uint8_t slot, b if (!tg.config().active()) { return false; } + + // check if the source RID is permitted for this talkgroup + if (!m_network->m_tidLookup->isRIDPermitted(dstId, slot, srcId)) { + return false; + } } else { lookups::RadioId target = m_network->m_ridLookup->find(dstId); @@ -1472,11 +1477,7 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, lc::CSBK* csbk, // always peers can violate the rules...hurray if (!isAlwaysPeer) { - // does the TGID have a permitted RID list? - if (tg.config().permittedRIDs().size() > 0) { - // does the transmitting RID have permission? - std::vector permittedRIDs = tg.config().permittedRIDs(); - if (std::find(permittedRIDs.begin(), permittedRIDs.end(), data.getSrcId()) == permittedRIDs.end()) { + if (!m_network->m_tidLookup->isRIDPermitted(data.getDstId(), data.getSlotNo(), data.getSrcId())) { // report error event to metrics TrafficNetwork::MetricsLogging::logCallErrorEvent(m_network, peerId, streamId, data.getSrcId(), data.getDstId(), std::string(DB_ERRSTR_RID_NOT_PERMITTED)); @@ -1486,7 +1487,6 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, lc::CSBK* csbk, // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; - } } } } diff --git a/src/fne/network/callhandler/TagNXDNData.cpp b/src/fne/network/callhandler/TagNXDNData.cpp index 1efa590c..1f07c723 100644 --- a/src/fne/network/callhandler/TagNXDNData.cpp +++ b/src/fne/network/callhandler/TagNXDNData.cpp @@ -782,6 +782,11 @@ bool TagNXDNData::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUni if (!tg.config().active()) { return false; } + + // check if the source RID is permitted for this talkgroup + if (!m_network->m_tidLookup->isRIDPermitted(dstId, 0U, srcId)) { + return false; + } } else { lookups::RadioId target = m_network->m_ridLookup->find(dstId); @@ -801,7 +806,7 @@ bool TagNXDNData::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUni if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) continue; - write_Message_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit); + write_Message_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit); } } } @@ -1293,11 +1298,7 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u // always peers can violate the rules...hurray if (!isAlwaysPeer) { - // does the TGID have a permitted RID list? - if (tg.config().permittedRIDs().size() > 0) { - // does the transmitting RID have permission? - std::vector permittedRIDs = tg.config().permittedRIDs(); - if (std::find(permittedRIDs.begin(), permittedRIDs.end(), lc.getSrcId()) == permittedRIDs.end()) { + if (!m_network->m_tidLookup->isRIDPermitted(lc.getDstId(), 0U, lc.getSrcId())) { // report error event to metrics TrafficNetwork::MetricsLogging::logCallErrorEvent(m_network, peerId, streamId, lc.getSrcId(), lc.getDstId(), std::string(DB_ERRSTR_RID_NOT_PERMITTED)); @@ -1307,7 +1308,6 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; - } } } diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 2674a641..f934699b 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -908,6 +908,11 @@ bool TagP25Data::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUnit if (!tg.config().active()) { return false; } + + // check if the source RID is permitted for this talkgroup + if (!m_network->m_tidLookup->isRIDPermitted(dstId, 0U, srcId)) { + return false; + } } else { lookups::RadioId target = m_network->m_ridLookup->find(dstId); @@ -2117,11 +2122,7 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const // always peers can violate the rules...hurray if (!isAlwaysPeer) { - // does the TGID have a permitted RID list? - if (tg.config().permittedRIDs().size() > 0) { - // does the transmitting RID have permission? - std::vector permittedRIDs = tg.config().permittedRIDs(); - if (std::find(permittedRIDs.begin(), permittedRIDs.end(), control.getSrcId()) == permittedRIDs.end()) { + if (!m_network->m_tidLookup->isRIDPermitted(control.getDstId(), 0U, control.getSrcId())) { // report error event to metrics TrafficNetwork::MetricsLogging::logCallErrorEvent(m_network, peerId, streamId, control.getSrcId(), control.getDstId(), std::string(DB_ERRSTR_RID_NOT_PERMITTED)); @@ -2131,7 +2132,6 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; - } } } diff --git a/src/fne/network/callhandler/TagP25P2Data.cpp b/src/fne/network/callhandler/TagP25P2Data.cpp index 3d86ae53..260b0b83 100644 --- a/src/fne/network/callhandler/TagP25P2Data.cpp +++ b/src/fne/network/callhandler/TagP25P2Data.cpp @@ -1139,11 +1139,7 @@ bool TagP25P2Data::validate(uint32_t peerId, uint32_t srcId, uint32_t dstId, uin // always peers can violate the rules...hurray if (!isAlwaysPeer) { - // does the TGID have a permitted RID list? - if (tg.config().permittedRIDs().size() > 0) { - // does the transmitting RID have permission? - std::vector permittedRIDs = tg.config().permittedRIDs(); - if (std::find(permittedRIDs.begin(), permittedRIDs.end(), srcId) == permittedRIDs.end()) { + if (!m_network->m_tidLookup->isRIDPermitted(dstId, slotNo, srcId)) { // report error event to metrics TrafficNetwork::MetricsLogging::logCallErrorEvent(m_network, peerId, streamId, srcId, dstId, std::string(DB_ERRSTR_RID_NOT_PERMITTED)); @@ -1153,7 +1149,6 @@ bool TagP25P2Data::validate(uint32_t peerId, uint32_t srcId, uint32_t dstId, uin // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25_P2, NET_ICC::REJECT_TRAFFIC, dstId, slotNo); return false; - } } } } diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index 3f46795d..b6f5d9f8 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -59,8 +59,8 @@ using namespace dmr::packet; // Validate the talkgroup ID. #define VALID_TGID(_PCKT_STR, _PCKT, _SRCID, _DSTID) \ - if (!acl::AccessControl::validateTGId(0U, _DSTID)) { \ - LogWarning(LOG_RF, "DMR Slot %u, %s denial, TGID rejection, dstId = %u", m_slot->m_slotNo, _PCKT_STR.c_str(), _DSTID); \ + if (!acl::AccessControl::validateTGId(0U, _DSTID) || !acl::AccessControl::validateTGAccess(m_slot->m_slotNo, _SRCID, _DSTID)) { \ + LogWarning(LOG_RF, "DMR Slot %u, %s denial, TGID/RID admission rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, _PCKT_STR.c_str(), _SRCID, _DSTID); \ writeRF_CSBK_ACK_RSP(_SRCID, ReasonCode::TS_DENY_RSN_TGT_GROUP_NOT_VALID, 0U); \ return false; \ } @@ -931,6 +931,21 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ return true; // do not generate grant packets for $FFFF (All Call) TGID } + ::lookups::TalkgroupRuleGroupVoice groupVoice = tscc->s_tidLookup->find(dstId); + const uint8_t admissionSlot = preferredSlot != 0U ? preferredSlot : groupVoice.source().tgSlot(); + + // determine the admission slot for the talkgroup + if (grp && !acl::AccessControl::validateTGAccess(admissionSlot, srcId, dstId)) { + LogWarning(net ? LOG_NET : LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access, GRP_VOICE_CALL (Group Voice Call) denied, RID not admitted to TGID, srcId = %u, dstId = %u", + tscc->m_slotNo, srcId, dstId); + if (!net) { + writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_GROUP_NOT_VALID, 1U); + ::ActivityLog("DMR", true, "Slot %u group grant request rejection from %u to TG %u", tscc->m_slotNo, srcId, dstId); + m_slot->m_rfState = RS_RF_REJECTED; + } + return false; + } + // are we skipping checking? if (!skip) { if (m_slot->m_rfState != RS_RF_LISTENING && m_slot->m_rfState != RS_RF_DATA) { @@ -970,7 +985,6 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } if (!tscc->s_affiliations->isGranted(dstId)) { - ::lookups::TalkgroupRuleGroupVoice groupVoice = tscc->s_tidLookup->find(dstId); if (preferredSlot != 0U) { slot = preferredSlot; } diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index 074e0fb3..1059ba3e 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -163,8 +163,8 @@ bool Data::process(uint8_t* data, uint32_t len) // validate the target ID if (gi) { - if (!acl::AccessControl::validateTGId(m_slot->m_slotNo, dstId)) { - LogWarning(LOG_RF, "DMR Slot %u, DATA_HEADER denial, TGID rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId); + if (!acl::AccessControl::validateTGId(m_slot->m_slotNo, dstId) || !acl::AccessControl::validateTGAccess(m_slot->m_slotNo, srcId, dstId)) { + LogWarning(LOG_RF, "DMR Slot %u, DATA_HEADER denial, TGID/RID admission rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId); m_slot->m_rfState = RS_RF_REJECTED; return false; } diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index bb5ae3c9..3e2a88d4 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -110,9 +110,9 @@ bool Voice::process(uint8_t* data, uint32_t len) // validate target TID, if the target is a talkgroup if (flco == FLCO::GROUP) { - if (!acl::AccessControl::validateTGId(m_slot->m_slotNo, dstId)) { + if (!acl::AccessControl::validateTGId(m_slot->m_slotNo, dstId) || !acl::AccessControl::validateTGAccess(m_slot->m_slotNo, srcId, dstId)) { if (m_slot->m_data->m_lastRejectId == 0U || m_slot->m_data->m_lastRejectId == dstId) { - LogWarning(LOG_RF, "DMR Slot %u, VOICE_LC_HEADER denial, TGID rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId); + LogWarning(LOG_RF, "DMR Slot %u, VOICE_LC_HEADER denial, TGID/RID admission rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, srcId, dstId); ::ActivityLog("DMR", true, "Slot %u RF voice rejection from %u to TG %u ", m_slot->m_slotNo, srcId, dstId); } diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index 7baf5b53..99fadb7d 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -63,8 +63,8 @@ using namespace nxdn::packet; // Validate the talkgroup ID. #define VALID_TGID(_PCKT_STR, _PCKT, _DSTID, _SRCID, _RSN) \ - if (!acl::AccessControl::validateTGId(_DSTID)) { \ - LogWarning(LOG_RF, "NXDN, %s denial, TGID rejection, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ + if (!acl::AccessControl::validateTGId(_DSTID) || !acl::AccessControl::validateTGAccess(_SRCID, _DSTID)) { \ + LogWarning(LOG_RF, "NXDN, %s denial, TGID/RID admission rejection, srcId = %u, dstId = %u", _PCKT_STR.c_str(), _SRCID, _DSTID); \ writeRF_Message_Deny(0U, _SRCID, _RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ @@ -577,6 +577,18 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, if (update) rcch->setMessageType(MessageType::RCCH_VCALL_ASSGN_DUP); + // check if the group access is valid for the source and destination IDs + if (grp && !acl::AccessControl::validateTGAccess(srcId, dstId)) { + LogWarning(net ? LOG_NET : LOG_RF, "NXDN, %s denied, RID not admitted to TGID, srcId = %u, dstId = %u", + rcch->toString().c_str(), srcId, dstId); + if (!net) { + writeRF_Message_Deny(0U, srcId, CauseResponse::VD_GRP_NOT_PERM, MessageType::RTCH_VCALL); + ::ActivityLog("NXDN", true, "group grant request rejection from %u to TG %u", srcId, dstId); + m_nxdn->m_rfState = RS_RF_REJECTED; + } + return false; + } + // are we skipping checking? if (!skip) { if (m_nxdn->m_rfState != RS_RF_LISTENING && m_nxdn->m_rfState != RS_RF_DATA) { diff --git a/src/host/nxdn/packet/Data.cpp b/src/host/nxdn/packet/Data.cpp index 41d3d55c..eda97a4f 100644 --- a/src/host/nxdn/packet/Data.cpp +++ b/src/host/nxdn/packet/Data.cpp @@ -131,9 +131,9 @@ bool Data::process(ChOption::E option, uint8_t* data, uint32_t len) } } else { - if (!acl::AccessControl::validateTGId(dstId)) { + if (!acl::AccessControl::validateTGId(dstId) || !acl::AccessControl::validateTGAccess(srcId, dstId)) { if (m_lastRejectId == 0 || m_lastRejectId != dstId) { - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, TGID rejection, dstId = %u", dstId); + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, TGID/RID admission rejection, srcId = %u, dstId = %u", srcId, dstId); ::ActivityLog("NXDN", true, "RF data rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); m_lastRejectId = dstId; } diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index f840ae8a..e873c0f8 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -141,9 +141,9 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u } } else { - if (!acl::AccessControl::validateTGId(dstId)) { + if (!acl::AccessControl::validateTGId(dstId) || !acl::AccessControl::validateTGAccess(srcId, dstId)) { if (m_lastRejectId == 0 || m_lastRejectId != dstId) { - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", dstId); + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID/RID admission rejection, srcId = %u, dstId = %u", srcId, dstId); ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); m_lastRejectId = dstId; } diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index b73eef28..91ab483e 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -71,8 +71,8 @@ using namespace p25::packet; // Validate the talkgroup ID. #define VALID_TGID(_PCKT_STR, _PCKT, _SRCID, _DSTID) \ - if (!acl::AccessControl::validateTGId(_DSTID)) { \ - LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, TGID rejection, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ + if (!acl::AccessControl::validateTGId(_DSTID) || !acl::AccessControl::validateTGAccess(_SRCID, _DSTID)) { \ + LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, TGID/RID admission rejection, srcId = %u, dstId = %u", _PCKT_STR.c_str(), _SRCID, _DSTID); \ writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_TGT_GROUP_NOT_VALID, _PCKT); \ m_p25->m_rfState = RS_RF_REJECTED; \ return false; \ @@ -785,18 +785,9 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetAnnounceGroup() != 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 + // validate the talkgroup ID and RID admission before using the affiliation state VALID_TGID(tsbk->toString(true), TSBKO::ISP_GRP_AFF_Q_RSP, srcId, dstId); // check if the target group ID is non-preferred @@ -808,6 +799,15 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetAnnounceGroup() != 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; + } + writeRF_TSDU_ACK_FNE(srcId, TSBKO::ISP_GRP_AFF_Q_RSP, false, true); m_p25->m_affiliations->touchUnitReg(srcId); @@ -2631,6 +2631,17 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ return true; // do not generate grant packets for $FFFF (All Call) TGID } + // TG admission applies only to group calls; U2U and telephone interconnect calls use RID validation + if (grp && !acl::AccessControl::validateTGAccess(srcId, dstId)) { + LogWarning(net ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, RID not admitted to TGID, srcId = %u, dstId = %u", srcId, dstId); + if (!net) { + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_REQ_UNIT_NOT_AUTH, TSBKO::IOSP_GRP_VCH, true, true); + ::ActivityLog("P25", true, "group grant request from %u to TG %u denied", srcId, dstId); + m_p25->m_rfState = RS_RF_REJECTED; + } + return false; + } + // are we skipping checking? if (!skip) { if (m_p25->m_rfState != RS_RF_LISTENING && m_p25->m_rfState != RS_RF_DATA) { @@ -3480,6 +3491,14 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI noNet = true; } + // check if the source RID has access to the specified talkgroup ID + if (!acl::AccessControl::validateTGAccess(srcId, dstId)) { + LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not admitted to TGID, srcId = %u, dstId = %u", iosp->toString().c_str(), srcId, dstId); + ::ActivityLog("P25", true, "group affiliation request from %u to TG %u denied", srcId, dstId); + iosp->setResponse(ResponseCode::DENY); + noNet = true; + } + // deny affiliation if the TG is non-preferred on this site/CC if (acl::AccessControl::tgidNonPreferred(dstId)) { LogWarning(LOG_RF, P25_TSDU_STR ", %s non-preferred on this site, TGID rejection, dstId = %u", iosp->toString().c_str(), dstId); @@ -3663,6 +3682,14 @@ bool ControlSignaling::writeRF_TSDU_Loc_Reg_Rsp(uint32_t srcId, uint32_t dstId, noNet = true; } + // check if the source RID has access to the specified talkgroup ID + if (!acl::AccessControl::validateTGAccess(srcId, dstId)) { + LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not admitted to TGID, srcId = %u, dstId = %u", osp->toString().c_str(), srcId, dstId); + ::ActivityLog("P25", true, "location registration request from %u to TG %u denied", srcId, dstId); + osp->setResponse(ResponseCode::DENY); + noNet = true; + } + // deny affiliation if the TG is non-preferred on this site/CC if (acl::AccessControl::tgidNonPreferred(dstId)) { LogWarning(LOG_RF, P25_TSDU_STR ", %s non-preferred on this site, TGID rejection, dstId = %u", osp->toString().c_str(), dstId); diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index 2433a8a6..a7974a3e 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -329,9 +329,9 @@ bool Voice::process(uint8_t* data, uint32_t len) } else { // validate the target ID, if the target is a talkgroup - if (!acl::AccessControl::validateTGId(dstId, m_p25->m_forceAllowTG0)) { + if (!acl::AccessControl::validateTGId(dstId, m_p25->m_forceAllowTG0) || !acl::AccessControl::validateTGAccess(srcId, dstId)) { if (m_lastRejectId == 0 || m_lastRejectId != dstId) { - LogWarning(LOG_RF, P25_HDU_STR " denial, TGID rejection, dstId = %u", dstId); + LogWarning(LOG_RF, P25_HDU_STR " denial, TGID/RID admission rejection, srcId = %u, dstId = %u", srcId, dstId); if (m_p25->m_enableControl) { m_p25->m_control->writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::IOSP_GRP_VCH, true, true); } diff --git a/src/host/p25/phase2/Slot.cpp b/src/host/p25/phase2/Slot.cpp index ac09df75..ae9d3833 100644 --- a/src/host/p25/phase2/Slot.cpp +++ b/src/host/p25/phase2/Slot.cpp @@ -988,7 +988,8 @@ bool Slot::validateRFCall(uint32_t srcId, uint32_t dstId, bool group) const bool aclEnabled = s_ridLookup != nullptr && s_tidLookup != nullptr; const bool validSrc = !aclEnabled || acl::AccessControl::validateSrcId(srcId); - const bool validDst = !aclEnabled || (group ? acl::AccessControl::validateTGId(dstId) : + const bool validDst = !aclEnabled || (group ? + (acl::AccessControl::validateTGId(dstId) && acl::AccessControl::validateTGAccess(srcId, dstId)) : acl::AccessControl::validateSrcId(dstId)); if (validSrc && validDst) return true; diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 04e59374..9c567467 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -29,6 +29,7 @@ #include #include #include +#include extern network::NetRPC* g_RPC; #include "host/dmr/Control.h" @@ -38,6 +39,29 @@ extern network::NetRPC* g_RPC; // --------------------------------------------------------------------------- namespace { +/** + * @brief Builds one DMR talkgroup rule for controller admission tests. + * @param tgId Talkgroup ID. + * @param slot DMR slot. + * @param permittedRIDs RIDs admitted to the talkgroup. + * @returns TalkgroupRuleGroupVoice Test rule. + */ +lookups::TalkgroupRuleGroupVoice admissionRule(uint32_t tgId, uint8_t slot, const std::vector& permittedRIDs) +{ + lookups::TalkgroupRuleGroupVoiceSource source; + source.tgId(tgId); + source.tgSlot(slot); + + lookups::TalkgroupRuleConfig config; + config.active(true); + config.permittedRIDs(permittedRIDs); + + lookups::TalkgroupRuleGroupVoice rule; + rule.source(source); + rule.config(config); + return rule; +} + /** * @brief Helper function to calculate the number of timer ticks needed to expire a given timer. * @param timer The timer for which to calculate the expiration ticks. @@ -396,13 +420,14 @@ public: * @brief Initializes a new instance of the DMRHostHarness class. * @param authoritative Indicates whether the host is authoritative. */ - explicit DMRHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, bool disallowUnitToUnit = false) : + explicit DMRHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, + bool disallowUnitToUnit = false, bool tgAcl = false) : m_rpc("127.0.0.1", 1U, 0U, "test", false), m_modem(new DMRTestModemPort(), false, false, false, false, false, false, 0U, 0U, 0U, 4096U, 4096U, 1024U, true, true, false, false, false, false), m_chLookup(), m_ridLookup("", 0U, false, false), - m_tidLookup("", 0U, false, false), + m_tidLookup("", 0U, tgAcl, false), m_idenLookup("", 0U), m_rssiMapper(), m_network(withNetwork ? new DMRTestNetwork(networkLocalPort, networkPeerId, disallowUnitToUnit) : nullptr), @@ -825,6 +850,35 @@ TEST_CASE("DMR processFrame accepts a synthetic RF voice call on the targeted sl REQUIRE(harness.m_control->getLastDstId(1U) == 2001U); } +TEST_CASE("DMR controller enforces TG admission without applying it to private calls", "[dmr][host][control][rf][talkgroup-admission]") +{ + SECTION("restricted group traffic is rejected") { + DMRHostHarness harness(true, false, 0U, 1U, false, true); + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + harness.m_tidLookup.addEntry(admissionRule(2001U, 1U, { 1002U })); + + REQUIRE_FALSE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1001U, 2001U, true)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_REJECTED); + } + + SECTION("admitted group traffic is accepted") { + DMRHostHarness harness(true, false, 0U, 1U, false, true); + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + harness.m_tidLookup.addEntry(admissionRule(2001U, 1U, { 1001U })); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1001U, 2001U, true)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_AUDIO); + } + + SECTION("private traffic bypasses talkgroup admission") { + DMRHostHarness harness(true, false, 0U, 1U, false, true); + dmr::Slot* slot = HostTestHooks::dmrSlot1(*harness.m_control); + + REQUIRE(HostTestHooks::dmrStartRFVoiceCall(*slot, 1001U, 2001U, false)); + REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_AUDIO); + } +} + TEST_CASE("DMR Tier III re-key on a retained payload channel recreates its grant", "[dmr][tier3][host][control][rf]") { DMRHostHarness harness; diff --git a/tests/nxdn/HostControl_Tests.cpp b/tests/nxdn/HostControl_Tests.cpp index 37122316..59b11662 100644 --- a/tests/nxdn/HostControl_Tests.cpp +++ b/tests/nxdn/HostControl_Tests.cpp @@ -30,6 +30,7 @@ #include #include #include +#include extern network::NetRPC* g_RPC; #include "host/nxdn/Control.h" @@ -39,6 +40,28 @@ extern network::NetRPC* g_RPC; // --------------------------------------------------------------------------- namespace { +/** + * @brief Builds one NXDN talkgroup rule for controller admission tests. + * @param tgId Talkgroup ID. + * @param permittedRIDs RIDs admitted to the talkgroup. + * @returns TalkgroupRuleGroupVoice Test rule. + */ +lookups::TalkgroupRuleGroupVoice admissionRule(uint32_t tgId, const std::vector& permittedRIDs) +{ + lookups::TalkgroupRuleGroupVoiceSource source; + source.tgId(tgId); + source.tgSlot(0U); + + lookups::TalkgroupRuleConfig config; + config.active(true); + config.permittedRIDs(permittedRIDs); + + lookups::TalkgroupRuleGroupVoice rule; + rule.source(source); + rule.config(config); + return rule; +} + /** * @brief Helper function to calculate the number of timer ticks needed to expire a given timer. * @param timer The timer for which to calculate the expiration ticks. @@ -414,13 +437,14 @@ public: * @brief Initializes a new instance of the NXDNHostHarness class. * @param authoritative Indicates whether the host is authoritative. */ - explicit NXDNHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, bool disallowUnitToUnit = false) : + explicit NXDNHostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, + bool disallowUnitToUnit = false, bool tgAcl = false) : m_rpc("127.0.0.1", 1U, 0U, "test", false), m_modem(new NXDNTestModemPort(), false, false, false, false, false, false, 0U, 0U, 0U, 1024U, 4096U, 1024U, true, true, false, false, false, false), m_chLookup(), m_ridLookup("", 0U, false, false), - m_tidLookup("", 0U, false, false), + m_tidLookup("", 0U, tgAcl, false), m_idenLookup("", 0U), m_rssiMapper(), m_network(withNetwork ? new NXDNTestNetwork(networkLocalPort, networkPeerId, disallowUnitToUnit) : nullptr), @@ -967,6 +991,32 @@ TEST_CASE("NXDN processFrame accepts a synthetic RF voice call", "[nxdn][host][c REQUIRE(harness.m_control->getLastDstId() == 2001U); } +TEST_CASE("NXDN controller enforces TG admission without applying it to individual calls", "[nxdn][host][control][rf][talkgroup-admission]") +{ + SECTION("restricted group traffic is rejected") { + NXDNHostHarness harness(true, false, 0U, 1U, false, true); + harness.m_tidLookup.addEntry(admissionRule(2001U, { 1002U })); + + REQUIRE_FALSE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U, true)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_REJECTED); + } + + SECTION("admitted group traffic is accepted") { + NXDNHostHarness harness(true, false, 0U, 1U, false, true); + harness.m_tidLookup.addEntry(admissionRule(2001U, { 1001U })); + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U, true)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); + } + + SECTION("individual traffic bypasses talkgroup admission") { + NXDNHostHarness harness(true, false, 0U, 1U, false, true); + + REQUIRE(HostTestHooks::nxdnStartRFCall(*harness.m_control, 1001U, 2001U, false)); + REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_AUDIO); + } +} + TEST_CASE("NXDN rfTGHang expiry ends active RF call and returns to listening", "[nxdn][host][control][rf]") { NXDNHostHarness harness; diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 8ad6a6e9..9c765155 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -27,6 +27,7 @@ #include #include #include +#include extern network::NetRPC* g_RPC; #include "host/p25/Control.h" @@ -72,6 +73,28 @@ static const uint8_t CAL_P25_LDU2_1K[p25::defines::P25_LDU_FRAME_LENGTH_BYTES] = // --------------------------------------------------------------------------- namespace { +/** + * @brief Builds one P25 talkgroup rule for controller admission tests. + * @param tgId Talkgroup ID. + * @param permittedRIDs RIDs admitted to the talkgroup. + * @returns TalkgroupRuleGroupVoice Test rule. + */ +lookups::TalkgroupRuleGroupVoice admissionRule(uint32_t tgId, const std::vector& permittedRIDs) +{ + lookups::TalkgroupRuleGroupVoiceSource source; + source.tgId(tgId); + source.tgSlot(0U); + + lookups::TalkgroupRuleConfig config; + config.active(true); + config.permittedRIDs(permittedRIDs); + + lookups::TalkgroupRuleGroupVoice rule; + rule.source(source); + rule.config(config); + return rule; +} + /** * @brief Helper function to calculate the number of timer ticks needed to expire a given timer. * @param timer The timer for which to calculate the expiration ticks. @@ -512,13 +535,14 @@ public: * @brief Initializes a new instance of the P25HostHarness class. * @param authoritative Indicates whether the host is authoritative. */ - explicit P25HostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, bool disallowUnitToUnit = false) : + explicit P25HostHarness(bool authoritative = true, bool withNetwork = false, uint16_t networkLocalPort = 0U, uint32_t networkPeerId = 1U, + bool disallowUnitToUnit = false, bool tgAcl = false) : m_rpc("127.0.0.1", 1U, 0U, "test", false), m_modem(new P25TestModemPort(), false, false, false, false, false, false, 0U, 0U, 0U, 1024U, 4096U, 1024U, true, true, false, false, false, false), m_chLookup(), m_ridLookup("", 0U, false, false), - m_tidLookup("", 0U, false, false), + m_tidLookup("", 0U, tgAcl, false), m_idenLookup("", 0U), m_rssiMapper(), m_network(withNetwork ? new P25TestNetwork(networkLocalPort, networkPeerId, disallowUnitToUnit) : nullptr), @@ -1710,6 +1734,49 @@ TEST_CASE("P25 processFrame accepts calibration-based RF LDU1 frame", "[p25][hos REQUIRE(harness.m_control->getLastDstId() == 1U); } +TEST_CASE("P25 controller enforces TG admission without applying it to U2U", "[p25][host][control][rf][talkgroup-admission]") +{ + SECTION("restricted group traffic is rejected") { + P25HostHarness harness(true, false, 0U, 1U, false, true); + harness.m_tidLookup.addEntry(admissionRule(1U, { 2U })); + + uint8_t frame[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U]; + buildP25RFFrame(CAL_P25_LDU1_1K, frame); + HostTestHooks::p25StampRFFrameNID(*harness.m_control, frame, p25::defines::DUID::LDU1); + + REQUIRE_FALSE(harness.m_control->processFrame(frame, sizeof(frame))); + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_REJECTED); + } + + SECTION("admitted group traffic is accepted") { + P25HostHarness harness(true, false, 0U, 1U, false, true); + harness.m_tidLookup.addEntry(admissionRule(1U, { 1U })); + + uint8_t frame[p25::defines::P25_LDU_FRAME_LENGTH_BYTES + 2U]; + buildP25RFFrame(CAL_P25_LDU1_1K, frame); + HostTestHooks::p25StampRFFrameNID(*harness.m_control, frame, p25::defines::DUID::LDU1); + + REQUIRE(harness.m_control->processFrame(frame, sizeof(frame))); + REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_AUDIO); + } + + SECTION("U2U grant bypasses talkgroup admission") { + P25HostHarness harness(true, false, 0U, 1U, false, true); + const uint32_t channelNo = 1U; + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, false); + HostTestHooks::p25RegisterUnit(*harness.m_control, 1001U); + HostTestHooks::p25RegisterUnit(*harness.m_control, 2001U); + + uint8_t request[p25::defines::P25_TSBK_LENGTH_BYTES]; + buildUUVchRequest(1001U, 2001U, request); + + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, request)); + REQUIRE(HostTestHooks::p25IsU2UGranted(*harness.m_control, 1001U, 2001U)); + } +} + TEST_CASE("P25 processFrame accepts calibration-based RF LDU2 frame after LDU1", "[p25][host][rf]") { P25HostHarness harness;