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

pull/121/merge
Bryan Biedenkapp 4 weeks ago
parent 895f5c8df7
commit 27ed5fc15f

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

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

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

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

@ -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<bool>(false);
m_affiliated = node["affiliated"].as<bool>(false);
m_parrot = node["parrot"].as<bool>(false);
m_strapping = (uint8_t)node["strapping"].as<uint32_t>(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.

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

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

@ -56,7 +56,7 @@ std::unique_ptr<TSBK> 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<TSBK> 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");

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

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

@ -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::nanoseconds>(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;

@ -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<uint32_t> alwaysSend = tg.config().alwaysSend();

@ -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::nanoseconds>(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::nanoseconds>(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;

@ -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::nanoseconds>(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::nanoseconds>(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;

@ -164,6 +164,8 @@ json::object tgToJson(const TalkgroupRuleGroupVoice& groupVoice)
config["affiliated"].set<bool>(affiliated);
bool parrot = groupVoice.config().parrot();
config["parrot"].set<bool>(parrot);
uint8_t strapping = groupVoice.config().strapping();
config["strapping"].set<uint8_t>(strapping);
json::array inclusions = json::array();
std::vector<uint32_t> 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<uint8_t>()) {
strapping = configObj["strapping"].get<uint8_t>();
}
TalkgroupRuleConfig config = groupVoice.config();
config.active(configObj["active"].get<bool>());
config.affiliated(configObj["affiliated"].get<bool>());
config.parrot(configObj["parrot"].get<bool>());
config.strapping(strapping);
if (!configObj["inclusion"].is<json::array>()) {
errorPayload(reply, "TG configuration \"inclusion\" was not a valid JSON array");

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

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

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

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

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

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.