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;