From 536997ac10c71e6d1d504f3fbbb9b7dd27a28b71 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sat, 10 Oct 2026 16:23:19 -0400 Subject: [PATCH] add Tier III UDT and registration authentication; --- configs/config.example.yml | 3 + src/common/dmr/DMRDefines.h | 1 + src/common/dmr/data/DataHeader.cpp | 4 +- src/common/dmr/data/DataHeader.h | 6 +- src/common/dmr/lc/CSBK.cpp | 2 +- src/common/dmr/lc/csbk/CSBKFactory.cpp | 3 + src/common/dmr/lc/csbk/CSBKFactory.h | 1 + src/common/dmr/lc/csbk/CSBK_ACK_RSP.cpp | 20 ++-- src/common/dmr/lc/csbk/CSBK_AHOY.cpp | 105 ++++++++++++++++++++ src/common/dmr/lc/csbk/CSBK_AHOY.h | 91 +++++++++++++++++ src/host/dmr/Control.cpp | 72 ++++++++++++++ src/host/dmr/Control.h | 12 +++ src/host/dmr/Slot.cpp | 120 +++++++++++++++++++++++ src/host/dmr/Slot.h | 92 +++++++++++++++++ src/host/dmr/packet/ControlSignaling.cpp | 68 ++++++++++++- src/host/dmr/packet/ControlSignaling.h | 9 ++ src/host/dmr/packet/Data.cpp | 30 +++++- src/host/dmr/packet/Data.h | 2 + tests/dmr/Tier3Lifecycle_Tests.cpp | 29 ++++++ 19 files changed, 650 insertions(+), 20 deletions(-) create mode 100644 src/common/dmr/lc/csbk/CSBK_AHOY.cpp create mode 100644 src/common/dmr/lc/csbk/CSBK_AHOY.h diff --git a/configs/config.example.yml b/configs/config.example.yml index fd3d1ac3..8a27f84f 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -194,6 +194,9 @@ protocols: slot: 1 # Flag indicating whether or not the source ID validation before granting disabled. disableGrantSourceIdCheck: false + # Require ETSI DMR authentication before accepting unit registration. The 16-byte + # authentication key is requested from the upstream FNE through the existing LLA key path. + requireAuthForReg: false # Flag indicating whether or not network calls will generate a channel grant. # (This applies only in conventional operations where channel granting is utilized and RF-only talkgroup diff --git a/src/common/dmr/DMRDefines.h b/src/common/dmr/DMRDefines.h index bed6ffeb..0a9d7f27 100644 --- a/src/common/dmr/DMRDefines.h +++ b/src/common/dmr/DMRDefines.h @@ -471,6 +471,7 @@ namespace dmr AHOY = 0x1CU, //!< AHOY - Enquiry from TSCC RAND = 0x1FU, //!< (ETSI) RAND - Random Access / (DMRA) CALL ALRT - Call Alert ACK_RSP = 0x20U, //!< ACK RSP - Acknowledge Response + ACK_RSP_U = 0x21U, //!< ACK RSP U - Acknowledge Response from Unit EXT_FNCT = 0x24U, //!< (DMRA) EXT FNCT - Extended Function NACK_RSP = 0x26U, //!< NACK RSP - Negative Acknowledgement Response BROADCAST = 0x28U, //!< BCAST - Announcement PDU diff --git a/src/common/dmr/data/DataHeader.cpp b/src/common/dmr/data/DataHeader.cpp index 583ee03a..144ac67c 100644 --- a/src/common/dmr/data/DataHeader.cpp +++ b/src/common/dmr/data/DataHeader.cpp @@ -61,11 +61,11 @@ DataHeader::DataHeader() : m_rspStatus(0U), m_srcPort(0U), m_dstPort(0U), + m_UDTO(0U), m_bptc(), m_data(nullptr), m_SF(false), - m_PF(false), - m_UDTO(0U) + m_PF(false) { m_data = new uint8_t[DMR_LC_HEADER_LENGTH_BYTES]; ::memset(m_data, 0x00U, DMR_LC_HEADER_LENGTH_BYTES); diff --git a/src/common/dmr/data/DataHeader.h b/src/common/dmr/data/DataHeader.h index d90efd01..cd3486a9 100644 --- a/src/common/dmr/data/DataHeader.h +++ b/src/common/dmr/data/DataHeader.h @@ -192,13 +192,17 @@ namespace dmr */ DECLARE_PROPERTY(uint8_t, dstPort, DstPort); + /** + * @brief Unified Data Transport opcode (UDTHD/UDTHU). + */ + DECLARE_PROPERTY(uint8_t, UDTO, UDTO); + private: edac::BPTC19696 m_bptc; uint8_t* m_data; bool m_SF; bool m_PF; - uint8_t m_UDTO; /** * @brief Internal helper to copy the class. diff --git a/src/common/dmr/lc/CSBK.cpp b/src/common/dmr/lc/CSBK.cpp index 03a9ca52..bad5d75a 100644 --- a/src/common/dmr/lc/CSBK.cpp +++ b/src/common/dmr/lc/CSBK.cpp @@ -400,7 +400,7 @@ void CSBK::copy(const CSBK& data) m_proxy = data.m_proxy; m_response = data.m_response; - m_response = data.m_reason; + m_reason = data.m_reason; m_siteOffsetTiming = data.m_siteOffsetTiming; diff --git a/src/common/dmr/lc/csbk/CSBKFactory.cpp b/src/common/dmr/lc/csbk/CSBKFactory.cpp index 27de9f30..3a03edb3 100644 --- a/src/common/dmr/lc/csbk/CSBKFactory.cpp +++ b/src/common/dmr/lc/csbk/CSBKFactory.cpp @@ -119,7 +119,10 @@ std::unique_ptr CSBKFactory::createCSBK(const uint8_t* data, DataType::E d return decode(new CSBK_NACK_RSP(), data, dataType); /** Tier 3 */ + case CSBKO::AHOY: + return decode(new CSBK_AHOY(), data, dataType); case CSBKO::ACK_RSP: + case CSBKO::ACK_RSP_U: return decode(new CSBK_ACK_RSP(), data, dataType); case CSBKO::BROADCAST: return decode(new CSBK_BROADCAST(), data, dataType); diff --git a/src/common/dmr/lc/csbk/CSBKFactory.h b/src/common/dmr/lc/csbk/CSBKFactory.h index 431644e9..fa631728 100644 --- a/src/common/dmr/lc/csbk/CSBKFactory.h +++ b/src/common/dmr/lc/csbk/CSBKFactory.h @@ -25,6 +25,7 @@ #include "common/dmr/DMRDefines.h" #include "common/dmr/lc/CSBK.h" #include "common/dmr/lc/csbk/CSBK_ACK_RSP.h" +#include "common/dmr/lc/csbk/CSBK_AHOY.h" #include "common/dmr/lc/csbk/CSBK_ALOHA.h" #include "common/dmr/lc/csbk/CSBK_BROADCAST.h" #include "common/dmr/lc/csbk/CSBK_BSDWNACT.h" diff --git a/src/common/dmr/lc/csbk/CSBK_ACK_RSP.cpp b/src/common/dmr/lc/csbk/CSBK_ACK_RSP.cpp index ebec778e..d9ef11cd 100644 --- a/src/common/dmr/lc/csbk/CSBK_ACK_RSP.cpp +++ b/src/common/dmr/lc/csbk/CSBK_ACK_RSP.cpp @@ -43,8 +43,9 @@ bool CSBK_ACK_RSP::decode(const uint8_t* data) ulong64_t csbkValue = CSBK::toValue(csbk); - m_GI = (((csbkValue >> 56) & 0xFFU) & 0x40U) == 0x40U; // Group/Individual Flag - m_reason = (uint8_t)((csbkValue >> 33) & 0xFFU); // Reason Code + m_response = (uint8_t)((csbkValue >> 57U) & 0x7FU); // Response Information + m_GI = (m_response & 0x40U) == 0x40U; // Group/Individual Flag + m_reason = (uint8_t)((csbkValue >> 49U) & 0xFFU); // Reason Code m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFFFU); // Target Radio Address m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Source Radio Address @@ -59,15 +60,14 @@ void CSBK_ACK_RSP::encode(uint8_t* data) ulong64_t csbkValue = 0U; - if (m_reason == ReasonCode::TS_ACK_RSN_REG) { - csbkValue = 0U; - } else { - csbkValue = (m_GI ? 0x40U : 0x00U) + // Source Type - (s_siteData.siteId() & 0x3FU); // Net + Site LSB - } - csbkValue = (csbkValue << 7) + (m_response & 0x7FU); // Response Information + uint8_t response = m_response & 0x7FU; + if (m_reason != ReasonCode::TS_ACK_RSN_REG && m_GI) + response |= 0x40U; + + csbkValue = response; // Response Information csbkValue = (csbkValue << 8) + (m_reason & 0xFFU); // Reason Code - csbkValue = (csbkValue << 25) + m_dstId; // Target Radio Address + csbkValue = (csbkValue << 1); // Reserved + csbkValue = (csbkValue << 24) + (m_dstId & 0xFFFFFFU); // Target Radio Address csbkValue = (csbkValue << 24) + m_srcId; // Source Radio Address std::unique_ptr csbk = CSBK::fromValue(csbkValue); diff --git a/src/common/dmr/lc/csbk/CSBK_AHOY.cpp b/src/common/dmr/lc/csbk/CSBK_AHOY.cpp new file mode 100644 index 00000000..6735c4fc --- /dev/null +++ b/src/common/dmr/lc/csbk/CSBK_AHOY.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "dmr/lc/csbk/CSBK_AHOY.h" + +using namespace dmr::defines; +using namespace dmr::lc; +using namespace dmr::lc::csbk; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the CSBK_AHOY class. */ + +CSBK_AHOY::CSBK_AHOY() : CSBK(), + m_serviceOptionsMirror(0U), + m_serviceKindFlag(false), + m_ambientListening(false), + m_appendedBlocks(0U), + m_serviceKind(0U) +{ + m_CSBKO = CSBKO::AHOY; +} + +/* Decode a control signalling block. */ + +bool CSBK_AHOY::decode(const uint8_t* data) +{ + assert(data != nullptr); + + uint8_t csbk[DMR_CSBK_LENGTH_BYTES]; + ::memset(csbk, 0x00U, DMR_CSBK_LENGTH_BYTES); + + bool ret = CSBK::decode(data, csbk); + if (!ret) + return false; + + ulong64_t csbkValue = CSBK::toValue(csbk); + + m_serviceOptionsMirror = (uint8_t)((csbkValue >> 57U) & 0x7FU); // 7-bit Service Options Mirror + m_serviceKindFlag = ((csbkValue >> 56U) & 0x01U) != 0U; // Service Kind Flag + m_ambientListening = ((csbkValue >> 55U) & 0x01U) != 0U; // Ambient Listening Flag + m_GI = ((csbkValue >> 54U) & 0x01U) != 0U; // GI Flag + m_appendedBlocks = (uint8_t)((csbkValue >> 52U) & 0x03U); // Appended Blocks + m_serviceKind = (uint8_t)((csbkValue >> 48U) & 0x0FU); // Service Kind + m_dstId = (uint32_t)((csbkValue >> 24U) & 0xFFFFFFU); // Destination ID + m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Source ID + + return true; +} + +/* Encode a control signalling block. */ + +void CSBK_AHOY::encode(uint8_t* data) +{ + assert(data != nullptr); + + ulong64_t csbkValue = 0U; + + csbkValue = m_serviceOptionsMirror & 0x7FU; // 7-bit Service Options Mirror + csbkValue = (csbkValue << 1U) | (m_serviceKindFlag ? 1U : 0U); // Service Kind Flag + csbkValue = (csbkValue << 1U) | (m_ambientListening ? 1U : 0U); // Ambient Listening Flag + csbkValue = (csbkValue << 1U) | (m_GI ? 1U : 0U); // GI Flag + csbkValue = (csbkValue << 2U) | (m_appendedBlocks & 0x03U); // Appended Blocks + csbkValue = (csbkValue << 4U) | (m_serviceKind & 0x0FU); // Service Kind + csbkValue = (csbkValue << 24U) | (m_dstId & 0xFFFFFFU); // Destination ID + csbkValue = (csbkValue << 24U) | (m_srcId & 0xFFFFFFU); // Source ID + + std::unique_ptr csbk = CSBK::fromValue(csbkValue); + CSBK::encode(data, csbk.get()); +} + +/* Returns a string that represents the current CSBK. */ + +std::string CSBK_AHOY::toString() +{ + return std::string("CSBKO, AHOY (Enquiry from TSCC)"); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Internal helper to copy the the class. */ + +void CSBK_AHOY::copy(const CSBK_AHOY& data) +{ + CSBK::copy(data); + + m_serviceOptionsMirror = data.m_serviceOptionsMirror; + m_serviceKindFlag = data.m_serviceKindFlag; + m_ambientListening = data.m_ambientListening; + m_appendedBlocks = data.m_appendedBlocks; + m_serviceKind = data.m_serviceKind; +} diff --git a/src/common/dmr/lc/csbk/CSBK_AHOY.h b/src/common/dmr/lc/csbk/CSBK_AHOY.h new file mode 100644 index 00000000..f68ac140 --- /dev/null +++ b/src/common/dmr/lc/csbk/CSBK_AHOY.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file CSBK_AHOY.h + * @ingroup dmr_csbk + * @file CSBK_AHOY.cpp + * @ingroup dmr_csbk + */ + #if !defined(__DMR_LC_CSBK__CSBK_AHOY_H__) +#define __DMR_LC_CSBK__CSBK_AHOY_H__ + +#include "common/Defines.h" +#include "common/dmr/lc/CSBK.h" + +namespace dmr +{ + namespace lc + { + namespace csbk + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements the C_AHOY enquiry from a TSCC. + * @ingroup dmr_csbk + */ + class DVM_COMMON_API CSBK_AHOY : public CSBK { + public: + /** + * @brief Initializes a new instance of the CSBK_AHOY class. + */ + CSBK_AHOY(); + + /** + * @brief Decodes a control signalling block. + * @param[in] data Buffer containing a CSBK to decode. + */ + bool decode(const uint8_t* data) override; + /** + * @brief Encodes a control signalling block. + * @param[out] data Buffer to encode a CSBK. + */ + void encode(uint8_t* data) override; + + /** + * @brief Returns a string that represents the current CSBK. + * @returns std::string String representation of the CSBK. + */ + std::string toString() override; + + public: + /** + * @brief Service Options. + */ + DECLARE_PROPERTY(uint8_t, serviceOptionsMirror, ServiceOptionsMirror); + /** + * @brief Service Kind Flag. + */ + DECLARE_PROPERTY(bool, serviceKindFlag, ServiceKindFlag); + + /** + * @brief Ambient Listening. + */ + DECLARE_PROPERTY(bool, ambientListening, AmbientListening); + + /** + * @brief Appended Blocks. + */ + DECLARE_PROPERTY(uint8_t, appendedBlocks, AppendedBlocks); + + /** + * @brief Service Kind. + */ + DECLARE_PROPERTY(uint8_t, serviceKind, ServiceKind); + + DECLARE_COPY(CSBK_AHOY); + }; + } // namespace csbk + } // namespace lc +} // namespace dmr + +#endif // __DMR_LC_CSBK__CSBK_AHOY_H__ diff --git a/src/host/dmr/Control.cpp b/src/host/dmr/Control.cpp index b886cc22..761c8597 100644 --- a/src/host/dmr/Control.cpp +++ b/src/host/dmr/Control.cpp @@ -12,6 +12,7 @@ #include "common/dmr/acl/AccessControl.h" #include "common/dmr/lc/CSBK.h" #include "common/dmr/lc/csbk/CSBKFactory.h" +#include "common/RC4Crypto.h" #include "common/Log.h" #include "dmr/Control.h" #include "Host.h" @@ -21,6 +22,32 @@ using namespace dmr::defines; #include #include +#include +#include + +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/** + * @brief Calculates the registration authentication response. + * @param challenge Registration challenge. + * @param key Authentication key. + * @return Calculated registration authentication response. + */ +uint32_t calculateRegistrationAuthResponse(uint32_t challenge, const uint8_t key[16U]) +{ + uint8_t rc4Key[19U]; + rc4Key[0U] = (uint8_t)((challenge >> 16U) & 0xFFU); + rc4Key[1U] = (uint8_t)((challenge >> 8U) & 0xFFU); + rc4Key[2U] = (uint8_t)(challenge & 0xFFU); + ::memcpy(rc4Key + 3U, key, 16U); + + crypto::RC4 rc4; + std::unique_ptr stream(rc4.keystream(259U, rc4Key, sizeof(rc4Key))); + return ((uint32_t)stream[256U] << 16U) | + ((uint32_t)stream[257U] << 8U) | stream[258U]; +} // --------------------------------------------------------------------------- // Public Class Members @@ -43,6 +70,7 @@ Control::Control(bool authoritative, uint32_t colorCode, uint32_t callHang, uint m_ridLookup(ridLookup), m_tidLookup(tidLookup), m_enableTSCC(false), + m_requireAuthForReg(false), m_tsccCnt(0U), m_tsccCntInterval(1000U, 0U, DMR_SLOT_TIME / 2U), m_tsccSlotNo(0U), @@ -104,6 +132,17 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa backOff = 15U; yaml::Node control = dmrProtocol["control"]; + m_requireAuthForReg = control["requireAuthForReg"].as(false); + if (m_requireAuthForReg && m_network != nullptr) { + m_network->setKEKKeyResponseCallback([this](uint32_t srcId, p25::kmm::KeyItem key, + uint8_t algId, uint8_t keyLength) { + processRegistrationAuthKey(srcId, key, algId, keyLength); + }); + } else if (m_requireAuthForReg) { + LogWarning(LOG_DMR, "DMR registration authentication disabled; no FNE network is configured"); + m_requireAuthForReg = false; + } + bool enableTSCC = control["enable"].as(false); bool dedicatedTSCC = false; if (enableTSCC) { @@ -798,6 +837,39 @@ void Control::processNetwork() } } +/* Helper to process a registration authentication key. */ + +void Control::processRegistrationAuthKey(uint32_t srcId, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength) +{ + (void)algId; // The FNE labels this shared 16-byte key as the P25 AES-128 KEK type. + Slot* tscc = getTSCCSlot(); + if (tscc == nullptr) + return; + + auto it = tscc->m_authTransactions.find(srcId); + if (it == tscc->m_authTransactions.end()) + return; + + if (keyLength != 16U) { + tscc->m_authTransactions.erase(it); + tscc->m_control->writeRF_CSBK_NACK_RSP(srcId, ReasonCode::TS_DENY_RSN_REG_DENIED, ServiceKind::REG_SVC); + return; + } + + uint8_t keyData[16U]; + ::memset(keyData, 0U, 16U); + key.getKey(keyData); + + static std::random_device random; + std::uniform_int_distribution distribution(0U, 0xFFFCDFU); + it->second.challenge = distribution(random); + it->second.expectedResponse = calculateRegistrationAuthResponse(it->second.challenge, keyData); + it->second.challengeSent = true; + it->second.expires = std::chrono::steady_clock::now() + std::chrono::seconds(30); + + tscc->m_control->writeRF_CSBK_AHOY(srcId, ServiceKind::REG_SVC, it->second.challenge, false, 0U); +} + /* Helper to process an In-Call Control message. */ void Control::processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slotNo) diff --git a/src/host/dmr/Control.h b/src/host/dmr/Control.h index f1763ac5..669a4743 100644 --- a/src/host/dmr/Control.h +++ b/src/host/dmr/Control.h @@ -55,6 +55,7 @@ namespace dmr class HOST_SW_API Slot; namespace packet { class HOST_SW_API ControlSignaling; } + namespace packet { class HOST_SW_API Data; } // --------------------------------------------------------------------------- // Class Declaration @@ -307,6 +308,8 @@ namespace dmr private: friend class Slot; + friend class packet::ControlSignaling; + friend class packet::Data; #if defined(CATCH2_TEST_COMPILATION) friend class ::HostTestHooks; #endif @@ -327,6 +330,7 @@ namespace dmr ::lookups::TalkgroupRulesLookup* m_tidLookup; bool m_enableTSCC; + bool m_requireAuthForReg; uint16_t m_tsccCnt; Timer m_tsccCntInterval; @@ -344,6 +348,14 @@ namespace dmr * @brief Process a data frames from the network. */ void processNetwork(); + /** + * @brief Helper to process a registration authentication key. + * @param srcId Source Radio ID. + * @param key Key item containing the authentication key. + * @param algId Algorithm ID. + * @param keyLength Length of the key. + */ + void processRegistrationAuthKey(uint32_t srcId, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength); /** * @brief Helper to process an In-Call Control message. * @param command In-Call Control Command. diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 859f140b..a9a93d00 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -592,6 +592,24 @@ void Slot::clock() uint32_t ms = m_interval.elapsed(); m_interval.start(); + const auto now = std::chrono::steady_clock::now(); + + // clean up expired authentication transactions + for (auto it = m_authTransactions.begin(); it != m_authTransactions.end(); ) { + if (it->second.expires <= now) + it = m_authTransactions.erase(it); + else + ++it; + } + + // clean up expired UDT transactions + for (auto it = m_udtTransactions.begin(); it != m_udtTransactions.end(); ) { + if (it->second.expires <= now) + it = m_udtTransactions.erase(it); + else + ++it; + } + if (s_network != nullptr) { if (s_network->getStatus() == network::NET_STAT_RUNNING) { s_siteData.setNetActive(true); @@ -1796,6 +1814,108 @@ void Slot::confirmTSCCPayloadActive() m_tsccPayloadActRetryCount = 0U; } +/* Starts registration authentication using a key requested by the root controller. */ + +void Slot::beginRegistrationAuth(uint32_t srcId, uint8_t serviceOptions) +{ + AuthTransaction& transaction = m_authTransactions[srcId]; + transaction.serviceOptions = serviceOptions; + transaction.challengeSent = false; + transaction.expires = std::chrono::steady_clock::now() + std::chrono::seconds(30); + + m_control->writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 0U); + if (s_network == nullptr || !s_network->writeLLAKeyReq(srcId)) { + m_authTransactions.erase(srcId); + m_control->writeRF_CSBK_NACK_RSP(srcId, ReasonCode::TS_DENY_RSN_REG_DENIED, ServiceKind::REG_SVC); + } +} + +/* Validates an authentication response and completes the pending registration. */ + +bool Slot::processRegistrationAuthResponse(uint32_t srcId, uint32_t response) +{ + auto it = m_authTransactions.find(srcId); + if (it == m_authTransactions.end() || !it->second.challengeSent) + return false; + + uint32_t difference = (it->second.expectedResponse ^ response) & 0xFFFFFFU; + uint8_t serviceOptions = it->second.serviceOptions; + m_authTransactions.erase(it); + + if (difference == 0U) + m_control->writeRF_CSBK_U_Reg_Rsp(srcId, serviceOptions); + else + m_control->writeRF_CSBK_NACK_RSP(srcId, ReasonCode::TS_DENY_RSN_REG_DENIED, ServiceKind::REG_SVC); + return true; +} + +/* Starts a TSCC-local Unified Data Transport transaction. */ + +void Slot::beginUDT(uint32_t srcId, uint32_t dstId, uint8_t serviceKind) +{ + bool group = serviceKind == ServiceKind::GRP_UDT_DATA_CALL; + m_udtTransactions[srcId] = { dstId, group, serviceKind, std::chrono::steady_clock::now() + std::chrono::seconds(30) }; + + m_control->writeRF_CSBK_AHOY(srcId, serviceKind, WUID_SDMI, group, 0U); +} + +/* Validates an inbound UDT upload and converts its header for delivery. */ + +bool Slot::prepareUDTHeader(data::DataHeader& header) +{ + // look for the UDT transaction corresponding to the source ID in the header + auto it = m_udtTransactions.find(header.getSrcId()); + if (it == m_udtTransactions.end() || header.getDstId() != WUID_SDMI || header.getUDTO() != 0x1BU) + return false; + + header.setDstId(it->second.target); + header.setSrcId(WUID_SDMI); + header.setGI(it->second.group); + header.setA(!it->second.group); + header.setUDTO(0x1AU); + it->second.expires = std::chrono::steady_clock::now() + std::chrono::seconds(30); + + return true; +} + +/* Completes an upload or waits for the individual recipient acknowledgement. */ + +void Slot::completeUDT(uint32_t srcId) +{ + // look for the UDT transaction corresponding to the given source ID + auto it = m_udtTransactions.find(srcId); + if (it == m_udtTransactions.end()) + return; + + // if the transaction is a group transaction, complete it and send an acknowledgement + if (it->second.group) { + m_udtTransactions.erase(it); + + m_control->writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_ACK_RSN_MSG, 1U); + } else { + it->second.expires = std::chrono::steady_clock::now() + std::chrono::seconds(30); + } +} + +/* Mirrors an individual recipient acknowledgement to the originating unit. */ + +bool Slot::processUDTAcknowledgement(uint32_t targetId) +{ + // look for a non-group UDT transaction matching the target ID + for (auto it = m_udtTransactions.begin(); it != m_udtTransactions.end(); ++it) { + if (!it->second.group && it->second.target == targetId) { + uint32_t caller = it->first; + m_udtTransactions.erase(it); + + m_control->writeRF_CSBK_ACK_RSP(caller, ReasonCode::TS_ACK_RSN_MSG, 0U); + + return true; + } + } + + return false; +} + /* Helper to set the DMR short LC. */ void Slot::setShortLC(uint32_t slotNo, uint32_t id, FLCO::E flco, SLCO_ACT_TYPE actType) diff --git a/src/host/dmr/Slot.h b/src/host/dmr/Slot.h index ca270772..5da5e6ec 100644 --- a/src/host/dmr/Slot.h +++ b/src/host/dmr/Slot.h @@ -19,6 +19,7 @@ #include "Defines.h" #include "common/dmr/SiteData.h" +#include "common/dmr/data/DataHeader.h" #include "common/lookups/RSSIInterpolator.h" #include "common/lookups/IdenTableLookup.h" #include "common/lookups/RadioIdLookup.h" @@ -36,6 +37,8 @@ #include #include +#include +#include // --------------------------------------------------------------------------- // Class Prototypes @@ -366,6 +369,56 @@ namespace dmr friend class packet::ControlSignaling; packet::ControlSignaling* m_control; + /** + * @brief Represents an authentication transaction. + */ + struct AuthTransaction { + /** + * @brief Service options for the authentication transaction. + */ + uint8_t serviceOptions; + /** + * @brief Challenge value for the authentication transaction. + */ + uint32_t challenge; + /** + * @brief Expected response for the authentication transaction. + */ + uint32_t expectedResponse; + /** + * @brief Expiration time of the authentication transaction. + */ + std::chrono::steady_clock::time_point expires; + /** + * @brief Flag indicating whether the challenge has been sent. + */ + bool challengeSent; + }; + std::unordered_map m_authTransactions; + + /** + * @brief Represents a Unified Data Transport (UDT) transaction. + */ + struct UDTTransaction { + /** + * @brief Target ID of the UDT transaction. + */ + uint32_t target; + /** + * @brief Indicates whether the UDT transaction is for a group. + */ + bool group; + /** + * @brief Service kind of the UDT transaction. + */ + uint8_t serviceKind; + /** + * @brief Expiration time of the UDT transaction. + */ + std::chrono::steady_clock::time_point expires; + }; + std::unordered_map m_udtTransactions; + uint32_t m_slotNo; RingBuffer m_txImmQueue; @@ -602,6 +655,45 @@ namespace dmr */ void confirmTSCCPayloadActive(); + /** + * @brief Starts registration authentication using a key requested by the root controller. + * @param srcId Source Radio ID. + * @param serviceOptions Service options for the registration request. + */ + void beginRegistrationAuth(uint32_t srcId, uint8_t serviceOptions); + /** + * @brief Validates an authentication response and completes the pending registration. + * @param srcId Source Radio ID. + * @param response Authentication response received from the radio. + * @return True if the response was valid and the registration was completed, false otherwise. + */ + bool processRegistrationAuthResponse(uint32_t srcId, uint32_t response); + + /** + * @brief Starts a TSCC-local Unified Data Transport transaction. + * @param srcId Source Radio ID. + * @param dstId Destination Radio ID. + * @param serviceKind Service kind for the UDT transaction. + */ + void beginUDT(uint32_t srcId, uint32_t dstId, uint8_t serviceKind); + /** + * @brief Validates an inbound UDT upload and converts its header for delivery. + * @param header Reference to the DataHeader object representing the UDT header. + * @return True if the header was successfully prepared for delivery, false otherwise. + */ + bool prepareUDTHeader(data::DataHeader& header); + /** + * @brief Completes an upload or waits for the individual recipient acknowledgement. + * @param srcId Source Radio ID. + */ + void completeUDT(uint32_t srcId); + /** + * @brief Mirrors an individual recipient acknowledgement to the originating unit. + * @param targetId Target Radio ID. + * @return True if the acknowledgement was successfully processed, false otherwise. + */ + bool processUDTAcknowledgement(uint32_t targetId); + /** * @brief Helper to set the DMR short LC. * @param slotNo DMR slot number. diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index b7a2c332..eb9c4098 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -237,7 +237,6 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) } break; case ServiceKind::IND_DATA_CALL: - case ServiceKind::IND_UDT_DATA_CALL: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::IND_VOICE_CALL, srcId); @@ -254,8 +253,24 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) writeRF_CSBK_Data_Grant(srcId, dstId, isp->getServiceOptions(), false); break; + case ServiceKind::IND_UDT_DATA_CALL: + // make sure control data is supported + IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::IND_UDT_DATA_CALL, srcId); + + // validate the source RID + VALID_SRCID(csbk->toString(), ServiceKind::IND_UDT_DATA_CALL, srcId); + + // validate the target RID + VALID_DSTID(csbk->toString(), ServiceKind::IND_UDT_DATA_CALL, srcId, dstId); + + // verify the source RID is registered + VERIFY_SRCID_REG(csbk->toString(), ServiceKind::IND_UDT_DATA_CALL, srcId); + + writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 0U); + + m_slot->beginUDT(srcId, dstId, isp->getServiceKind()); + break; case ServiceKind::GRP_DATA_CALL: - case ServiceKind::GRP_UDT_DATA_CALL: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::GRP_VOICE_CALL, srcId); @@ -269,11 +284,28 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) writeRF_CSBK_Data_Grant(srcId, dstId, isp->getServiceOptions(), true); break; + case ServiceKind::GRP_UDT_DATA_CALL: + // make sure control data is supported + IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::GRP_UDT_DATA_CALL, srcId); + + // validate the source RID + VALID_SRCID(csbk->toString(), ServiceKind::GRP_UDT_DATA_CALL, srcId); + + // validate the talkgroup ID + VALID_TGID(csbk->toString(), ServiceKind::GRP_UDT_DATA_CALL, srcId, dstId); + + writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 1U); + + m_slot->beginUDT(srcId, dstId, isp->getServiceKind()); + break; case ServiceKind::REG_SVC: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(csbk->toString(), ServiceKind::REG_SVC, srcId); - writeRF_CSBK_U_Reg_Rsp(srcId, isp->getServiceOptions()); + if (m_slot->s_dmr->m_requireAuthForReg && (isp->getServiceOptions() & 0x01U) != 0U) + m_slot->beginRegistrationAuth(srcId, isp->getServiceOptions()); + else + writeRF_CSBK_U_Reg_Rsp(srcId, isp->getServiceOptions()); break; default: LogWarning(LOG_RF, "DMR Slot %u, CSBK, RAND (Random Access), unhandled service, serviceKind = %02X", m_slot->m_slotNo, isp->getServiceKind()); @@ -284,9 +316,15 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) } break; case CSBKO::ACK_RSP: + case CSBKO::ACK_RSP_U: { VERBOSE_LOG_CSBK(csbk->toString(), srcId, dstId); ::ActivityLog("DMR", true, "Slot %u ack response from %u to %u", m_slot->m_slotNo, srcId, dstId); + CSBK_ACK_RSP* ack = static_cast(csbk.get()); + if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_AUTH_RESP) + m_slot->processRegistrationAuthResponse(srcId, dstId); + else if (csbk->getCSBKO() == CSBKO::ACK_RSP_U && ack->getReason() == ReasonCode::MS_ACK_RSN_MSG) + m_slot->processUDTAcknowledgement(srcId); } break; case CSBKO::EXT_FNCT: @@ -811,6 +849,30 @@ void ControlSignaling::writeRF_CSBK_ACK_RSP(uint32_t dstId, uint8_t reason, uint writeRF_CSBK_Imm(csbk.get()); } +/* Helper to write an AHOY packet. */ + +void ControlSignaling::writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind, + uint32_t sourceOrChallenge, bool gi, uint8_t appendedBlocks) +{ + std::unique_ptr csbk = std::make_unique(); + csbk->setServiceKind(serviceKind); + csbk->setServiceKindFlag(false); + csbk->setServiceOptionsMirror(0U); + csbk->setAmbientListening(false); + csbk->setAppendedBlocks(appendedBlocks); + csbk->setGI(gi); + csbk->setDstId(dstId); + csbk->setSrcId(sourceOrChallenge); + + if (m_verbose) { + LogInfoEx(LOG_DMR, "DMR Slot %u, CSBK, %s, serviceKind = $%02X, srcId = %u, dstId = %u", + m_slot->m_slotNo, csbk->toString().c_str(), serviceKind, + csbk->getSrcId(), csbk->getDstId()); + } + + writeRF_CSBK_Imm(csbk.get()); +} + /* Helper to write a NACK RSP packet. */ void ControlSignaling::writeRF_CSBK_NACK_RSP(uint32_t dstId, uint8_t reason, uint8_t service) diff --git a/src/host/dmr/packet/ControlSignaling.h b/src/host/dmr/packet/ControlSignaling.h index 1e3e874e..b5959128 100644 --- a/src/host/dmr/packet/ControlSignaling.h +++ b/src/host/dmr/packet/ControlSignaling.h @@ -139,6 +139,15 @@ namespace dmr * @param responseInfo Response code. */ void writeRF_CSBK_ACK_RSP(uint32_t dstId, uint8_t reason, uint8_t responseInfo); + /** + * @brief Helper to write an AHOY packet. + * @param dstId Destination ID. + * @param serviceKind Service Kind. + * @param sourceOrChallenge Source or Challenge ID. + * @param gi Flag indicating whether the GI bit is set. + * @param appendedBlocks Number of appended blocks. + */ + void writeRF_CSBK_AHOY(uint32_t dstId, uint8_t serviceKind, uint32_t sourceOrChallenge, bool gi, uint8_t appendedBlocks); /** * @brief Helper to write a NACK RSP packet. * @param dstId Destination ID. diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index f2ba680b..afc68b47 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -126,6 +126,8 @@ bool Data::process(uint8_t* data, uint32_t len) uint32_t srcId = m_rfDataHeader.getSrcId(); uint32_t dstId = m_rfDataHeader.getDstId(); + m_rfUDT = false; + if (!m_slot->s_authoritative && m_slot->m_permittedDstId != dstId) { if (!g_disableNonAuthoritativeLogging) LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); @@ -168,6 +170,18 @@ bool Data::process(uint8_t* data, uint32_t len) } } + if (m_slot->m_enableTSCC && m_rfDataHeader.getDPF() == DPF::UDT) { + m_rfUDTSource = srcId; + m_rfUDT = m_slot->prepareUDTHeader(m_rfDataHeader); + if (!m_rfUDT) { + LogWarning(LOG_RF, "DMR Slot %u, unsolicited or invalid UDT upload from %u", m_slot->m_slotNo, srcId); + return false; + } + gi = m_rfDataHeader.getGI(); + dstId = m_rfDataHeader.getDstId(); + srcId = m_rfDataHeader.getSrcId(); + } + m_rfDataBlockCnt = 0U; m_slot->m_rfFrames = m_rfDataHeader.getBlocksToFollow(); @@ -229,7 +243,7 @@ bool Data::process(uint8_t* data, uint32_t len) data[0U] = m_slot->m_rfFrames == 0U ? modem::TAG_EOT : modem::TAG_DATA; data[1U] = 0x00U; - if (m_slot->s_duplex && m_repeatDataPacket) + if (m_slot->s_duplex && (m_repeatDataPacket || m_rfUDT)) m_slot->addFrame(data); uint8_t controlByte = 0U; @@ -253,6 +267,10 @@ bool Data::process(uint8_t* data, uint32_t len) m_pduDataOffset = 0U; if (m_slot->m_rfFrames == 0U) { + if (m_rfUDT) { + m_slot->completeUDT(m_rfUDTSource); + } + ::ActivityLog("DMR", true, "Slot %u ended RF data transmission", m_slot->m_slotNo); m_slot->writeEndRF(); } @@ -290,7 +308,7 @@ bool Data::process(uint8_t* data, uint32_t len) m_rfDataBlockCnt++; } - if (m_rfDataHeader.getBlocksToFollow() > 0U && m_slot->m_rfFrames == 0U) { + if (!m_rfUDT && m_rfDataHeader.getBlocksToFollow() > 0U && m_slot->m_rfFrames == 0U) { // ooookay -- lets do the insane, and ridiculously stupid, ETSI Big-Endian reversed byte ordering bullshit for the CRC-32 uint8_t crcBytes[MAX_PDU_COUNT * DMR_PDU_UNCODED_LENGTH_BYTES + 2U]; ::memset(crcBytes, 0x00U, MAX_PDU_COUNT * DMR_PDU_UNCODED_LENGTH_BYTES + 2U); @@ -325,11 +343,15 @@ bool Data::process(uint8_t* data, uint32_t len) m_slot->writeNetwork(data, dataType, 0U); - if (m_slot->s_duplex && m_repeatDataPacket) { + if (m_slot->s_duplex && (m_repeatDataPacket || m_rfUDT)) { m_slot->addFrame(data); } if (m_slot->m_rfFrames == 0U) { + if (m_rfUDT) { + m_slot->completeUDT(m_rfUDTSource); + } + LogInfoEx(LOG_RF, "DMR Slot %u, RATE_12/34_DATA, ended data transmission", m_slot->m_slotNo); m_slot->writeEndRF(); } @@ -621,6 +643,8 @@ Data::Data(Slot* slot, network::BaseNetwork* network, bool dumpDataPacket, bool m_lastRejectId(0U), m_dumpDataPacket(dumpDataPacket), m_repeatDataPacket(repeatDataPacket), + m_rfUDT(false), + m_rfUDTSource(0U), m_verbose(verbose), m_debug(debug) { diff --git a/src/host/dmr/packet/Data.h b/src/host/dmr/packet/Data.h index 954101ec..db62f4f9 100644 --- a/src/host/dmr/packet/Data.h +++ b/src/host/dmr/packet/Data.h @@ -88,6 +88,8 @@ namespace dmr bool m_dumpDataPacket; bool m_repeatDataPacket; + bool m_rfUDT; + uint32_t m_rfUDTSource; bool m_verbose; bool m_debug; diff --git a/tests/dmr/Tier3Lifecycle_Tests.cpp b/tests/dmr/Tier3Lifecycle_Tests.cpp index b4eba24f..9ab4180a 100644 --- a/tests/dmr/Tier3Lifecycle_Tests.cpp +++ b/tests/dmr/Tier3Lifecycle_Tests.cpp @@ -11,6 +11,7 @@ #include "host/Defines.h" #include "common/dmr/DMRDefines.h" #include "common/dmr/lc/csbk/CSBK_ACK_RSP.h" +#include "common/dmr/lc/csbk/CSBK_AHOY.h" #include "common/dmr/lc/csbk/CSBK_MAINT.h" #include "common/dmr/lc/csbk/CSBK_P_CLEAR.h" #include "common/lookups/ChannelLookup.h" @@ -38,6 +39,34 @@ TEST_CASE("DMR Tier III acknowledgement preserves 24-bit target addresses", "[dm REQUIRE(decoded.decode(burst)); CHECK(decoded.getDstId() == 0xABCDEFU); CHECK(decoded.getSrcId() == WUID_TSI); + CHECK(decoded.getReason() == ReasonCode::TS_ACK_RSN_MSG); + CHECK(decoded.getResponse() == 0x55U); +} + +TEST_CASE("C_AHOY preserves all ETSI information elements", "[dmr][tier3][ahoy]") { + CSBK_AHOY encoded; + encoded.setServiceOptionsMirror(0x55U); + encoded.setServiceKindFlag(true); + encoded.setAmbientListening(true); + encoded.setGI(true); + encoded.setAppendedBlocks(3U); + encoded.setServiceKind(ServiceKind::REG_SVC); + encoded.setDstId(0xFEDCBAU); + encoded.setSrcId(0xABCDEFU); + + uint8_t frame[DMR_FRAME_LENGTH_BYTES] = { 0U }; + encoded.encode(frame); + CSBK_AHOY decoded; + decoded.setDataType(DataType::CSBK); + REQUIRE(decoded.decode(frame)); + REQUIRE(decoded.getServiceOptionsMirror() == 0x55U); + REQUIRE(decoded.getServiceKindFlag()); + REQUIRE(decoded.getAmbientListening()); + REQUIRE(decoded.getGI()); + REQUIRE(decoded.getAppendedBlocks() == 3U); + REQUIRE(decoded.getServiceKind() == ServiceKind::REG_SVC); + REQUIRE(decoded.getDstId() == 0xFEDCBAU); + REQUIRE(decoded.getSrcId() == 0xABCDEFU); } TEST_CASE("DMR Tier III maintenance preserves 24-bit addresses", "[dmr][tier3][csbk]")