From 27ed5fc15f007aad175458504094013d76ba5f9a Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Thu, 25 Jun 2026 11:44:00 -0400 Subject: [PATCH] remove m_peers lock around peer update in erasePeer(); add hacky fuzzy matching logic to allow up to 3 bit errors for a CRC-CCITT162 (this better helps some P25 conditions where a TSBK may fault with a CRC-CCITT error, usually while enc enabled); implement support for selectively setting talkgroup encryption modes, selectable, strapped or clear (note due to how both DVMs implementation and how the protocols work there are some limitations, for example in DMR the FNE cannot reliably prevent calls from flowing to the network based on the selective talkgroup encryption modes, NXDN cannot reliably prevent a call grant because the encryption state isn't known at grant time, dvmhost provides no network-sourced traffic protection for clear or strapped as the assumption is the FNE will not allow a call to flow to begin with); --- configs/talkgroup_rules.example.yml | 12 ++++ src/common/edac/CRC.cpp | 24 +++++++- src/common/edac/CRC.h | 3 +- src/common/lookups/TalkgroupRulesLookup.cpp | 10 +++- src/common/lookups/TalkgroupRulesLookup.h | 27 ++++++++- src/common/network/Network.cpp | 15 ++++- src/common/p25/lc/TSBK.cpp | 4 +- src/common/p25/lc/tsbk/TSBKFactory.cpp | 4 +- src/fne/network/TrafficNetwork.cpp | 12 +++- src/fne/network/TrafficNetwork.h | 4 +- src/fne/network/callhandler/TagAnalogData.cpp | 24 ++++++++ src/fne/network/callhandler/TagDMRData.cpp | 8 +++ src/fne/network/callhandler/TagNXDNData.cpp | 54 +++++++++++++++++ src/fne/network/callhandler/TagP25Data.cpp | 54 +++++++++++++++++ src/fne/restapi/RESTAPI.cpp | 8 +++ src/host/dmr/Slot.cpp | 4 +- src/host/dmr/packet/ControlSignaling.cpp | 16 +++++ src/host/dmr/packet/Voice.cpp | 59 ++++++++++++++++++- src/host/nxdn/packet/ControlSignaling.cpp | 25 +++++++- src/host/nxdn/packet/Voice.cpp | 17 ++++++ src/host/p25/packet/ControlSignaling.cpp | 20 +++++++ src/host/p25/packet/Voice.cpp | 30 ++++++++++ 22 files changed, 412 insertions(+), 22 deletions(-) diff --git a/configs/talkgroup_rules.example.yml b/configs/talkgroup_rules.example.yml index 105cd693..555a69b6 100644 --- a/configs/talkgroup_rules.example.yml +++ b/configs/talkgroup_rules.example.yml @@ -39,6 +39,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). inclusion: [] # List of peer IDs excluded for this talkgroup (peers listed here will be ignored for traffic). @@ -74,6 +76,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # Flag indicating whether or not this talkgroup is a parrot talkgroup. parrot: true # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). @@ -111,6 +115,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # Flag indicating whether or not this talkgroup is a parrot talkgroup. parrot: true # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). @@ -154,6 +160,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). inclusion: [] # List of peer IDs excluded for this talkgroup (peers listed here will be ignored for traffic). @@ -189,6 +197,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). inclusion: [] # List of peer IDs excluded for this talkgroup (peers listed here will be ignored for traffic). @@ -224,6 +234,8 @@ groupVoice: active: true # Flag indicating whether this talkgroup will only repeat with affiliations. affiliated: false + # Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + strapping: 0 # List of peer IDs included for this talkgroup (peers listed here will be selected for traffic). inclusion: [] # List of peer IDs excluded for this talkgroup (peers listed here will be ignored for traffic). diff --git a/src/common/edac/CRC.cpp b/src/common/edac/CRC.cpp index 9b483ee2..cc577c9d 100644 --- a/src/common/edac/CRC.cpp +++ b/src/common/edac/CRC.cpp @@ -183,7 +183,7 @@ void CRC::encodeFiveBit(const bool* in, uint32_t& tcrc) /* Check 16-bit CRC CCITT-162. */ -bool CRC::checkCCITT162(const uint8_t *in, uint32_t length) +bool CRC::checkCCITT162(const uint8_t *in, uint32_t length, bool fuzzy) { assert(in != nullptr); assert(length > 2U); @@ -200,11 +200,29 @@ bool CRC::checkCCITT162(const uint8_t *in, uint32_t length) crc16 = ~crc16; -#if DEBUG_CRC_CHECK uint16_t inCrc = (in[length - 2U] << 8) | (in[length - 1U] << 0); - LogDebugEx(LOG_HOST, "CRC::checkCCITT162()", "crc = $%04X, in = $%04X, len = %u", crc16, inCrc, length); +#if DEBUG_CRC_CHECK + LogDebugEx(LOG_HOST, "CRC::checkCCITT162()", "crc = $%04X, in = $%04X, len = %u, fuzzy = %u", crc16, inCrc, length, fuzzy); #endif + // if fuzzy checking is enabled, allow for up to 3 bit errors in the CRC comparison + if (fuzzy) { + uint16_t diff = crc16 ^ inCrc; + if (diff == 0U) + return true; + else { + uint8_t count = 0U; + while (diff) { + diff &= (diff - 1U); // clear the least significant bit set + count++; + } + + if (count <= 3U) + LogWarning(LOG_HOST, "CRC-CCITT162, fuzzy CRC match: %u bit errors", count); + return count <= 3U; + } + } + return crc8[0U] == in[length - 1U] && crc8[1U] == in[length - 2U]; } diff --git a/src/common/edac/CRC.h b/src/common/edac/CRC.h index 8007dd29..e16e32b9 100644 --- a/src/common/edac/CRC.h +++ b/src/common/edac/CRC.h @@ -52,9 +52,10 @@ namespace edac * * @param[in] in Input byte array. * @param length Length of byte array. + * @param fuzzy If true, a fuzzy CRC check is performed, which allows for a three bit error in the CRC. * @returns bool True, if CRC is valid, otherwise false. */ - static bool checkCCITT162(const uint8_t* in, uint32_t length); + static bool checkCCITT162(const uint8_t* in, uint32_t length, bool fuzzy = false); /** * @brief Encode 16-bit CRC CCITT-162. * diff --git a/src/common/lookups/TalkgroupRulesLookup.cpp b/src/common/lookups/TalkgroupRulesLookup.cpp index 53ff6f91..b19ba67f 100644 --- a/src/common/lookups/TalkgroupRulesLookup.cpp +++ b/src/common/lookups/TalkgroupRulesLookup.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL * Copyright (C) 2024 Patrick McDonnell, W3AXL * */ @@ -132,7 +132,8 @@ void TalkgroupRulesLookup::clear() /* Adds a new entry to the lookup table by the specified unique ID. */ -void TalkgroupRulesLookup::addEntry(uint32_t id, uint8_t slot, bool enabled, bool affiliated, bool nonPreferred) +void TalkgroupRulesLookup::addEntry(uint32_t id, uint8_t slot, bool enabled, bool affiliated, bool nonPreferred, + uint8_t strapping) { TalkgroupRuleGroupVoiceSource source; TalkgroupRuleConfig config; @@ -141,6 +142,7 @@ void TalkgroupRulesLookup::addEntry(uint32_t id, uint8_t slot, bool enabled, boo config.active(enabled); config.affiliated(affiliated); config.nonPreferred(nonPreferred); + config.strapping(strapping); __LOCK_TABLE(); @@ -161,6 +163,7 @@ void TalkgroupRulesLookup::addEntry(uint32_t id, uint8_t slot, bool enabled, boo config.active(enabled); config.affiliated(affiliated); config.nonPreferred(nonPreferred); + config.strapping(strapping); TalkgroupRuleGroupVoice entry = *it; entry.config(config); @@ -353,6 +356,7 @@ bool TalkgroupRulesLookup::load() bool active = groupVoice.config().active(); bool parrot = groupVoice.config().parrot(); bool affil = groupVoice.config().affiliated(); + uint8_t strapping = groupVoice.config().strapping(); uint32_t incCount = groupVoice.config().inclusion().size(); uint32_t excCount = groupVoice.config().exclusion().size(); @@ -369,7 +373,7 @@ bool TalkgroupRulesLookup::load() ::LogWarning(LOG_HOST, "Talkgroup (%s) is marked as affiliation required and has a defined always send list! Always send peers take rule precedence and defined peers will always receive traffic.", groupName.c_str()); } - ::LogInfoEx(LOG_HOST, "Talkgroup NAME: %s SRC_TGID: %u SRC_TS: %u ACTIVE: %u PARROT: %u AFFILIATED: %u INCLUSIONS: %u EXCLUSIONS: %u REWRITES: %u ALWAYS: %u PREFERRED: %u PERMITTED RIDS: %u", groupName.c_str(), tgId, tgSlot, active, parrot, affil, incCount, excCount, rewrCount, alwyCount, prefCount, permRIDCount); + ::LogInfoEx(LOG_HOST, "Talkgroup NAME: %s SRC_TGID: %u SRC_TS: %u ACTIVE: %u PARROT: %u AFFILIATED: %u STRAPPING: %u INCLUSIONS: %u EXCLUSIONS: %u REWRITES: %u ALWAYS: %u PREFERRED: %u PERMITTED RIDS: %u", groupName.c_str(), tgId, tgSlot, active, parrot, affil, strapping, incCount, excCount, rewrCount, alwyCount, prefCount, permRIDCount); } } diff --git a/src/common/lookups/TalkgroupRulesLookup.h b/src/common/lookups/TalkgroupRulesLookup.h index 6160bacc..74dfc55a 100644 --- a/src/common/lookups/TalkgroupRulesLookup.h +++ b/src/common/lookups/TalkgroupRulesLookup.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL * Copyright (C) 2024 Patrick McDonnell, W3AXL * */ @@ -33,6 +33,17 @@ namespace lookups { + // --------------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------------- + + /** @brief Talkgroup Strapping Selectable */ + const uint8_t TG_STRAPPING_SELECTABLE = 0U; + /** @brief Talkgroup Strapping Strapped */ + const uint8_t TG_STRAPPING_STRAPPED = 1U; + /** @brief Talkgroup Strapping Clear */ + const uint8_t TG_STRAPPING_CLEAR = 2U; + // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- @@ -187,6 +198,7 @@ namespace lookups TalkgroupRuleConfig() : m_active(false), m_affiliated(false), + m_strapping(TG_STRAPPING_SELECTABLE), m_parrot(false), m_inclusion(), m_exclusion(), @@ -208,6 +220,7 @@ namespace lookups m_active = node["active"].as(false); m_affiliated = node["affiliated"].as(false); m_parrot = node["parrot"].as(false); + m_strapping = (uint8_t)node["strapping"].as(TG_STRAPPING_SELECTABLE); yaml::Node& inclusionList = node["inclusion"]; if (inclusionList.size() > 0U) { @@ -267,6 +280,7 @@ namespace lookups if (this != &data) { m_active = data.m_active; m_affiliated = data.m_affiliated; + m_strapping = data.m_strapping; m_parrot = data.m_parrot; m_inclusion = data.m_inclusion; m_exclusion = data.m_exclusion; @@ -320,8 +334,9 @@ namespace lookups // We have to convert the bools back to strings to pass to the yaml node node["active"] = __BOOL_STR(m_active); node["affiliated"] = __BOOL_STR(m_affiliated); + node["strapping"] = __INT_STR(m_strapping); node["parrot"] = __BOOL_STR(m_parrot); - + // Get the lists yaml::Node inclusionList; if (m_inclusion.size() > 0U) { @@ -387,6 +402,10 @@ namespace lookups * @brief Flag indicating whether this talkgroup will only repeat with affiliations. */ DECLARE_PROPERTY_PLAIN(bool, affiliated); + /** + * @brief Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) + */ + DECLARE_PROPERTY_PLAIN(uint8_t, strapping); /** * @brief Flag indicating whether or not the talkgroup is a parrot. */ @@ -579,8 +598,10 @@ namespace lookups * @param enabled Flag indicating if talkgroup ID is enabled or not. * @param affiliated Flag indicating if talkgroup ID requires affiliated or not. * @param nonPreferred Flag indicating if the talkgroup ID is non-preferred. + * @param strapping Encryption strapping level for this talkgroup. (0 = Selectable, 1 = Strapped, 2 = Clear) */ - void addEntry(uint32_t id, uint8_t slot, bool enabled, bool affiliated = false, bool nonPreferred = false); + void addEntry(uint32_t id, uint8_t slot, bool enabled, bool affiliated = false, bool nonPreferred = false, + uint8_t strapping = TG_STRAPPING_SELECTABLE); /** * @brief Adds a new entry to the lookup table. * @param groupVoice Group Voice Configuration Block. diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index d4ad69f0..61c51ee6 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -873,6 +873,19 @@ void Network::clock(uint32_t ms) bool affiliated = (buffer[offs + 3U] & 0x40U) == 0x40U; bool nonPreferred = (buffer[offs + 3U] & 0x80U) == 0x80U; + // encryption strapping bits + bool strappedBit = (buffer[offs + 3U] & 0x20U) == 0x20U; + bool clearBit = (buffer[offs + 3U] & 0x10U) == 0x10U; + uint8_t strapping = lookups::TG_STRAPPING_SELECTABLE; + + if (strappedBit) { + strapping = lookups::TG_STRAPPING_STRAPPED; + } + + if (clearBit) { + strapping = lookups::TG_STRAPPING_CLEAR; + } + lookups::TalkgroupRuleGroupVoice tid = m_tidLookup->find(id, slot); // if the TG is marked as non-preferred, and the TGID exists in the local entries @@ -891,7 +904,7 @@ void Network::clock(uint32_t ms) LogInfoEx(LOG_NET, "Activated%s%s TG %u TS %u in TGID table", (nonPreferred) ? " non-preferred" : "", (affiliated) ? " affiliated" : "", id, slot); - m_tidLookup->addEntry(id, slot, true, affiliated, nonPreferred); + m_tidLookup->addEntry(id, slot, true, affiliated, nonPreferred, strapping); } offs += 5U; diff --git a/src/common/p25/lc/TSBK.cpp b/src/common/p25/lc/TSBK.cpp index 52e49cfa..2084ec70 100644 --- a/src/common/p25/lc/TSBK.cpp +++ b/src/common/p25/lc/TSBK.cpp @@ -199,7 +199,7 @@ bool TSBK::decode(const uint8_t* data, uint8_t* payload, bool rawTSBK) if (rawTSBK) { ::memcpy(tsbk, data, P25_TSBK_LENGTH_BYTES); - bool ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); + bool ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES, true); if (!ret) { if (s_warnCRC) { // if we're already warning instead of erroring CRC, don't announce invalid CRC in the @@ -226,7 +226,7 @@ bool TSBK::decode(const uint8_t* data, uint8_t* payload, bool rawTSBK) } if (ret) { - ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); + ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES, true); if (!ret) { if (s_warnCRC) { LogWarning(LOG_P25, "TSBK::decode(), failed CRC CCITT-162 check"); diff --git a/src/common/p25/lc/tsbk/TSBKFactory.cpp b/src/common/p25/lc/tsbk/TSBKFactory.cpp index e8700473..200f9446 100644 --- a/src/common/p25/lc/tsbk/TSBKFactory.cpp +++ b/src/common/p25/lc/tsbk/TSBKFactory.cpp @@ -56,7 +56,7 @@ std::unique_ptr TSBKFactory::createTSBK(const uint8_t* data, bool rawTSBK) if (rawTSBK) { ::memcpy(tsbk, data, P25_TSBK_LENGTH_BYTES); - bool ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); + bool ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES, true); if (!ret) { if (s_warnCRC) { // if we're already warning instead of erroring CRC, don't announce invalid CRC in the @@ -85,7 +85,7 @@ std::unique_ptr TSBKFactory::createTSBK(const uint8_t* data, bool rawTSBK) } if (ret) { - ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); + ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES, true); if (!ret) { if (s_warnCRC) { LogWarning(LOG_P25, "TSBKFactory::createTSBK(), failed CRC CCITT-162 check"); diff --git a/src/fne/network/TrafficNetwork.cpp b/src/fne/network/TrafficNetwork.cpp index 6bb16a6f..f27f591c 100644 --- a/src/fne/network/TrafficNetwork.cpp +++ b/src/fne/network/TrafficNetwork.cpp @@ -2258,13 +2258,11 @@ void TrafficNetwork::erasePeer(uint32_t peerId) { bool neighborFNE = false; { - m_peers.lock(); auto it = std::find_if(m_peers.begin(), m_peers.end(), [&](PeerMapPair x) { return x.first == peerId; }); if (it != m_peers.end()) { neighborFNE = it->second->peerClass() == PEER_CONN_CLASS_NEIGHBOR; m_peers.erase(peerId); } - m_peers.unlock(); } // erase any CC maps for this peer @@ -3073,6 +3071,16 @@ void TrafficNetwork::writeTGIDs(uint32_t peerId, uint32_t streamId, bool sendRep slotNo |= 0x40U; } + // set the $20 bit of the slot number to identify if this TG is strapped or not + if (entry.config().strapping() == lookups::TG_STRAPPING_STRAPPED) { + slotNo |= 0x20U; + } + + // set the $10 bit of the slot number to identify if this TG is clear only + if (entry.config().strapping() != lookups::TG_STRAPPING_CLEAR) { + slotNo |= 0x10U; + } + tgidList.push_back({ entry.source().tgId(), slotNo }); } } diff --git a/src/fne/network/TrafficNetwork.h b/src/fne/network/TrafficNetwork.h index c2da1899..96d512a1 100644 --- a/src/fne/network/TrafficNetwork.h +++ b/src/fne/network/TrafficNetwork.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2023-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -88,6 +88,8 @@ namespace network #define INFLUXDB_ERRSTR_DISABLED_DST_RID "disabled destination RID" #define INFLUXDB_ERRSTR_INV_TALKGROUP "illegal/invalid talkgroup" #define INFLUXDB_ERRSTR_DISABLED_TALKGROUP "disabled talkgroup" + #define INFLUXDB_ERRSTR_ENC_TALKGROUP_CLR "encrypted talkgroup with clear traffic" + #define INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC "clear talkgroup with encrypted traffic" #define INFLUXDB_ERRSTR_INV_SLOT "invalid slot for talkgroup" #define INFLUXDB_ERRSTR_RID_NOT_PERMITTED "RID not permitted for talkgroup" #define INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS "illegal/unknown RID attempted access" diff --git a/src/fne/network/callhandler/TagAnalogData.cpp b/src/fne/network/callhandler/TagAnalogData.cpp index 13d161e3..da3dba29 100644 --- a/src/fne/network/callhandler/TagAnalogData.cpp +++ b/src/fne/network/callhandler/TagAnalogData.cpp @@ -781,6 +781,30 @@ bool TagAnalogData::validate(uint32_t peerId, data::NetData& data, uint32_t stre m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); return false; } + else { + // analog doesn't support strapping, so if the TG is strapped, reject the call + if (tg.config().strapping() == lookups::TG_STRAPPING_STRAPPED) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + if (m_network->m_logDenials) + LogError(LOG_ANALOG, INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC ", peer = %u, srcId = %u, dstId = %u", peerId, data.getSrcId(), data.getDstId()); + + // 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; + } + } // peer always send list takes priority over any following affiliation rules bool isAlwaysPeer = false; diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index c370ea18..4a4046b1 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -1272,6 +1272,14 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, lc::CSBK* csbk, return false; } + /* + ** bryanb: yes the encryption else condition is missing here -- DMR is a bit of a mess when it comes to + ** handling encryption because AFAIK the TS.102 spec doesn't clearly indicate how to handle encryption; + ** DVM's FNE cannot reliably implement selectable, strapped, clear checking because it won't know reliably + ** if the call is encrypted or not until it receives a VOICE_LC_HEADER or VOICE_PI_HEADER frame, and by that + ** time it'll really be too late to reject the call + */ + // peer always send list takes priority over any following affiliation rules bool isAlwaysPeer = false; std::vector alwaysSend = tg.config().alwaysSend(); diff --git a/src/fne/network/callhandler/TagNXDNData.cpp b/src/fne/network/callhandler/TagNXDNData.cpp index 74579beb..e99ace99 100644 --- a/src/fne/network/callhandler/TagNXDNData.cpp +++ b/src/fne/network/callhandler/TagNXDNData.cpp @@ -1084,6 +1084,60 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; } + else { + // "selectable" strapping doesn't care about encryption state -- anything else does + if (tg.config().strapping() != lookups::TG_STRAPPING_SELECTABLE) { + // is the TG strapped but the LC is reporting unencrypted? + if (tg.config().strapping() == lookups::TG_STRAPPING_STRAPPED) { + if (lc.getAlgId() == NXDDEF::CIPHER_TYPE_NONE) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(lc.getSrcId())) + .tag("dstId", std::to_string(lc.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_ENC_TALKGROUP_CLR)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + if (m_network->m_logDenials) + LogError(LOG_NXDN, INFLUXDB_ERRSTR_ENC_TALKGROUP_CLR ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + + // 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; + } + } + + // is the TG unstrapped but the LC is reporting encrypted? + if (tg.config().strapping() == lookups::TG_STRAPPING_CLEAR) { + if (lc.getAlgId() != NXDDEF::CIPHER_TYPE_NONE) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(lc.getSrcId())) + .tag("dstId", std::to_string(lc.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + if (m_network->m_logDenials) + LogError(LOG_NXDN, INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + + // 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; + } + } + } + } // peer always send list takes priority over any following affiliation rules bool isAlwaysPeer = false; diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 4c09e4de..e0b10b68 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -1785,6 +1785,60 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; } + else { + // "selectable" strapping doesn't care about encryption state -- anything else does + if (tg.config().strapping() != lookups::TG_STRAPPING_SELECTABLE) { + // is the TG strapped but the LC is reporting unencrypted? + if (tg.config().strapping() == lookups::TG_STRAPPING_STRAPPED) { + if (control.getAlgId() == P25DEF::ALGO_UNENCRYPT) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(control.getSrcId())) + .tag("dstId", std::to_string(control.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_ENC_TALKGROUP_CLR)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + if (m_network->m_logDenials) + LogError(LOG_P25, INFLUXDB_ERRSTR_ENC_TALKGROUP_CLR ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + + // 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; + } + } + + // is the TG unstrapped but the LC is reporting encrypted? + if (tg.config().strapping() == lookups::TG_STRAPPING_CLEAR) { + if (control.getAlgId() != P25DEF::ALGO_UNENCRYPT) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(control.getSrcId())) + .tag("dstId", std::to_string(control.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + if (m_network->m_logDenials) + LogError(LOG_P25, INFLUXDB_ERRSTR_CLR_TALKGROUP_ENC ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + + // 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; + } + } + } + } // peer always send list takes priority over any following affiliation rules bool isAlwaysPeer = false; diff --git a/src/fne/restapi/RESTAPI.cpp b/src/fne/restapi/RESTAPI.cpp index 216ee033..49f01c8a 100644 --- a/src/fne/restapi/RESTAPI.cpp +++ b/src/fne/restapi/RESTAPI.cpp @@ -164,6 +164,8 @@ json::object tgToJson(const TalkgroupRuleGroupVoice& groupVoice) config["affiliated"].set(affiliated); bool parrot = groupVoice.config().parrot(); config["parrot"].set(parrot); + uint8_t strapping = groupVoice.config().strapping(); + config["strapping"].set(strapping); json::array inclusions = json::array(); std::vector inclusion = groupVoice.config().inclusion(); @@ -320,10 +322,16 @@ TalkgroupRuleGroupVoice jsonToTG(json::object& req, HTTPPayload& reply) return TalkgroupRuleGroupVoice(); } + uint8_t strapping = TG_STRAPPING_SELECTABLE; + if (configObj["strapping"].is()) { + strapping = configObj["strapping"].get(); + } + TalkgroupRuleConfig config = groupVoice.config(); config.active(configObj["active"].get()); config.affiliated(configObj["affiliated"].get()); config.parrot(configObj["parrot"].get()); + config.strapping(strapping); if (!configObj["inclusion"].is()) { errorPayload(reply, "TG configuration \"inclusion\" was not a valid JSON array"); diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 0cee8287..c955c6f0 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2015,2016,2017,2018 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -551,7 +551,7 @@ void Slot::processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId) m_rfLastSrcId = 0U; m_rfTGHang.stop(); m_rfState = RS_RF_REJECTED; - m_reverseChannelCommand = network::NET_ICC::NOP; + m_reverseChannelCommand = network::NET_ICC::DMR_RC_CEASE_TRANSMIT; } } break; diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index cf50e964..1c8ab9c1 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -882,6 +882,22 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } + // perform encryption strapping check on the voice frame of a call + if (grp) { + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (!privacy) { + LogWarning(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access, GRP_VOICE_CALL (Group Voice Call) denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", tscc->m_slotNo, srcId, dstId); + writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_DENY_RSN_TGT_GROUP_NOT_VALID, (grp) ? 1U : 0U); + + ::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; + } + } + } + } + uint32_t availChNo = tscc->s_affiliations->getAvailableChannelForSlot(slot); if (!tscc->s_affiliations->rfCh()->isRFChAvailable() || availChNo == 0U) { if (grp) { diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index a57a638d..272b1bf7 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2015,2016,2017,2018 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -276,6 +276,33 @@ bool Voice::process(uint8_t* data, uint32_t len) uint32_t errors = 0U; uint8_t fid = m_slot->m_rfLC->getFID(); bool pf = m_slot->m_rfLC->getPF(); + + // perform encryption strapping check on the first voice sync frame of a call + ::lookups::TalkgroupRuleGroupVoice groupVoice = m_slot->s_tidLookup->find(m_slot->m_rfLC->getDstId()); + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + bool encrypted = false; + if (m_slot->m_rfPrivacyLC != nullptr) { + if (m_slot->m_rfPrivacyLC->getAlgId() != 0U) + encrypted = true; + } + else if (fid == FID_KENWOOD && pf) + encrypted = true; + + if (!encrypted) { + LogWarning(LOG_RF, "DMR Slot %u, VOICE_SYNC denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId()); + ::ActivityLog("DMR", true, "Slot %u RF voice rejection from %u to TG %u ", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId()); + + m_slot->m_rfLastDstId = 0U; + m_slot->m_rfLastSrcId = 0U; + m_slot->m_rfTGHang.stop(); + + m_slot->m_rfState = RS_RF_REJECTED; + return false; + } + } + } + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { if (fid == FID_KENWOOD && pf) errors = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames @@ -344,6 +371,33 @@ bool Voice::process(uint8_t* data, uint32_t len) uint32_t errors = 0U; uint8_t fid = m_slot->m_rfLC->getFID(); bool pf = m_slot->m_rfLC->getPF(); + + // perform encryption strapping check on the voice frame of a call + ::lookups::TalkgroupRuleGroupVoice groupVoice = m_slot->s_tidLookup->find(m_slot->m_rfLC->getDstId()); + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + bool encrypted = false; + if (m_slot->m_rfPrivacyLC != nullptr) { + if (m_slot->m_rfPrivacyLC->getAlgId() != 0U) + encrypted = true; + } + else if (fid == FID_KENWOOD && pf) + encrypted = true; + + if (!encrypted) { + LogWarning(LOG_RF, "DMR Slot %u, VOICE_SYNC denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId()); + ::ActivityLog("DMR", true, "Slot %u RF voice rejection from %u to TG %u ", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId()); + + m_slot->m_rfLastDstId = 0U; + m_slot->m_rfLastSrcId = 0U; + m_slot->m_rfTGHang.stop(); + + m_slot->m_rfState = RS_RF_REJECTED; + return false; + } + } + } + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { if (fid == FID_KENWOOD && pf) errors = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames @@ -475,6 +529,7 @@ bool Voice::process(uint8_t* data, uint32_t len) // Emit reverse-channel data in the final voice burst of a superframe if (m_rfN == 5U) { applyReverseChannelCommand(m_slot->m_reverseChannelCommand, data, emb); + m_slot->m_reverseChannelCommand = network::NET_ICC::NOP; } emb.encode(data + 2U); @@ -1325,6 +1380,8 @@ void Voice::logGPSPosition(const uint32_t srcId, const uint8_t* data) bool Voice::applyReverseChannelCommand(network::NET_ICC::ENUM command, uint8_t* data, dmr::data::EMB& emb) { + m_slot->m_reverseChannelCommand = network::NET_ICC::NOP; + const uint8_t* payload = nullptr; switch (command) { case network::NET_ICC::DMR_RC_CEASE_TRANSMIT: diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index 416b125d..6e0bfead 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -501,6 +501,29 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin return false; } } + + /** + * bryanb: this is disabled -- per the NXDN-TS-1-A spec, there is no reliable way to know if the call + * is encrypted or not, so we cannot perform this check; because VCALL_REQ does not have a field + * to indicate encryption, we cannot know if the call is encrypted or not + */ + /* + // perform encryption strapping check on the voice frame of a call + if (grp) { + if (!tid.isInvalid()) { + if (tid.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (!encryption) { + LogWarning(LOG_RF, "NXDN, %s denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", rcch->toString().c_str(), srcId, dstId); + 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; + } + } + } + } + */ } if (!grp && !m_nxdn->m_ignoreAffiliationCheck) { @@ -528,7 +551,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin LogWarning(LOG_RF, "NXDN, %s queued, no channels available, dstId = %u", rcch->toString().c_str(), dstId); writeRF_Message_Deny(0U, srcId, CauseResponse::VD_QUE_CHN_RESOURCE_NOT_AVAIL, MessageType::RTCH_VCALL); - ::ActivityLog("P25", true, "unit-to-unit grant request from %u to %u queued", srcId, dstId); + ::ActivityLog("NXDN", true, "unit-to-unit grant request from %u to %u queued", srcId, dstId); m_nxdn->m_rfState = RS_RF_REJECTED; } diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index 817e4228..86736fd4 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -155,6 +155,23 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u return false; } } + + // perform encryption strapping check + ::lookups::TalkgroupRuleGroupVoice groupVoice = m_nxdn->m_tidLookup->find(dstId); + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (lc.getAlgId() == NXDDEF::CIPHER_TYPE_NONE) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", srcId, dstId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } + } } else { return false; } diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 0d405095..fc5372a0 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -2153,6 +2153,9 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ return true; // do not generate grant packets for $FFFF (All Call) TGID } + LogDebugEx(LOG_RF, "ControlSignaling::writeRF_TSDU_Grant()", "srcId = %u, dstId = %u, serviceOptions = $%02X, grp = %u, net = %u, skip = %u, chNo = %u", + srcId, dstId, serviceOptions, grp, net, skip, chNo); + // are network channel grants disabled? if (m_p25->m_disableNetworkGrant) { // don't process RF grant if the network isn't in a idle state and the RF destination is the network destination @@ -2231,6 +2234,23 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } + if (grp) { + // perform encryption strapping check + ::lookups::TalkgroupRuleGroupVoice tid = m_p25->m_tidLookup->find(dstId); + if (!tid.isInvalid()) { + if (tid.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (!encryption) { + LogWarning(LOG_RF, "P25, " P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", srcId, dstId); + writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_TGT_GROUP_NOT_VALID, TSBKO::IOSP_GRP_VCH, grp, true); + + ::ActivityLog("P25", true, "group grant request from %u to TG %u denied", srcId, dstId); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } + } + } + } + if (!grp && !m_p25->m_ignoreAffiliationCheck) { // is this the target registered? if (!m_p25->m_affiliations->isUnitReg(dstId)) { diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index 832a4dac..4302dfc8 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -142,6 +142,19 @@ bool Voice::process(uint8_t* data, uint32_t len) } } + // perform encryption strapping check + ::lookups::TalkgroupRuleGroupVoice groupVoice = m_p25->m_tidLookup->find(lc.getDstId()); + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (lc.getAlgId() == P25DEF::ALGO_UNENCRYPT) { + LogWarning(LOG_RF, "P25, " P25_HDU_STR " denial, TGID enc. strapping rejection, dstId = %u", lc.getDstId()); + resetRF(); + m_p25->m_rfState = RS_RF_LISTENING; + return false; + } + } + } + // don't process RF frames if this modem isn't authoritative if (!m_p25->m_authoritative && m_p25->m_permittedDstId != lc.getDstId()) { if (!g_disableNonAuthoritativeLogging) @@ -320,6 +333,23 @@ bool Voice::process(uint8_t* data, uint32_t len) m_p25->m_rfState = RS_RF_REJECTED; return false; } + + // perform encryption strapping check + ::lookups::TalkgroupRuleGroupVoice groupVoice = m_p25->m_tidLookup->find(dstId); + if (!groupVoice.isInvalid()) { + if (groupVoice.config().strapping() == ::lookups::TG_STRAPPING_STRAPPED) { + if (!lc.getEncrypted()) { + LogWarning(LOG_RF, "P25, " P25_HDU_STR " denial, TGID enc. strapping rejection, srcId = %u, dstId = %u", srcId, dstId); + ::ActivityLog("P25", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + + m_p25->m_rfLastDstId = 0U; + m_p25->m_rfLastSrcId = 0U; + m_p25->m_rfTGHang.stop(); + m_p25->m_rfState = RS_RF_REJECTED; + return false; + } + } + } } if (group && dstId == 0U && m_p25->m_forceAllowTG0) {