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;

pull/134/head
Bryan Biedenkapp 2 days ago
parent 88cb85cc8f
commit 5177685e48

@ -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 @@
}
}
}
}
}

@ -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

@ -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<uint8_t>(slotNo), srcId);
}
/* Helper to determine if a talkgroup ID is non-preferred. */
bool AccessControl::tgidNonPreferred(uint32_t slotNo, uint32_t id)

@ -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.

@ -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<uint32_t> 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<TalkgroupRuleGroupVoice>& admissions)
{
std::lock_guard<std::mutex> 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<uint32_t>());
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)

@ -432,7 +432,7 @@ namespace lookups
DECLARE_PROPERTY_PLAIN(std::vector<uint32_t>, 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<uint32_t>, 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<TalkgroupRuleGroupVoice>& admissions);
/**
* @brief Saves loaded talkgroup rules.
* @param quiet Disable logging during save operation.

@ -450,7 +450,7 @@ namespace network
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Destination ID | Reserved |S| Reserverd |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Mode | Service Options| Reserved |
* | Mode | Service Opts | Reserved |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* \endcode
* @param mode Digital Mode.

@ -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),

@ -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;

@ -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

@ -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<lookups::TalkgroupRuleGroupVoice> 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<uint64_t>(offs) + static_cast<uint64_t>(ridCount) * 4U > static_cast<uint64_t>(messageLength)) {
valid = false;
break;
}
lookups::TalkgroupRuleGroupVoiceSource source;
lookups::TalkgroupRuleConfig config;
std::vector<uint32_t> 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) {

@ -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)

@ -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.

@ -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)

@ -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.

@ -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<lookups::TalkgroupRuleGroupVoice> admissions;
uint64_t payloadLength = 4U;
for (lookups::TalkgroupRuleGroupVoice entry : m_tidLookup->groupVoice()) {
const std::vector<uint32_t> inclusion = entry.config().inclusion();
const std::vector<uint32_t> 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<uint32_t> 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<uint64_t>(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<uint32_t>(payloadLength));
SET_UINT32(admissions.size(), payload, 0U);
uint32_t offs = 4U;
for (const lookups::TalkgroupRuleGroupVoice& entry : admissions) {
const std::vector<uint32_t> 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<uint32_t>(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<uint32_t>(admissions.size()), static_cast<uint32_t>(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)

@ -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.

@ -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<uint32_t> 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;
}
}
}
}

@ -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<uint32_t> 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;
}
}
}
}

@ -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<uint32_t> 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;
}
}
}

@ -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<uint32_t> 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;
}
}
}

@ -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<uint32_t> 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;
}
}
}
}

@ -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;
}

@ -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;
}

@ -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);
}

@ -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) {

@ -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;
}

@ -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;
}

@ -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_ptr<lc::
m_groupAffQueryTransactions.erase(query);
// check if the announced group matches the expected announcement group and if the source is affiliated with the target group
if (isp->getAnnounceGroup() != m_announcementGroup || !m_p25->m_affiliations->isGroupAff(srcId, dstId)) {
LogWarning(LOG_RF, P25_TSDU_STR ", affiliation query mismatch, srcId = %u, dstId = %u, anncId = %u", srcId, dstId, isp->getAnnounceGroup());
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::ISP_GRP_AFF_Q_RSP);
writeRF_TSDU_U_Reg_Cmd(srcId);
m_p25->m_rfState = RS_RF_REJECTED;
return false;
}
// preserve the normal affiliation path's null-group exception
if (dstId != 0U) {
// validate the talkgroup ID
// 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_ptr<lc::
}
}
// check if the announced group matches the expected announcement group and if the source is affiliated with the target group
if (isp->getAnnounceGroup() != m_announcementGroup || !m_p25->m_affiliations->isGroupAff(srcId, dstId)) {
LogWarning(LOG_RF, P25_TSDU_STR ", affiliation query mismatch, srcId = %u, dstId = %u, anncId = %u", srcId, dstId, isp->getAnnounceGroup());
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::ISP_GRP_AFF_Q_RSP);
writeRF_TSDU_U_Reg_Cmd(srcId);
m_p25->m_rfState = RS_RF_REJECTED;
return false;
}
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);

@ -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);
}

@ -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;

@ -29,6 +29,7 @@
#include <chrono>
#include <cstring>
#include <thread>
#include <vector>
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<uint32_t>& 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;

@ -30,6 +30,7 @@
#include <chrono>
#include <cstring>
#include <thread>
#include <vector>
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<uint32_t>& 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;

@ -27,6 +27,7 @@
#include <chrono>
#include <cstring>
#include <thread>
#include <vector>
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<uint32_t>& 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;

Loading…
Cancel
Save

Powered by TurnKey Linux.