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