From b0e2903fb60dee876bae358d73e9d38d11ba7ae9 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Thu, 8 Oct 2026 10:07:40 -0400 Subject: [PATCH] perform refactor for how convential CAI P25 Data and SCEP is handled; --- .../p25/data/ConventionalDataService.cpp | 185 +++++++++ src/common/p25/data/ConventionalDataService.h | 238 +++++++++++ .../p25/data/ConventionalRegistration.cpp | 71 ++++ .../p25/data/ConventionalRegistration.h | 63 +++ src/common/p25/data/IPConvergenceService.cpp | 128 ++++++ src/common/p25/data/IPConvergenceService.h | 248 +++++++++++ src/common/p25/data/IPv4Packet.cpp | 40 ++ src/common/p25/data/IPv4Packet.h | 69 ++++ src/common/p25/data/PacketDataState.cpp | 167 ++++++++ src/common/p25/data/PacketDataState.h | 298 ++++++++++++++ .../callhandler/packetdata/P25PacketData.cpp | 384 +++++++++--------- .../callhandler/packetdata/P25PacketData.h | 11 +- src/host/p25/packet/Data.cpp | 240 ++++------- src/host/p25/packet/Data.h | 44 +- tests/p25/P25_PacketDataState_Tests.cpp | 353 ++++++++++++++++ 15 files changed, 2161 insertions(+), 378 deletions(-) create mode 100644 src/common/p25/data/ConventionalDataService.cpp create mode 100644 src/common/p25/data/ConventionalDataService.h create mode 100644 src/common/p25/data/ConventionalRegistration.cpp create mode 100644 src/common/p25/data/ConventionalRegistration.h create mode 100644 src/common/p25/data/IPConvergenceService.cpp create mode 100644 src/common/p25/data/IPConvergenceService.h create mode 100644 src/common/p25/data/IPv4Packet.cpp create mode 100644 src/common/p25/data/IPv4Packet.h create mode 100644 src/common/p25/data/PacketDataState.cpp create mode 100644 src/common/p25/data/PacketDataState.h create mode 100644 tests/p25/P25_PacketDataState_Tests.cpp diff --git a/src/common/p25/data/ConventionalDataService.cpp b/src/common/p25/data/ConventionalDataService.cpp new file mode 100644 index 00000000..ba96be20 --- /dev/null +++ b/src/common/p25/data/ConventionalDataService.cpp @@ -0,0 +1,185 @@ +// 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 "common/p25/data/ConventionalDataService.h" +#include "common/p25/P25Defines.h" + +using namespace p25::data; +using namespace p25::defines; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the ConventionalDataService class. */ + +ConventionalDataService::ConventionalDataService(DataBindingRegistry& bindings, + DataLinkManager& links, const ConventionalRegistrationPolicy& policy) : + m_bindings(bindings), + m_links(links), + m_policy(policy), + m_registrations() +{ + /* stub */ +} + +/* Processes a conventional registration request. */ + +ConventionalRegistrationResult ConventionalDataService::process(const uint8_t* data, uint32_t length, uint32_t headerLlId, + const ConventionalRegistrationProvisioning& provisioning, uint64_t nowMs) +{ + ConventionalRegistration request; + if (!ConventionalRegistration::decode(data, length, request)) + return deny(request, RegistrationDenyReason::MALFORMED); + + if (headerLlId != 0U && request.llId != headerLlId) + return deny(request, RegistrationDenyReason::IDENTITY_MISMATCH); + + // handle disconnect requests first + if (request.type == PDURegType::DISCONNECT) { + bool changed = m_bindings.erase(request.llId); + changed = m_registrations.erase(request.llId) > 0U || changed; + m_links.erase(request.llId); + + ConventionalRegistrationResult result; + result.decision = RegistrationDecision::DISCONNECTED; + result.denyReason = RegistrationDenyReason::NONE; + result.response = request; + result.bindingChanged = changed; + return result; + } + + if (request.type != PDURegType::CONNECT) + return deny(request, RegistrationDenyReason::MALFORMED); + if (!provisioning.known) + return deny(request, RegistrationDenyReason::NOT_PROVISIONED); + if (!provisioning.enabled) + return deny(request, RegistrationDenyReason::DISABLED); + if ((request.options & 0x0EU) != 0U || + ((request.options & 0x01U) != 0U && !m_policy.allowCMSScan)) + return deny(request, RegistrationDenyReason::UNSUPPORTED_OPTIONS); + + // determine the IP address to use for the registration + uint32_t ipAddress = provisioning.staticIPAddress; + bool dynamic = false; + if (ipAddress != 0U) { + if (request.ipAddress != 0U && request.ipAddress != ipAddress) + return deny(request, RegistrationDenyReason::ADDRESS_CONFLICT); + } else { + if (!provisioning.allowDynamicAddress) + return deny(request, RegistrationDenyReason::ADDRESS_UNAVAILABLE); + ipAddress = request.ipAddress; + if (ipAddress == 0U && m_policy.allocateDynamicAddress) + ipAddress = m_policy.allocateDynamicAddress(request.llId, request.ipAddress); + if (ipAddress == 0U) + return deny(request, RegistrationDenyReason::ADDRESS_UNAVAILABLE); + dynamic = true; + } + + // create or update the IP binding for the registration + IPBinding binding; + binding.link.llId = request.llId; + binding.ipAddress = ipAddress; + binding.accessMode = DataAccessMode::CONVENTIONAL; + binding.convergenceMode = DataConvergenceMode::SCEP; + binding.origin = dynamic ? DataBindingOrigin::CONVENTIONAL_REGISTRATION : + DataBindingOrigin::PROVISIONED; + binding.authorized = true; + if (!m_bindings.upsert(binding)) + return deny(request, RegistrationDenyReason::ADDRESS_CONFLICT); + + // update the registration state + RegistrationState& state = m_registrations[request.llId]; + bool changed = state.ipAddress != ipAddress || state.options != request.options || + state.dynamic != dynamic; + state.ipAddress = ipAddress; + state.options = request.options & 0x01U; + state.dynamic = dynamic; + state.expiresAtMs = m_policy.registrationLifetimeMs == 0U ? 0U : + nowMs + m_policy.registrationLifetimeMs; + + // prepare the result of the registration request + ConventionalRegistrationResult result; + result.decision = RegistrationDecision::ACCEPT; + result.denyReason = RegistrationDenyReason::NONE; + result.response.type = PDURegType::ACCEPT; + result.response.options = state.options; + result.response.llId = request.llId; + result.response.ipAddress = ipAddress; + result.bindingChanged = changed; + + return result; +} + +/* Installs a static registration for the specified logical link identifier. */ + +bool ConventionalDataService::installStatic(uint32_t llId, uint32_t ipAddress, + bool authorized) +{ + // create or update the IP binding for the static registration + IPBinding binding; + binding.link.llId = llId; + binding.ipAddress = ipAddress; + binding.accessMode = DataAccessMode::CONVENTIONAL; + binding.convergenceMode = DataConvergenceMode::SCEP; + binding.origin = DataBindingOrigin::PROVISIONED; + binding.authorized = authorized; + if (!m_bindings.upsert(binding)) + return false; + + // update the registration state for the static registration + RegistrationState& state = m_registrations[llId]; + state.ipAddress = ipAddress; + state.dynamic = false; + state.expiresAtMs = 0U; + + return true; +} + +/* Expires registrations that have passed their expiration time. */ + +void ConventionalDataService::expire(uint64_t nowMs) +{ + // iterate through the registrations and remove any that have expired + for (auto it = m_registrations.begin(); it != m_registrations.end();) { + if (it->second.expiresAtMs != 0U && nowMs >= it->second.expiresAtMs) { + m_bindings.erase(it->first); + m_links.erase(it->first); + it = m_registrations.erase(it); + } else { + ++it; + } + } +} + +/* Checks if the specified logical link identifier is registered. */ + +bool ConventionalDataService::isRegistered(uint32_t llId) const +{ + return m_registrations.find(llId) != m_registrations.end(); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Denies a registration request. */ + +ConventionalRegistrationResult ConventionalDataService::deny( + const ConventionalRegistration& request, RegistrationDenyReason reason) const +{ + // prepare the result of the denial operation + ConventionalRegistrationResult result; + result.decision = RegistrationDecision::DENY; + result.denyReason = reason; + result.response.type = PDURegType::DENY; + result.response.llId = request.llId; + + return result; +} diff --git a/src/common/p25/data/ConventionalDataService.h b/src/common/p25/data/ConventionalDataService.h new file mode 100644 index 00000000..3e85ec1e --- /dev/null +++ b/src/common/p25/data/ConventionalDataService.h @@ -0,0 +1,238 @@ +// 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 ConventionalDataService.h + * @ingroup p25_pdu + * @file ConventionalDataService.cpp + * @ingroup p25_pdu + */ +#if !defined(__P25_DATA__CONVENTIONALDATASERVICE_H__) +#define __P25_DATA__CONVENTIONALDATASERVICE_H__ + +#include "common/Defines.h" +#include "common/p25/data/ConventionalRegistration.h" +#include "common/p25/data/PacketDataState.h" + +#include +#include +#include + +namespace p25 +{ + namespace data + { + // --------------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------------- + + /** + * @addtogroup p25_pdu + * @{ + */ + + /** + * @brief Conventional packet data registration decision. + */ + enum class RegistrationDecision : uint8_t { + ACCEPT, //!< Registration request accepted. + DENY, //!< Registration request denied. + DISCONNECTED //!< Registration disconnected. + }; + + /** + * @brief Reason a conventional packet data registration was denied. + */ + enum class RegistrationDenyReason : uint8_t { + NONE, //!< No specific reason. + MALFORMED, //!< Registration request was malformed. + IDENTITY_MISMATCH, //!< Identity did not match expected value. + NOT_PROVISIONED, //!< Device not provisioned. + DISABLED, //!< Registration disabled. + UNSUPPORTED_OPTIONS, //!< Registration request contained unsupported options. + ADDRESS_UNAVAILABLE, //!< Requested address unavailable. + ADDRESS_CONFLICT //!< Address conflict detected. + }; + /* @} */ + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Provisioning inputs used to decide a registration request. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API ConventionalRegistrationProvisioning { + /** + * @brief Indicates whether the device is known. + */ + bool known = false; + /** + * @brief Indicates whether the device is enabled. + */ + bool enabled = false; + /** + * @brief Indicates whether dynamic address allocation is allowed. + */ + bool allowDynamicAddress = false; + /** + * @brief The static IP address of the device. + */ + uint32_t staticIPAddress = 0U; + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Local policy applied to conventional registration requests. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API ConventionalRegistrationPolicy { + /** + * @brief Indicates whether CMS scan is allowed. + */ + bool allowCMSScan = false; + /** + * @brief The lifetime of a registration in milliseconds. + */ + uint64_t registrationLifetimeMs = 0U; + /** + * @brief Function to allocate a dynamic IP address. + */ + std::function allocateDynamicAddress; + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Result of a conventional registration transaction. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API ConventionalRegistrationResult { + /** + * @brief The decision of the registration request. + */ + RegistrationDecision decision = RegistrationDecision::DENY; + /** + * @brief The reason for denying the registration request. + */ + RegistrationDenyReason denyReason = RegistrationDenyReason::MALFORMED; + /** + * @brief The response to the registration request. + */ + ConventionalRegistration response; + /** + * @brief Indicates whether the binding has changed as a result of the registration request. + */ + bool bindingChanged { false }; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements conventional packet data registration and binding state. + * @ingroup p25_pdu + */ + class DVM_COMMON_API ConventionalDataService { + public: + /** + * @brief Initializes a new instance of the ConventionalDataService class. + * @param bindings The data binding registry. + * @param links The data link manager. + * @param policy The conventional registration policy. + */ + ConventionalDataService(DataBindingRegistry& bindings, DataLinkManager& links, + const ConventionalRegistrationPolicy& policy = ConventionalRegistrationPolicy()); + + /** + * @brief Processes a conventional registration request. + * @param data The raw registration request data. + * @param length The length of the registration request data. + * @param headerLlId The logical link identifier from the header. + * @param provisioning The conventional registration provisioning information. + * @param nowMs The current time in milliseconds. + * @return The result of the registration request. + */ + ConventionalRegistrationResult process(const uint8_t* data, uint32_t length, + uint32_t headerLlId, const ConventionalRegistrationProvisioning& provisioning, + uint64_t nowMs = 0U); + + /** + * @brief Installs a static registration for the specified logical link identifier. + * @param llId The logical link identifier. + * @param ipAddress The IP address associated with the registration. + * @param authorized Indicates whether the registration is authorized. + * @return True if the static registration was successfully installed; otherwise, false. + */ + bool installStatic(uint32_t llId, uint32_t ipAddress, bool authorized = true); + /** + * @brief Expires registrations that have passed their expiration time. + * @param nowMs The current time in milliseconds. + */ + void expire(uint64_t nowMs); + /** + * @brief Checks if the specified logical link identifier is registered. + * @param llId The logical link identifier. + * @return True if the logical link identifier is registered; otherwise, false. + */ + bool isRegistered(uint32_t llId) const; + /** + * @brief Gets the number of current registrations. + * @return The number of current registrations. + */ + size_t registrationCount() const noexcept { return m_registrations.size(); } + + private: + /** + * @brief Represents the state of a registration. + */ + struct RegistrationState { + /** + * @brief The IP address associated with the registration. + */ + uint32_t ipAddress = 0U; + /** + * @brief The options associated with the registration. + */ + uint8_t options = 0U; + /** + * @brief The expiration time of the registration in milliseconds. + */ + uint64_t expiresAtMs = 0U; + /** + * @brief Indicates whether the registration is dynamic. + */ + bool dynamic = false; + }; + + DataBindingRegistry& m_bindings; + DataLinkManager& m_links; + ConventionalRegistrationPolicy m_policy; + std::unordered_map m_registrations; + + /** + * @brief Denies a registration request. + * @param request The registration request to deny. + * @param reason The reason for denying the registration. + * @return The result of the denial operation. + */ + ConventionalRegistrationResult deny(const ConventionalRegistration& request, + RegistrationDenyReason reason) const; + }; + } // namespace data +} // namespace p25 + +#endif // __P25_DATA__CONVENTIONALDATASERVICE_H__ diff --git a/src/common/p25/data/ConventionalRegistration.cpp b/src/common/p25/data/ConventionalRegistration.cpp new file mode 100644 index 00000000..d8e2a898 --- /dev/null +++ b/src/common/p25/data/ConventionalRegistration.cpp @@ -0,0 +1,71 @@ +// 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 "common/p25/data/ConventionalRegistration.h" +#include "common/p25/P25Defines.h" + +#include + +using namespace p25::data; +using namespace p25::defines; + +// --------------------------------------------------------------------------- +// Structure Members +// --------------------------------------------------------------------------- + +/* Decodes a conventional registration PDU from the given data buffer. */ + +bool ConventionalRegistration::decode(const uint8_t* data, uint32_t length, ConventionalRegistration& registration) +{ + if (data == nullptr || length < DISCONNECT_LENGTH) + return false; + + // extract the type of the registration from the data buffer + uint8_t type = (data[0U] >> 4U) & 0x0FU; + if (type != PDURegType::CONNECT && type != PDURegType::DISCONNECT && + type != PDURegType::ACCEPT && type != PDURegType::DENY) + return false; + if (type != PDURegType::DISCONNECT && length < LENGTH) + return false; + + // extract the logical link identifier from the data buffer + uint32_t llId = GET_UINT24(data, 1U); + if (llId == 0U) + return false; + + // populate the registration object with the extracted data + registration.type = type; + registration.options = data[0U] & 0x0FU; + registration.llId = llId; + registration.ipAddress = type == PDURegType::DISCONNECT ? 0U : GET_UINT32(data, 8U); + + return true; +} + +/* Encodes the conventional registration PDU into the given data buffer. */ + +bool ConventionalRegistration::encode(uint8_t* data, uint32_t capacity) const +{ + // determine the length of the encoded PDU based on the type of registration + uint32_t encodedLength = type == PDURegType::DISCONNECT ? DISCONNECT_LENGTH : LENGTH; + if (data == nullptr || capacity < encodedLength || llId == 0U) + return false; + if (type != PDURegType::CONNECT && type != PDURegType::DISCONNECT && + type != PDURegType::ACCEPT && type != PDURegType::DENY) + return false; + + ::memset(data, 0x00U, encodedLength); + data[0U] = uint8_t(((type & 0x0FU) << 4U) | (options & 0x0FU)); + SET_UINT24(llId, data, 1U); + if (type != PDURegType::DISCONNECT) { + SET_UINT32(ipAddress, data, 8U); + } + + return true; +} diff --git a/src/common/p25/data/ConventionalRegistration.h b/src/common/p25/data/ConventionalRegistration.h new file mode 100644 index 00000000..b0fc064b --- /dev/null +++ b/src/common/p25/data/ConventionalRegistration.h @@ -0,0 +1,63 @@ +// 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 ConventionalRegistration.h + * @ingroup p25_pdu + * @file ConventionalRegistration.cpp + * @ingroup p25_pdu + */ +#if !defined(__P25_DATA__CONVENTIONALREGISTRATION_H__) +#define __P25_DATA__CONVENTIONALREGISTRATION_H__ + +#include "common/Defines.h" + +#include + +namespace p25 +{ + namespace data + { + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents a conventional packet data registration PDU. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API ConventionalRegistration { + static constexpr uint32_t DISCONNECT_LENGTH = 8U; + static constexpr uint32_t LENGTH = 12U; + + uint8_t type = 0U; //!< The type of the registration (e.g., CONNECT, DISCONNECT) + uint8_t options = 0U; //!< The options associated with the registration + uint32_t llId = 0U; //!< The logical link identifier for the registration + uint32_t ipAddress = 0U; //!< The IP address associated with the registration + + /** + * @brief Decodes a conventional registration PDU from the given data buffer. + * @param data The pointer to the data buffer containing the PDU. + * @param length The length of the data buffer. + * @param registration The ConventionalRegistration object to populate with the decoded data. + * @return True if the decoding was successful, false otherwise. + */ + static bool decode(const uint8_t* data, uint32_t length, ConventionalRegistration& registration); + /** + * @brief Encodes the conventional registration PDU into the given data buffer. + * @param data The pointer to the data buffer to store the encoded PDU. + * @param capacity The capacity of the data buffer. + * @return True if the encoding was successful, false otherwise. + */ + bool encode(uint8_t* data, uint32_t capacity) const; + }; + } // namespace data +} // namespace p25 + +#endif // __P25_DATA__CONVENTIONALREGISTRATION_H__ diff --git a/src/common/p25/data/IPConvergenceService.cpp b/src/common/p25/data/IPConvergenceService.cpp new file mode 100644 index 00000000..3accd090 --- /dev/null +++ b/src/common/p25/data/IPConvergenceService.cpp @@ -0,0 +1,128 @@ +// 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 "common/p25/data/IPConvergenceService.h" +#include "common/p25/P25Defines.h" + +using namespace p25::data; +using namespace p25::defines; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the SCEPService class. */ + +SCEPService::SCEPService(const DataBindingRegistry& bindings, const SCEPPolicy& policy) : + m_bindings(bindings), + m_policy(policy) +{ + /* stub */ +} + +/* Decodes an IP datagram from the provided data buffer. */ + +IPDecodeResult SCEPService::decode(const uint8_t* data, uint32_t length) const +{ + IPDecodeResult result; + if (!IPv4Packet::parse(data, length, result.packet)) + return result; + + result.result = result.packet.totalLength > m_policy.mtu ? + ConvergenceResult::MTU_EXCEEDED : ConvergenceResult::OK; + + return result; +} + +/* Encodes an IP datagram into the provided data buffer. */ + +IPEncodeResult SCEPService::encode(const uint8_t* data, uint32_t length) const +{ + IPEncodeResult result; + IPDecodeResult decoded = decode(data, length); + result.result = decoded.result; + if (decoded.result != ConvergenceResult::OK) + return result; + + result.length = decoded.packet.totalLength; + bool broadcast = isBroadcast(decoded.packet.destinationAddress); + bool multicast = isMulticast(decoded.packet.destinationAddress); + if ((broadcast && !m_policy.allowBroadcast) || (multicast && !m_policy.allowMulticast)) { + result.result = ConvergenceResult::POLICY_REJECTED; + return result; + } + + if (broadcast || multicast) { + result.llId = WUID_ALL; + result.delivery = m_policy.broadcastDelivery; + return result; + } + + result.llId = routeDownlink(decoded.packet.destinationAddress); + if (result.llId == 0U) { + result.result = ConvergenceResult::NO_BINDING; + return result; + } + + result.delivery = m_policy.unicastDelivery; + + return result; +} + +/* Authorizes an uplink transmission for the specified logical link ID and IP packet. */ + +ConvergenceResult SCEPService::authorizeUplink(uint32_t llId, const IPv4Packet& packet) const +{ + // check if the packet exceeds the maximum transmission unit (MTU) defined in the policy + if (packet.totalLength > m_policy.mtu) + return ConvergenceResult::MTU_EXCEEDED; + + // check if the packet violates the broadcast or multicast policy + if ((isBroadcast(packet.destinationAddress) && !m_policy.allowBroadcast) || + (isMulticast(packet.destinationAddress) && !m_policy.allowMulticast)) + return ConvergenceResult::POLICY_REJECTED; + + // check if the logical link ID corresponds to an authorized SCEP binding + const IPBinding* binding = m_bindings.findByLLId(llId); + if (binding == nullptr || !binding->authorized || + binding->convergenceMode != DataConvergenceMode::SCEP) + return ConvergenceResult::NO_BINDING; + + // check if the source IP address of the packet matches the authorized binding + if (binding->ipAddress != packet.sourceAddress) + return ConvergenceResult::SOURCE_MISMATCH; + + return ConvergenceResult::OK; +} + +/* Routes a downlink transmission to the specified destination address. */ + +uint32_t SCEPService::routeDownlink(uint32_t destinationAddress) const +{ + const IPBinding* binding = m_bindings.findByIPAddress(destinationAddress); + if (binding == nullptr || !binding->authorized || + binding->convergenceMode != DataConvergenceMode::SCEP) + return 0U; + + return binding->link.llId; +} + +/* Determines if the specified address is a broadcast address. */ + +bool SCEPService::isBroadcast(uint32_t address) +{ + return address == 0xFFFFFFFFU; +} + +/* Determines if the specified address is a multicast address. */ + +bool SCEPService::isMulticast(uint32_t address) +{ + return (address & 0xF0000000U) == 0xE0000000U; +} diff --git a/src/common/p25/data/IPConvergenceService.h b/src/common/p25/data/IPConvergenceService.h new file mode 100644 index 00000000..6a9bba81 --- /dev/null +++ b/src/common/p25/data/IPConvergenceService.h @@ -0,0 +1,248 @@ +// 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 IPConvergenceService.h + * @ingroup p25_pdu + * @file IPConvergenceService.cpp + * @ingroup p25_pdu + */ +#if !defined(__P25_DATA__IPCONVERGENCESERVICE_H__) +#define __P25_DATA__IPCONVERGENCESERVICE_H__ + +#include "common/Defines.h" +#include "common/p25/data/IPv4Packet.h" +#include "common/p25/data/PacketDataState.h" + +#include + +namespace p25 +{ + namespace data + { + // --------------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------------- + + /** + * @addtogroup p25_pdu + * @{ + */ + + /** + * @brief Result of an IP convergence operation. + */ + enum class ConvergenceResult : uint8_t { + OK, //!< Operation was successful. + MALFORMED, //!< The IP datagram is malformed. + NO_BINDING, //!< No binding exists for the IP datagram. + SOURCE_MISMATCH, //!< The source address does not match the expected source. + MTU_EXCEEDED, //!< The IP datagram exceeds the maximum transmission unit. + POLICY_REJECTED //!< The IP datagram was rejected by policy. + }; + + /** + * @brief LLC delivery mode selected for an IP datagram. + */ + enum class DataDeliveryMode : uint8_t { + CONFIRMED, //!< The IP datagram requires confirmed delivery. + UNCONFIRMED //!< The IP datagram does not require confirmed delivery. + }; + /* @} */ + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Result returned when decoding an IP datagram. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API IPDecodeResult { + /** + * @brief The result of the IP convergence operation. + */ + ConvergenceResult result = ConvergenceResult::MALFORMED; + /** + * @brief The decoded IP packet. + */ + IPv4Packet packet; + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Result returned when routing an IP datagram for encoding. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API IPEncodeResult { + /** + * @brief The result of the IP convergence operation. + */ + ConvergenceResult result = ConvergenceResult::MALFORMED; + /** + * @brief The logical link identifier for the encoded IP datagram. + */ + uint32_t llId = 0U; + /** + * @brief The length of the encoded IP datagram. + */ + uint16_t length = 0U; + /** + * @brief The delivery mode selected for the encoded IP datagram. + */ + DataDeliveryMode delivery = DataDeliveryMode::CONFIRMED; + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Policy controlling SCEP routing and delivery. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API SCEPPolicy { + /** + * @brief The maximum transmission unit for the SCEP policy. + */ + uint16_t mtu = 510U; + /** + * @brief Indicates whether broadcast transmission is allowed under the SCEP policy. + */ + bool allowBroadcast = true; + /** + * @brief Indicates whether multicast transmission is allowed under the SCEP policy. + */ + bool allowMulticast = false; + /** + * @brief The delivery mode selected for unicast transmissions under the SCEP policy. + */ + DataDeliveryMode unicastDelivery = DataDeliveryMode::CONFIRMED; + /** + * @brief The delivery mode selected for broadcast transmissions under the SCEP policy. + */ + DataDeliveryMode broadcastDelivery = DataDeliveryMode::UNCONFIRMED; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Defines an IP convergence service for a P25 packet data bearer. + * @ingroup p25_pdu + */ + class DVM_COMMON_API IPConvergenceService { + public: + /** + * @brief Finalizes a instance of IPConvergenceService. + */ + virtual ~IPConvergenceService() = default; + + /** + * @brief Decodes an IP datagram from the provided data buffer. + * @param data Pointer to the buffer containing the IP datagram. + * @param length Length of the data buffer. + * @return The result of the IP datagram decoding operation. + */ + virtual IPDecodeResult decode(const uint8_t* data, uint32_t length) const = 0; + /** + * @brief Encodes an IP datagram into the provided data buffer. + * @param data Pointer to the buffer where the IP datagram will be encoded. + * @param length Length of the data buffer. + * @return The result of the IP datagram encoding operation. + */ + virtual IPEncodeResult encode(const uint8_t* data, uint32_t length) const = 0; + /** + * @brief Authorizes an uplink transmission for the specified logical link and IP packet. + * @param llId The logical link identifier. + * @param packet The IP packet to be transmitted. + * @return The result of the uplink authorization operation. + */ + virtual ConvergenceResult authorizeUplink(uint32_t llId, + const IPv4Packet& packet) const = 0; + /** + * @brief Routes a downlink transmission to the specified destination address. + * @param destinationAddress The destination IP address for the downlink transmission. + * @return The result of the downlink routing operation. + */ + virtual uint32_t routeDownlink(uint32_t destinationAddress) const = 0; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements the conventional SCEP IP convergence service. + * @ingroup p25_pdu + */ + class DVM_COMMON_API SCEPService : public IPConvergenceService { + public: + /** + * @brief Initializes an instance of the SCEPService class. + * @param bindings The data binding registry to be used by the service. + * @param policy The SCEP policy to be applied by the service. + */ + explicit SCEPService(const DataBindingRegistry& bindings, + const SCEPPolicy& policy = SCEPPolicy()); + + /** + * @brief Decodes an IP datagram from the provided data buffer. + * @param data Pointer to the buffer containing the IP datagram. + * @param length Length of the data buffer. + * @return The result of the IP datagram decoding operation. + */ + IPDecodeResult decode(const uint8_t* data, uint32_t length) const override; + /** + * @brief Encodes an IP datagram into the provided data buffer. + * @param data Pointer to the buffer containing the IP datagram. + * @param length Length of the data buffer. + * @return The result of the IP datagram encoding operation. + */ + IPEncodeResult encode(const uint8_t* data, uint32_t length) const override; + /** + * @brief Authorizes an uplink transmission for the specified logical link ID and IP packet. + * @param llId The logical link ID for the uplink transmission. + * @param packet The IP packet to be transmitted. + * @return The result of the uplink authorization operation. + */ + ConvergenceResult authorizeUplink(uint32_t llId, const IPv4Packet& packet) const override; + /** + * @brief Routes a downlink transmission to the specified destination address. + * @param destinationAddress The destination address for the downlink transmission. + * @return The logical link ID to which the downlink transmission should be routed. + */ + uint32_t routeDownlink(uint32_t destinationAddress) const override; + + /** + * @brief Determines if the specified address is a broadcast address. + * @param address The address to be checked. + * @return True if the address is a broadcast address, false otherwise. + */ + static bool isBroadcast(uint32_t address); + /** + * @brief Determines if the specified address is a multicast address. + * @param address The address to be checked. + * @return True if the address is a multicast address, false otherwise. + */ + static bool isMulticast(uint32_t address); + + private: + const DataBindingRegistry& m_bindings; + SCEPPolicy m_policy; + }; + + } // namespace data +} // namespace p25 + +#endif // __P25_DATA__IPCONVERGENCESERVICE_H__ diff --git a/src/common/p25/data/IPv4Packet.cpp b/src/common/p25/data/IPv4Packet.cpp new file mode 100644 index 00000000..b5270371 --- /dev/null +++ b/src/common/p25/data/IPv4Packet.cpp @@ -0,0 +1,40 @@ +// 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 "common/p25/data/IPv4Packet.h" + +using namespace p25::data; + +// --------------------------------------------------------------------------- +// Structure Members +// --------------------------------------------------------------------------- + +/* Parses and validates an IPv4 packet. */ + +bool IPv4Packet::parse(const uint8_t* data, uint32_t length, IPv4Packet& packet) +{ + if (data == nullptr || length < 20U || (data[0U] >> 4U) != 4U) + return false; + + uint8_t headerLength = uint8_t((data[0U] & 0x0FU) * 4U); + if (headerLength < 20U || headerLength > length) + return false; + + uint16_t totalLength = GET_UINT16(data, 2U);//uint16_t((uint16_t(data[2U]) << 8U) | uint16_t(data[3U])); + if (totalLength < headerLength || totalLength > length) + return false; + + packet.headerLength = headerLength; + packet.protocol = data[9U]; + packet.totalLength = totalLength; + packet.sourceAddress = GET_UINT32(data, 12U); + packet.destinationAddress = GET_UINT32(data, 16U); + + return true; +} diff --git a/src/common/p25/data/IPv4Packet.h b/src/common/p25/data/IPv4Packet.h new file mode 100644 index 00000000..f865085f --- /dev/null +++ b/src/common/p25/data/IPv4Packet.h @@ -0,0 +1,69 @@ +// 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 IPv4Packet.h + * @ingroup p25_pdu + * @file IPv4Packet.cpp + * @ingroup p25_pdu + */ +#if !defined(__P25_DATA__IPV4PACKET_H__) +#define __P25_DATA__IPV4PACKET_H__ + +#include "common/Defines.h" + +#include + +namespace p25 +{ + namespace data + { + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents validated fields from an IPv4 packet. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API IPv4Packet { + /** + * @brief The length of the IPv4 header in bytes. + */ + uint8_t headerLength = 0U; + /** + * @brief The protocol field from the IPv4 header. + */ + uint8_t protocol = 0U; + /** + * @brief The total length of the IPv4 packet in bytes. + */ + uint16_t totalLength = 0U; + /** + * @brief The source IPv4 address. + */ + uint32_t sourceAddress = 0U; + /** + * @brief The destination IPv4 address. + */ + uint32_t destinationAddress = 0U; + + /** + * @brief Parses and validates an IPv4 packet. + * @param data Pointer to the raw IPv4 packet data. + * @param length Length of the raw data in bytes. + * @param packet Reference to an IPv4Packet structure to populate with parsed data. + * @return True if the packet is successfully parsed and validated, false otherwise. + */ + static bool parse(const uint8_t* data, uint32_t length, IPv4Packet& packet); + }; + } // namespace data +} // namespace p25 + +#endif // __P25_DATA__IPV4PACKET_H__ diff --git a/src/common/p25/data/PacketDataState.cpp b/src/common/p25/data/PacketDataState.cpp new file mode 100644 index 00000000..f35665ea --- /dev/null +++ b/src/common/p25/data/PacketDataState.cpp @@ -0,0 +1,167 @@ +// 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 "common/p25/data/PacketDataState.h" + +using namespace p25::data; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Inserts or updates an IP binding in the registry. Returns true if the operation was successful. */ + +bool DataBindingRegistry::upsert(const IPBinding& binding) +{ + if (binding.link.llId == 0U || binding.ipAddress == 0U) + return false; + + const IPBinding* owner = findByIPAddress(binding.ipAddress); + if (owner != nullptr && owner->link.llId != binding.link.llId) + return false; + + m_bindings[binding.link.llId] = binding; + return true; +} + +/* Erases an IP binding from the registry by its logical link ID. Returns true if the operation was successful. */ + +bool DataBindingRegistry::erase(uint32_t llId) +{ + return m_bindings.erase(llId) > 0U; +} + +/* Clears all IP bindings from the registry. */ + +void DataBindingRegistry::clear() +{ + m_bindings.clear(); +} + +/* Finds an IP binding by its logical link ID. */ + +const IPBinding* DataBindingRegistry::findByLLId(uint32_t llId) const +{ + auto it = m_bindings.find(llId); + return it == m_bindings.end() ? nullptr : &it->second; +} + +/* Finds an IP binding by its IP address. */ + +const IPBinding* DataBindingRegistry::findByIPAddress(uint32_t ipAddress) const +{ + for (const auto& entry : m_bindings) { + if (entry.second.ipAddress == ipAddress) + return &entry.second; + } + return nullptr; +} + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Returns the next send sequence number for the specified logical link. */ + +uint8_t DataLinkManager::nextSendSequence(uint32_t llId, bool& synchronize) +{ + DataLinkState& state = m_states[llId]; + if (!state.sendInitialized) { + state.sendInitialized = true; + state.sendSequence = 0U; + synchronize = true; + return state.sendSequence; + } + + state.sendSequence = (state.sendSequence + 1U) & 0x07U; + synchronize = false; + return state.sendSequence; +} + +/* Accepts a received sequence number for the specified logical link. */ + +ReceiveSequenceResult DataLinkManager::acceptReceiveSequence(uint32_t llId, uint8_t sequence, + bool synchronize, uint32_t packetFingerprint, uint8_t& expectedSequence) +{ + DataLinkState& state = m_states[llId]; + expectedSequence = state.receiveInitialized ? uint8_t((state.receiveSequence + 1U) & 0x07U) : 0U; + + if (sequence > 7U || (synchronize && sequence != 0U)) + return ReceiveSequenceResult::OUT_OF_SEQUENCE; + + if (!state.receiveInitialized || synchronize) { + state.receiveInitialized = true; + state.receiveSequence = sequence; + state.receiveFingerprint = packetFingerprint; + return ReceiveSequenceResult::ACCEPTED; + } + + if (sequence == state.receiveSequence && packetFingerprint == state.receiveFingerprint) + return ReceiveSequenceResult::DUPLICATE; + + // BAED permits gaps: record the newly received N(S), allowing the upper + // layer to observe that intervening logical packets were lost + state.receiveSequence = sequence; + state.receiveFingerprint = packetFingerprint; + return ReceiveSequenceResult::ACCEPTED; +} + +/* Computes the fingerprint of the given data. */ + +uint32_t DataLinkManager::fingerprint(const uint8_t* data, uint32_t length) +{ + if (data == nullptr) + return 0U; + + // stable FNV-1a fingerprint used only for duplicate detection -- the CAI + // assembler has already validated CRCPACKET before this point + uint32_t value = 2166136261U; + for (uint32_t i = 0U; i < length; i++) { + value ^= data[i]; + value *= 16777619U; + } + return value; +} + +/* Sets the readiness state for the specified logical link. */ + +void DataLinkManager::setReady(uint32_t llId, bool ready) +{ + if (llId != 0U) + m_states[llId].readyForNextPacket = ready; +} + +/* Checks if the specified logical link is ready. */ + +bool DataLinkManager::isReady(uint32_t llId) const +{ + auto it = m_states.find(llId); + return it == m_states.end() ? true : it->second.readyForNextPacket; +} + +/* Checks if the specified logical link has an associated state. */ + +bool DataLinkManager::hasState(uint32_t llId) const +{ + return m_states.find(llId) != m_states.end(); +} + +/* Erases the state associated with the specified logical link. */ + +void DataLinkManager::erase(uint32_t llId) +{ + m_states.erase(llId); +} + +/* Clears all logical link states. */ + +void DataLinkManager::clear() +{ + m_states.clear(); +} diff --git a/src/common/p25/data/PacketDataState.h b/src/common/p25/data/PacketDataState.h new file mode 100644 index 00000000..46d58a8a --- /dev/null +++ b/src/common/p25/data/PacketDataState.h @@ -0,0 +1,298 @@ +// 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 PacketDataState.h + * @ingroup p25_pdu + * @file PacketDataState.cpp + * @ingroup p25_pdu + */ +#if !defined(__P25_DATA__PACKETDATASTATE_H__) +#define __P25_DATA__PACKETDATASTATE_H__ + +#include "common/Defines.h" + +#include +#include + +namespace p25 +{ + namespace data + { + // --------------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------------- + + /** + * @addtogroup p25_pdu + * @{ + */ + + /** + * @brief P25 packet data access mode. + */ + enum class DataAccessMode : uint8_t { + CONVENTIONAL, //!< Conventional access mode + TRUNKED //!< Trunked access mode + }; + + /** + * @brief P25 packet data convergence mode. + */ + enum class DataConvergenceMode : uint8_t { + CAI_DATA, //!< CAI data convergence mode + SCEP, //!< SCEP data convergence mode + SNDCP //!< SNDCP data convergence mode + }; + + /** + * @brief Source from which an IP binding was learned. + */ + enum class DataBindingOrigin : uint8_t { + PROVISIONED, //!< Provisioned binding origin + CONVENTIONAL_REGISTRATION, //!< Conventional registration binding origin + SNDCP_STATIC, //!< SNDCP static binding origin + SNDCP_DYNAMIC, //!< SNDCP dynamic binding origin + ARP_LEARNED //!< ARP learned binding origin + }; + + /** + * @brief Result of LLC receive sequence processing. + */ + enum class ReceiveSequenceResult : uint8_t { + ACCEPTED, //!< Accepted receive sequence result + DUPLICATE, //!< Duplicate receive sequence result + OUT_OF_SEQUENCE //!< Out of sequence receive sequence result + }; + /* @} */ + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Identifies a P25 logical data link. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API DataLinkKey { + /** + * @brief The logical link identifier. + */ + uint32_t llId = 0U; + + /** + * @brief Compares this DataLinkKey with another for equality. + * @param other The other DataLinkKey to compare with. + * @return True if the DataLinkKeys are equal; otherwise, false. + */ + bool operator==(const DataLinkKey& other) const noexcept + { + return llId == other.llId; + } + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Associates a logical data link with an authorized IP address. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API IPBinding { + /** + * @brief The logical data link associated with this IP binding. + */ + DataLinkKey link; + /** + * @brief The IP address associated with this binding. + */ + uint32_t ipAddress = 0U; + /** + * @brief The access mode for this binding. + */ + DataAccessMode accessMode = DataAccessMode::CONVENTIONAL; + /** + * @brief The convergence mode for this binding. + */ + DataConvergenceMode convergenceMode = DataConvergenceMode::SCEP; + /** + * @brief The origin from which this binding was learned. + */ + DataBindingOrigin origin = DataBindingOrigin::PROVISIONED; + /** + * @brief The NSAPI associated with this binding. + */ + uint8_t nsapi = 0U; + /** + * @brief Indicates whether the NSAPI is valid. + */ + bool hasNSAPI = false; + /** + * @brief Indicates whether this binding is authorized. + */ + bool authorized = false; + }; + + // --------------------------------------------------------------------------- + // Struct Declaration + // --------------------------------------------------------------------------- + + /** + * @brief LLC sequencing and readiness state for one logical link. + * @ingroup p25_pdu + */ + struct DVM_COMMON_API DataLinkState { + /** + * @brief The send sequence number for this logical link. + */ + uint8_t sendSequence = 0U; + /** + * @brief The receive sequence number for this logical link. + */ + uint8_t receiveSequence = 0U; + /** + * @brief The receive fingerprint for this logical link. + */ + uint32_t receiveFingerprint = 0U; + /** + * @brief Indicates whether the send sequence has been initialized. + */ + bool sendInitialized = false; + /** + * @brief Indicates whether the receive sequence has been initialized. + */ + bool receiveInitialized = false; + /** + * @brief Indicates whether the link is ready for the next packet. + */ + bool readyForNextPacket = true; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Stores typed P25 packet data IP bindings. + * @ingroup p25_pdu + */ + class DVM_COMMON_API DataBindingRegistry { + public: + /** + * @brief Inserts or updates an IP binding in the registry. Returns true if the operation was successful. + * @param binding The IP binding to insert or update. + * @return True if the operation was successful, false otherwise. + */ + bool upsert(const IPBinding& binding); + /** + * @brief Erases an IP binding from the registry by its logical link ID. Returns true if the operation was successful. + * @param llId The logical link ID of the IP binding to erase. + * @return True if the operation was successful, false otherwise. + */ + bool erase(uint32_t llId); + /** + * @brief Clears all IP bindings from the registry. + */ + void clear(); + + /** + * @brief Finds an IP binding by its logical link ID. + * @param llId The logical link ID of the IP binding to find. + * @return A pointer to the IP binding if found, nullptr otherwise. + */ + const IPBinding* findByLLId(uint32_t llId) const; + /** + * @brief Finds an IP binding by its IP address. + * @param ipAddress The IP address of the IP binding to find. + * @return A pointer to the IP binding if found, nullptr otherwise. + */ + const IPBinding* findByIPAddress(uint32_t ipAddress) const; + /** + * @brief Returns the number of IP bindings in the registry. + * @return The number of IP bindings. + */ + size_t size() const noexcept { return m_bindings.size(); } + + private: + std::unordered_map m_bindings; + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Stores LLC state independently for each logical link. + * @ingroup p25_pdu + */ + class DVM_COMMON_API DataLinkManager { + public: + /** + * @brief Returns the next send sequence number for the specified logical link. + * @param llId The logical link ID. + * @param synchronize Indicates whether to synchronize the sequence. + * @return The next send sequence number. + */ + uint8_t nextSendSequence(uint32_t llId, bool& synchronize); + /** + * @brief Accepts a received sequence number for the specified logical link. + * @param llId The logical link ID. + * @param sequence The received sequence number. + * @param synchronize Indicates whether to synchronize the sequence. + * @param packetFingerprint The fingerprint of the received packet. + * @param expectedSequence The expected sequence number. + * @return The result of the receive sequence acceptance. + */ + ReceiveSequenceResult acceptReceiveSequence(uint32_t llId, uint8_t sequence, + bool synchronize, uint32_t packetFingerprint, uint8_t& expectedSequence); + + /** + * @brief Computes the fingerprint of the given data. + * @param data Pointer to the data. + * @param length Length of the data. + * @return The computed fingerprint. + */ + static uint32_t fingerprint(const uint8_t* data, uint32_t length); + + /** + * @brief Sets the readiness state for the specified logical link. + * @param llId The logical link ID. + * @param ready The readiness state to set. + */ + void setReady(uint32_t llId, bool ready); + /** + * @brief Checks if the specified logical link is ready. + * @param llId The logical link ID. + * @return True if the logical link is ready, false otherwise. + */ + bool isReady(uint32_t llId) const; + /** + * @brief Checks if the specified logical link has an associated state. + * @param llId The logical link ID. + * @return True if the logical link has an associated state, false otherwise. + */ + bool hasState(uint32_t llId) const; + /** + * @brief Erases the state associated with the specified logical link. + * @param llId The logical link ID. + */ + void erase(uint32_t llId); + /** + * @brief Clears all logical link states. + */ + void clear(); + + private: + std::unordered_map m_states; + }; + } // namespace data +} // namespace p25 + +#endif // __P25_DATA__PACKETDATASTATE_H__ diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.cpp b/src/fne/network/callhandler/packetdata/P25PacketData.cpp index 36b7ce39..295cc536 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.cpp +++ b/src/fne/network/callhandler/packetdata/P25PacketData.cpp @@ -9,6 +9,8 @@ */ #include "fne/Defines.h" #include "common/p25/kmm/KMMFactory.h" +#include "common/p25/data/ConventionalRegistration.h" +#include "common/p25/data/IPv4Packet.h" #include "common/p25/sndcp/SNDCPFactory.h" #include "common/p25/Sync.h" #include "common/edac/CRC.h" @@ -63,9 +65,10 @@ P25PacketData::P25PacketData(TrafficNetwork* network, TagP25Data* tag, bool debu m_queuedFrameBytes(0U), m_status(), m_arpTable(), - m_readyForNextPkt(), - m_suSendSeq(), - m_suRecvSeq(), + m_bindingRegistry(), + m_dataLinkManager(), + m_conventionalDataService(m_bindingRegistry, m_dataLinkManager), + m_scepService(m_bindingRegistry), m_debug(debug) { assert(network != nullptr); @@ -219,7 +222,7 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee status->hasRxHeader = true; status->llId = status->assembler.dataHeader.getLLId(); - m_readyForNextPkt[status->llId] = true; + m_dataLinkManager.setReady(status->llId, true); // is this a response header? if (status->assembler.dataHeader.getFormat() == PDUFormatType::RSP) { @@ -304,55 +307,42 @@ void P25PacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); #if !defined(_WIN32) - // validate minimum IPv4 header size - if (len < sizeof(struct ip)) { - LogError(LOG_P25, "VTUN packet too small: %u bytes (need minimum %u for IPv4 header)", len, (uint32_t)sizeof(struct ip)); + p25::data::IPDecodeResult decoded = m_scepService.decode(data, len); + if (decoded.result != p25::data::ConvergenceResult::OK) { + LogError(LOG_P25, "VTUN SCEP packet rejected, length = %u, reason = %u", + len, uint8_t(decoded.result)); return; } + const p25::data::IPv4Packet& ipPacket = decoded.packet; - // check IP version (must be IPv4) - if ((data[0] & 0xF0) != 0x40) { - LogWarning(LOG_P25, "VTUN non-IPv4 packet received, version = %u", (data[0] >> 4)); - return; - } - - // validate Internet Header Length - uint8_t ihl = (data[0] & 0x0F) * 4; // IHL in 32-bit words, convert to bytes - if (len < ihl || ihl < 20U) { - LogError(LOG_P25, "VTUN packet has invalid or truncated IP header: len=%u, IHL=%u", len, ihl); - return; - } - - struct ip* ipHeader = (struct ip*)data; + uint32_t networkSrcAddr = htonl(ipPacket.sourceAddress); + uint32_t networkDstAddr = htonl(ipPacket.destinationAddress); char srcIp[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(ipHeader->ip_src), srcIp, INET_ADDRSTRLEN); + inet_ntop(AF_INET, &networkSrcAddr, srcIp, INET_ADDRSTRLEN); char dstIp[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(ipHeader->ip_dst), dstIp, INET_ADDRSTRLEN); - - uint8_t proto = ipHeader->ip_p; - uint16_t pktLen = Utils::reverseEndian(ipHeader->ip_len); // bryanb: this could be problematic on different endianness - - // validate IP total length field against actual received length - if (pktLen > len) { - LogError(LOG_P25, "VTUN IP total length field (%u) exceeds actual packet size (%u)", pktLen, len); - return; - } + inet_ntop(AF_INET, &networkDstAddr, dstIp, INET_ADDRSTRLEN); - if (pktLen < ihl) { - LogError(LOG_P25, "VTUN IP total length (%u) is less than header length (%u)", pktLen, ihl); - return; - } + uint8_t proto = ipPacket.protocol; + uint16_t pktLen = ipPacket.totalLength; #if DEBUG_P25_PDU_DATA Utils::dump(1U, "P25, P25PacketData::processPacketFrame() packet", data, pktLen); #endif - uint32_t llId = getLLIdAddress(Utils::reverseEndian(ipHeader->ip_dst.s_addr)); + // lazily materialize provisioned static SCEP bindings before routing + getLLIdAddress(ipPacket.destinationAddress); + p25::data::IPEncodeResult route = m_scepService.encode(data, len); + if (route.result != p25::data::ConvergenceResult::OK) { + LogWarning(LOG_P25, "VTUN SCEP route rejected, dstIp = %s, reason = %u", + __IP_FROM_UINT(ipPacket.destinationAddress).c_str(), uint8_t(route.result)); + return; + } + uint32_t llId = route.llId; - uint32_t srcProtoAddr = Utils::reverseEndian(ipHeader->ip_src.s_addr); - uint32_t tgtProtoAddr = Utils::reverseEndian(ipHeader->ip_dst.s_addr); + uint32_t srcProtoAddr = ipPacket.sourceAddress; + uint32_t tgtProtoAddr = ipPacket.destinationAddress; std::string srcIpStr = __IP_FROM_UINT(srcProtoAddr); std::string tgtIpStr = __IP_FROM_UINT(tgtProtoAddr); @@ -363,9 +353,10 @@ void P25PacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a // assemble a P25 PDU frame header for transport... data::DataHeader* pktHeader = new data::DataHeader(); - pktHeader->setFormat(PDUFormatType::CONFIRMED); + bool confirmed = route.delivery == p25::data::DataDeliveryMode::CONFIRMED; + pktHeader->setFormat(confirmed ? PDUFormatType::CONFIRMED : PDUFormatType::UNCONFIRMED); pktHeader->setMFId(MFG_STANDARD); - pktHeader->setAckNeeded(true); + pktHeader->setAckNeeded(confirmed); pktHeader->setOutbound(true); pktHeader->setSAP(PDUSAP::PACKET_DATA); pktHeader->setLLId(llId); @@ -513,6 +504,7 @@ void P25PacketData::clock(uint32_t ms) { #if !defined(_WIN32) uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + m_conventionalDataService.expire(now); if (m_queuedFrames.size() == 0U) { return; @@ -533,7 +525,7 @@ void P25PacketData::clock(uint32_t ms) if (frame->retryCnt >= (MAX_PKT_RETRY_CNT * 2U) && frame->extendRetry) { LogWarning(LOG_P25, P25_PDU_STR ", max packet retry count exceeded, dropping packet, dstIp = %s", __IP_FROM_UINT(frame->tgtProtoAddr).c_str()); - m_readyForNextPkt[frame->llId] = true; // force ready for next packet + m_dataLinkManager.setReady(frame->llId, true); // force ready for next packet goto pkt_clock_abort; } @@ -541,10 +533,8 @@ void P25PacketData::clock(uint32_t ms) // extract protocol for logging uint8_t proto = 0x00; - if (frame->userDataLen >= 20) { - struct ip* ipHeader = (struct ip*)(frame->userData); - proto = ipHeader->ip_p; - } + if (frame->userDataLen >= 20U) + proto = frame->userData[9U]; LogInfoEx(LOG_P25, "VTUN -> PDU IP Data (queued), dstIp = %s (%u), userDataLen = %u, proto = %02X%s, retries = %u", tgtIpStr.c_str(), frame->llId, frame->userDataLen, proto, (proto == 0x01) ? " (ICMP)" : "", frame->retryCnt); @@ -567,9 +557,8 @@ void P25PacketData::clock(uint32_t ms) } // is the SU ready for the next packet? - auto ready = std::find_if(m_readyForNextPkt.begin(), m_readyForNextPkt.end(), [=](ReadyForNextPktPair x) { return x.first == frame->llId; }); - if (ready != m_readyForNextPkt.end()) { - if (!ready->second) { + if (m_dataLinkManager.hasState(frame->llId)) { + if (!m_dataLinkManager.isReady(frame->llId)) { LogWarning(LOG_P25, P25_PDU_STR ", subscriber not ready, dstIp = %s (%u), proto = %02X%s, will retry in %ums", tgtIpStr.c_str(), frame->llId, proto, (proto == 0x01) ? " (ICMP)" : "", SUBSCRIBER_READY_RETRY_MS); processed = false; @@ -580,7 +569,7 @@ void P25PacketData::clock(uint32_t ms) } } - m_readyForNextPkt[frame->llId] = false; + m_dataLinkManager.setReady(frame->llId, false); //LogDebugEx(LOG_P25, "P25PacketData::clock()", "dispatching queued PDU to llId %u (proto = %02X)", frame->llId, proto); dispatchUserFrameToFNE(*frame->header, false, false, frame->userData); } @@ -664,7 +653,7 @@ void P25PacketData::dispatch(uint32_t peerId) status->assembler.dataHeader.getLLId(), status->assembler.dataHeader.getSrcLLId()); // bryanb: this is naive and possibly error prone - m_readyForNextPkt[status->assembler.dataHeader.getSrcLLId()] = true; + m_dataLinkManager.setReady(status->assembler.dataHeader.getSrcLLId(), true); if (status->assembler.dataHeader.getResponseClass() == PDUAckClass::ACK && status->assembler.dataHeader.getResponseType() == PDUAckType::ACK) { LogInfoEx(LOG_P25, P25_PDU_STR ", ISP, response, OSP ACK, peer = %u, llId = %u, all blocks received OK, n = %u", @@ -700,7 +689,23 @@ void P25PacketData::dispatch(uint32_t peerId) } if (status->assembler.dataHeader.getFormat() == PDUFormatType::UNCONFIRMED) { - m_readyForNextPkt[status->assembler.dataHeader.getSrcLLId()] = true; + m_dataLinkManager.setReady(status->assembler.dataHeader.getSrcLLId(), true); + } + + // do not route or inject incomplete/invalid confirmed payloads -- the empty + // ACK_RETRY is the interoperable whole-PDU fallback until bitmap-selective + // retry is implemented; a complete packet with a bad CRC is explicitly NACKed + if (status->assembler.dataHeader.getFormat() == PDUFormatType::CONFIRMED && + (status->assembler.getUndecodableBlockCount() > 0U || + status->assembler.getPacketCRCFailed())) { + const uint32_t srcLlId = status->assembler.getExtendedAddress() ? status->assembler.dataHeader.getSrcLLId() : status->assembler.dataHeader.getLLId(); + const uint32_t dstLlId = status->assembler.dataHeader.getLLId(); + const uint8_t responseClass = status->assembler.getUndecodableBlockCount() > 0U ? PDUAckClass::ACK_RETRY : PDUAckClass::NACK; + const uint8_t responseType = status->assembler.getUndecodableBlockCount() > 0U ? PDUAckType::ACK : PDUAckType::NACK_PACKET_CRC; + + write_PDU_Ack_Response(responseClass, responseType, status->assembler.dataHeader.getNs(), srcLlId, + status->assembler.getExtendedAddress(), dstLlId); + return; } uint8_t sap = (status->assembler.getExtendedAddress()) ? status->assembler.dataHeader.getEXSAP() : status->assembler.dataHeader.getSAP(); @@ -749,14 +754,7 @@ void P25PacketData::dispatch(uint32_t peerId) m_arpTable[srcHWAddr] = srcProtoAddr; // is the SU ready for the next packet? - auto ready = std::find_if(m_readyForNextPkt.begin(), m_readyForNextPkt.end(), [=](ReadyForNextPktPair x) { return x.first == srcHWAddr; }); - if (ready != m_readyForNextPkt.end()) { - if (!ready->second) { - m_readyForNextPkt[srcHWAddr] = true; - } - } else { - m_readyForNextPkt[srcHWAddr] = true; - } + m_dataLinkManager.setReady(srcHWAddr, true); } } #else @@ -778,103 +776,70 @@ void P25PacketData::dispatch(uint32_t peerId) if (!status->assembler.getExtendedAddress()) dstLlId = WUID_FNE; - // validate minimum IP header size - if (status->pduUserDataLength < sizeof(struct ip)) { - LogError(LOG_P25, P25_PDU_STR ", PACKET_DATA too small, %u bytes", - status->pduUserDataLength); + p25::data::IPDecodeResult decoded = m_scepService.decode(status->pduUserData, + status->pduUserDataLength); + if (decoded.result != p25::data::ConvergenceResult::OK) { + LogError(LOG_P25, P25_PDU_STR ", SCEP uplink decode rejected, length = %u, reason = %u", + status->pduUserDataLength, uint8_t(decoded.result)); break; } - - struct ip* ipHeader = (struct ip*)(status->pduUserData); - - // verify IPv4 version - if ((status->pduUserData[0] & 0xF0) != 0x40) { - LogWarning(LOG_P25, P25_PDU_STR ", PACKET_DATA non-IPv4 packet"); + const p25::data::IPv4Packet& ipPacket = decoded.packet; + + // provisioned static entries become typed bindings on first use -- ARP + // observations are intentionally not consulted for authorization + getIPAddress(srcLlId); + p25::data::ConvergenceResult authorization = + m_scepService.authorizeUplink(srcLlId, ipPacket); + if (authorization != p25::data::ConvergenceResult::OK) { + LogWarning(LOG_P25, P25_PDU_STR ", SCEP uplink unauthorized, llId = %u, srcIp = %s, reason = %u", + srcLlId, __IP_FROM_UINT(ipPacket.sourceAddress).c_str(), uint8_t(authorization)); + write_PDU_Ack_Response(PDUAckClass::NACK, PDUAckType::NACK_INVL_USER, + status->assembler.dataHeader.getNs(), srcLlId, + status->assembler.getExtendedAddress(), dstLlId); break; } - // validate IP header length - uint8_t ihl = (status->pduUserData[0] & 0x0F) * 4; - if (ihl < sizeof(struct ip) || ihl > status->pduUserDataLength) { - LogError(LOG_P25, P25_PDU_STR ", PACKET_DATA invalid IHL, ihl = %u", ihl); - break; - } + uint32_t networkSrcAddr = htonl(ipPacket.sourceAddress); + uint32_t networkDstAddr = htonl(ipPacket.destinationAddress); + uint16_t pktLen = ipPacket.totalLength; char srcIp[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(ipHeader->ip_src), srcIp, INET_ADDRSTRLEN); + inet_ntop(AF_INET, &networkSrcAddr, srcIp, INET_ADDRSTRLEN); char dstIp[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(ipHeader->ip_dst), dstIp, INET_ADDRSTRLEN); - - uint8_t proto = ipHeader->ip_p; - uint16_t pktLen = Utils::reverseEndian(ipHeader->ip_len); // bryanb: this could be problematic on different endianness - - // reflect broadcast messages back to the CAI network - bool handled = false; - if (status->assembler.dataHeader.getLLId() == WUID_ALL) { - LogInfoEx(LOG_P25, "PDU -> VTUN, IP Data, repeated to CAI, broadcast packet, dstIp = %s (%u)", - dstIp, status->assembler.dataHeader.getLLId()); - - dispatchUserFrameToFNE(status->assembler.dataHeader, status->assembler.getExtendedAddress(), - status->assembler.getAuxiliaryES(), status->pduUserData); - handled = true; - - // is the source SU one we have proper ARP entries for? - auto arpEntry = std::find_if(m_arpTable.begin(), m_arpTable.end(), [=](ArpTablePair x) { return x.first == status->assembler.dataHeader.getSrcLLId(); }); - if (arpEntry == m_arpTable.end()) { - uint32_t srcProtoAddr = Utils::reverseEndian(ipHeader->ip_src.s_addr); - LogInfoEx(LOG_P25, P25_PDU_STR ", adding ARP entry, %s is at %u", __IP_FROM_UINT(srcProtoAddr).c_str(), status->assembler.dataHeader.getSrcLLId()); - m_arpTable[status->assembler.dataHeader.getSrcLLId()] = Utils::reverseEndian(ipHeader->ip_src.s_addr); - } - } + inet_ntop(AF_INET, &networkDstAddr, dstIp, INET_ADDRSTRLEN); - // is the target SU one we have proper ARP entries for? - auto arpEntry = std::find_if(m_arpTable.begin(), m_arpTable.end(), [=](ArpTablePair x) { return x.first == status->assembler.dataHeader.getLLId(); }); - if (arpEntry != m_arpTable.end()) { - LogInfoEx(LOG_P25, "PDU -> VTUN, IP Data, repeated to CAI, destination IP has a CAI ARP table entry, dstIp = %s (%u)", - dstIp, status->assembler.dataHeader.getLLId()); + uint8_t proto = ipPacket.protocol; - dispatchUserFrameToFNE(status->assembler.dataHeader, status->assembler.getExtendedAddress(), status->assembler.getAuxiliaryES(), - status->pduUserData); - handled = true; - - // is the source SU one we have proper ARP entries for? - auto arpEntry = std::find_if(m_arpTable.begin(), m_arpTable.end(), [=](ArpTablePair x) { return x.first == status->assembler.dataHeader.getSrcLLId(); }); - if (arpEntry == m_arpTable.end()) { - uint32_t srcProtoAddr = Utils::reverseEndian(ipHeader->ip_src.s_addr); - LogInfoEx(LOG_P25, P25_PDU_STR ", adding ARP entry, %s is at %u", __IP_FROM_UINT(srcProtoAddr).c_str(), status->assembler.dataHeader.getSrcLLId()); - m_arpTable[status->assembler.dataHeader.getSrcLLId()] = Utils::reverseEndian(ipHeader->ip_src.s_addr); - } - } + // decide whether this datagram also needs CAI delivery, but do not + // reflect it until sequence/duplicate validation has succeeded + bool reflectBroadcast = status->assembler.dataHeader.getLLId() == WUID_ALL; + uint32_t routedLlId = m_scepService.routeDownlink(ipPacket.destinationAddress); + bool reflectRouted = routedLlId != 0U; + bool handled = reflectBroadcast || reflectRouted; // sequence validation - check N(S) against V(R) uint8_t receivedNs = status->assembler.dataHeader.getNs(); bool synchronize = status->assembler.dataHeader.getSynchronize(); - // Initialize V(R) if first packet from this LLId - auto recvSeqIt = m_suRecvSeq.find(srcLlId); - if (recvSeqIt == m_suRecvSeq.end()) { - m_suRecvSeq[srcLlId] = 0U; - } - - uint8_t expectedNs = m_suRecvSeq[srcLlId]; - bool sequenceValid = false; - - // handle synchronize flag - resets receive window per TIA-102 - if (synchronize) { - //LogDebugEx(LOG_P25, "P25PacketData::dispatch()", "synchronize flag set by llId %u, resetting V(R) from %u to %u", - // srcLlId, expectedNs, (receivedNs + 1) % 8); - m_suRecvSeq[srcLlId] = (receivedNs + 1) % 8; - sequenceValid = true; - } - else if (receivedNs == expectedNs || receivedNs == (expectedNs + 1) % 8) { - // accept if N(S) == V(R) or V(R)+1 (allows one-ahead windowing) - m_suRecvSeq[srcLlId] = (receivedNs + 1) % 8; - sequenceValid = true; - //LogDebugEx(LOG_P25, "P25PacketData::dispatch()", "sequence valid, llId %u, N(S) = %u, V(R) now = %u", - // srcLlId, receivedNs, m_suRecvSeq[srcLlId]); + uint8_t expectedNs = 0U; + uint32_t packetFingerprint = p25::data::DataLinkManager::fingerprint( + status->pduUserData, pktLen); + p25::data::ReceiveSequenceResult sequenceResult = + m_dataLinkManager.acceptReceiveSequence(srcLlId, receivedNs, synchronize, + packetFingerprint, expectedNs); + if (sequenceResult == p25::data::ReceiveSequenceResult::DUPLICATE) { + // re-ACK a duplicate, but never deliver the IP datagram twice + if (status->assembler.getExtendedAddress()) { + write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, receivedNs, + srcLlId, true, dstLlId); + } else { + write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, receivedNs, + srcLlId, false); + } + break; } - else { + if (sequenceResult == p25::data::ReceiveSequenceResult::OUT_OF_SEQUENCE) { // out of sequence - send NACK_OUT_OF_SEQ LogWarning(LOG_P25, P25_PDU_STR ", NACK_OUT_OF_SEQ, llId %u, expected N(S) %u or %u, received N(S) = %u", srcLlId, expectedNs, (expectedNs + 1) % 8, receivedNs); @@ -886,8 +851,12 @@ void P25PacketData::dispatch(uint32_t peerId) break; // don't process out-of-sequence packet } - if (!sequenceValid) { - break; // should never reach here due to logic above, but safety check + if (handled) { + LogInfoEx(LOG_P25, "PDU -> VTUN, IP Data, repeated to CAI, dstIp = %s (%u)", + dstIp, reflectBroadcast ? WUID_ALL : routedLlId); + dispatchUserFrameToFNE(status->assembler.dataHeader, + status->assembler.getExtendedAddress(), status->assembler.getAuxiliaryES(), + status->pduUserData); } // transmit packet to IP network @@ -907,10 +876,10 @@ void P25PacketData::dispatch(uint32_t peerId) if (!handled) { //LogDebugEx(LOG_P25, "P25PacketData::dispatch()", "marking llId %u ready for next packet (proto = %02X)", srcLlId, proto); if (status->assembler.getExtendedAddress()) { - m_readyForNextPkt[srcLlId] = true; + m_dataLinkManager.setReady(srcLlId, true); write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, receivedNs, srcLlId, true, dstLlId); } else { - m_readyForNextPkt[srcLlId] = true; + m_dataLinkManager.setReady(srcLlId, true); write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, receivedNs, srcLlId, false); } } @@ -1026,15 +995,13 @@ void P25PacketData::dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bo uint32_t srcId = (extendedAddress) ? dataHeader.getSrcLLId() : dataHeader.getLLId(); uint32_t dstId = dataHeader.getLLId(); - // update the sequence number - m_suSendSeq[srcId]++; - if (m_suSendSeq[srcId] >= 8U) - { - m_suSendSeq[srcId] = 0U; + // update V(S) independently for each logical link + bool synchronize = false; + uint8_t sendSequence = m_dataLinkManager.nextSendSequence(srcId, synchronize); + if (synchronize) { dataHeader.setSynchronize(true); } - - dataHeader.setNs(m_suSendSeq[srcId]); + dataHeader.setNs(sendSequence); /* ** MASTER TRAFFIC @@ -1056,37 +1023,62 @@ void P25PacketData::dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bo bool P25PacketData::processConvDataReg(RxStatus* status) { - uint8_t regType = (status->pduUserData[0U] >> 4) & 0x0F; - switch (regType) { - case PDURegType::CONNECT: - { - uint32_t llId = (status->pduUserData[1U] << 16) + (status->pduUserData[2U] << 8) + status->pduUserData[3U]; - uint32_t ipAddr = (status->pduUserData[8U] << 24) + (status->pduUserData[9U] << 16) + (status->pduUserData[10U] << 8) + status->pduUserData[11U]; + if (status == nullptr || status->pduUserData == nullptr) { + LogError(LOG_P25, P25_PDU_STR ", truncated conventional registration payload"); + return false; + } - if (ipAddr == 0U) { - LogWarning(LOG_P25, P25_PDU_STR ", CONNECT (Registration Request Connect) with zero IP address, llId = %u", llId); - ipAddr = getIPAddress(llId); - } + p25::data::ConventionalRegistration registration; + if (!p25::data::ConventionalRegistration::decode(status->pduUserData, + status->pduUserDataLength, registration)) { + LogError(LOG_P25, P25_PDU_STR ", invalid conventional registration payload"); + return false; + } - LogInfoEx(LOG_P25, P25_PDU_STR ", CONNECT (Registration Request Connect), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); - m_arpTable[llId] = ipAddr; // update ARP table + p25::data::ConventionalRegistrationProvisioning provisioning; + lookups::RadioId rid = m_network->m_ridLookup->find(registration.llId); + provisioning.known = !rid.radioDefault(); + provisioning.enabled = provisioning.known && rid.radioEnabled(); + provisioning.allowDynamicAddress = provisioning.enabled && rid.radioIPAddress().empty(); + if (provisioning.enabled && !rid.radioIPAddress().empty()) + provisioning.staticIPAddress = __IP_FROM_STR(rid.radioIPAddress()); + + uint32_t headerLlId = status->assembler.getExtendedAddress() ? + status->assembler.dataHeader.getSrcLLId() : status->assembler.dataHeader.getLLId(); + p25::data::ConventionalRegistrationResult result = m_conventionalDataService.process( + status->pduUserData, status->pduUserDataLength, headerLlId, provisioning); + + if (result.decision == p25::data::RegistrationDecision::DISCONNECTED) { + m_arpTable.erase(registration.llId); + LogInfoEx(LOG_P25, P25_PDU_STR ", DISCONNECT, llId = %u", registration.llId); + return true; } - break; - case PDURegType::DISCONNECT: - { - uint32_t llId = (status->pduUserData[1U] << 16) + (status->pduUserData[2U] << 8) + status->pduUserData[3U]; - LogInfoEx(LOG_P25, P25_PDU_STR ", DISCONNECT (Registration Request Disconnect), llId = %u", llId); + // only the FNE emits the registration decision -- the host merely validates + // and forwards the request, so an ACCEPT always reflects installed state + uint8_t responseData[p25::data::ConventionalRegistration::LENGTH]; + if (!result.response.encode(responseData, sizeof(responseData))) + return false; - m_arpTable.erase(llId); - } - break; - default: - LogError(LOG_P25, "P25 unhandled PDU registration type, regType = $%02X", regType); - break; + data::DataHeader responseHeader; + responseHeader.setFormat(PDUFormatType::CONFIRMED); + responseHeader.setMFId(MFG_STANDARD); + responseHeader.setAckNeeded(true); + responseHeader.setOutbound(true); + responseHeader.setSAP(PDUSAP::CONV_DATA_REG); + responseHeader.setLLId(registration.llId); + responseHeader.calculateLength(sizeof(responseData)); + dispatchUserFrameToFNE(responseHeader, false, false, responseData); + + if (result.decision == p25::data::RegistrationDecision::ACCEPT) { + LogInfoEx(LOG_P25, P25_PDU_STR ", registration ACCEPT, llId = %u, ipAddr = %s", + registration.llId, __IP_FROM_UINT(result.response.ipAddress).c_str()); + return true; } - return true; + LogWarning(LOG_P25, P25_PDU_STR ", registration DENY, llId = %u, reason = %u", + registration.llId, uint8_t(result.denyReason)); + return false; } /* Helper used to process SNDCP control data from PDU data. */ @@ -1199,7 +1191,7 @@ bool P25PacketData::processSNDCPControl(RxStatus* status) rspHeader.calculateLength(13U); m_arpTable[llId] = staticIP; - m_readyForNextPkt[llId] = true; + m_dataLinkManager.setReady(llId, true); dispatchUserFrameToFNE(rspHeader, false, false, txPduUserData); @@ -1264,7 +1256,7 @@ bool P25PacketData::processSNDCPControl(RxStatus* status) rspHeader.calculateLength(13U); m_arpTable[llId] = dynamicIP; - m_readyForNextPkt[llId] = true; + m_dataLinkManager.setReady(llId, true); dispatchUserFrameToFNE(rspHeader, false, false, txPduUserData); @@ -1310,7 +1302,7 @@ bool P25PacketData::processSNDCPControl(RxStatus* status) isp->getDeactType()); m_arpTable.erase(llId); - m_readyForNextPkt.erase(llId); + m_dataLinkManager.erase(llId); // send ACK response write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, @@ -1505,18 +1497,18 @@ uint32_t P25PacketData::getIPAddress(uint32_t llId) return 0U; } - if (hasARPEntry(llId)) { - return m_arpTable[llId]; - } else { - // do we have a static entry for this LLID? - lookups::RadioId rid = m_network->m_ridLookup->find(llId); - if (!rid.radioDefault()) { - if (rid.radioEnabled()) { - std::string addr = rid.radioIPAddress(); - uint32_t ipAddr = __IP_FROM_STR(addr); - return ipAddr; - } - } + const p25::data::IPBinding* binding = m_bindingRegistry.findByLLId(llId); + if (binding != nullptr && binding->authorized) { + return binding->ipAddress; + } + + // provisioned RID addresses are authoritative; ARP is discovery only and + // must never create packet-data authorization + lookups::RadioId rid = m_network->m_ridLookup->find(llId); + if (!rid.radioDefault() && rid.radioEnabled() && !rid.radioIPAddress().empty()) { + uint32_t ipAddr = __IP_FROM_STR(rid.radioIPAddress()); + if (ipAddr != 0U && m_conventionalDataService.installStatic(llId, ipAddr)) + return ipAddr; } return 0U; @@ -1530,12 +1522,9 @@ uint32_t P25PacketData::getLLIdAddress(uint32_t addr) return 0U; } - // lookup ARP table entry - for (auto entry : m_arpTable) { - if (entry.second == addr) { - return entry.first; - } - } + const p25::data::IPBinding* binding = m_bindingRegistry.findByIPAddress(addr); + if (binding != nullptr && binding->authorized) + return binding->link.llId; // lookup IP from static RID table std::string ipAddr = __IP_FROM_UINT(addr); @@ -1548,7 +1537,8 @@ uint32_t P25PacketData::getLLIdAddress(uint32_t addr) return false; }); if (it != ridTable.end()) { - return it->first; + if (m_conventionalDataService.installStatic(it->first, addr)) + return it->first; } return 0U; diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.h b/src/fne/network/callhandler/packetdata/P25PacketData.h index 61429ea0..89200649 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.h +++ b/src/fne/network/callhandler/packetdata/P25PacketData.h @@ -22,6 +22,9 @@ #include "common/concurrent/unordered_map.h" #include "common/p25/P25Defines.h" #include "common/p25/data/Assembler.h" +#include "common/p25/data/ConventionalDataService.h" +#include "common/p25/data/IPConvergenceService.h" +#include "common/p25/data/PacketDataState.h" #include "common/p25/data/DataHeader.h" #include "common/p25/data/DataBlock.h" #include "network/TrafficNetwork.h" @@ -228,10 +231,10 @@ namespace network typedef std::pair ArpTablePair; std::unordered_map m_arpTable; - typedef std::pair ReadyForNextPktPair; - std::unordered_map m_readyForNextPkt; - std::unordered_map m_suSendSeq; // V(S) send state variable per LLId - std::unordered_map m_suRecvSeq; // V(R) receive state variable per LLId + p25::data::DataBindingRegistry m_bindingRegistry; + p25::data::DataLinkManager m_dataLinkManager; + p25::data::ConventionalDataService m_conventionalDataService; + p25::data::SCEPService m_scepService; bool m_debug; diff --git a/src/host/p25/packet/Data.cpp b/src/host/p25/packet/Data.cpp index 20c9c920..c168e3bc 100644 --- a/src/host/p25/packet/Data.cpp +++ b/src/host/p25/packet/Data.cpp @@ -11,6 +11,7 @@ #include "Defines.h" #include "common/p25/P25Defines.h" #include "common/p25/acl/AccessControl.h" +#include "common/p25/data/ConventionalRegistration.h" #include "common/p25/lc/tdulc/TDULCFactory.h" #include "common/p25/sndcp/SNDCPFactory.h" #include "common/p25/P25Utils.h" @@ -137,13 +138,7 @@ bool Data::process(uint8_t* data, uint32_t len) if (m_rfAssembler->dataHeader.getResponseClass() == PDUAckClass::ACK && m_rfAssembler->dataHeader.getResponseType() == PDUAckType::ACK) { LogInfoEx(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK, llId = %u, all blocks received OK, n = %u", m_rfAssembler->dataHeader.getLLId(), m_rfAssembler->dataHeader.getResponseStatus()); - if (m_retryPDUData != nullptr && m_retryPDUBitLength > 0U) { - delete[] m_retryPDUData; - m_retryPDUData = nullptr; - - m_retryPDUBitLength = 0U; - m_retryCount = 0U; - } + m_retryPDUState.erase(m_rfAssembler->dataHeader.getLLId()); } else { if (m_rfAssembler->dataHeader.getResponseClass() == PDUAckClass::NACK) { switch (m_rfAssembler->dataHeader.getResponseType()) { @@ -174,18 +169,16 @@ bool Data::process(uint8_t* data, uint32_t len) // really this is supposed to check the bit field in the included response // and only return those bits -- but we're responding with the entire previous packet... - if (m_retryPDUData != nullptr && m_retryPDUBitLength > 0U) { - if (m_retryCount < MAX_PDU_RETRY_CNT) { + uint32_t retryLlId = m_rfAssembler->dataHeader.getLLId(); + auto retry = m_retryPDUState.find(retryLlId); + if (retry != m_retryPDUState.end() && retry->second.bitLength > 0U && !retry->second.data.empty()) { + if (retry->second.retryCount < MAX_PDU_RETRY_CNT) { m_p25->writeRF_Preamble(); - writeRF_PDU(m_retryPDUData, m_retryPDUBitLength, false, true); - m_retryCount++; + writeRF_PDU(retry->second.data.data(), retry->second.bitLength, false, true, retryLlId, false); + retry->second.retryCount++; } else { - delete[] m_retryPDUData; - m_retryPDUData = nullptr; - - m_retryPDUBitLength = 0U; - m_retryCount = 0U; + m_retryPDUState.erase(retry); LogInfoEx(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK RETRY, llId = %u, exceeded retries, undeliverable", m_rfAssembler->dataHeader.getLLId()); @@ -260,6 +253,26 @@ bool Data::process(uint8_t* data, uint32_t len) m_rfPduUserDataLength = m_rfAssembler->getUserDataLength(); m_rfAssembler->getUserData(m_rfPduUserData); + // never expose a partially decoded or CRC-invalid confirmed packet to + // registration, routing, or the IP side -- a response without a bitmap + // requests a conservative whole-PDU retry; selective retry can be added + // later without weakening this integrity boundary + if (m_rfAssembler->dataHeader.getFormat() == PDUFormatType::CONFIRMED && (m_rfAssembler->getUndecodableBlockCount() > 0U || m_rfAssembler->getPacketCRCFailed())) { + const uint8_t responseClass = m_rfAssembler->getUndecodableBlockCount() > 0U ? PDUAckClass::ACK_RETRY : PDUAckClass::NACK; + const uint8_t responseType = m_rfAssembler->getUndecodableBlockCount() > 0U ? PDUAckType::ACK : PDUAckType::NACK_PACKET_CRC; + + writeRF_PDU_Ack_Response(responseClass, responseType, + m_rfAssembler->dataHeader.getNs(), m_rfAssembler->dataHeader.getLLId(), + m_rfAssembler->getExtendedAddress(), m_rfAssembler->dataHeader.getSrcLLId()); + + m_rfPDUCount = 0U; + m_rfPDUBits = 0U; + m_rfPduUserDataLength = 0U; + ::memset(m_rfPDU, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U); + m_p25->m_rfState = RS_RF_LISTENING; + return true; + } + uint8_t sap = (m_rfAssembler->getExtendedAddress()) ? m_rfAssembler->dataHeader.getEXSAP() : m_rfAssembler->dataHeader.getSAP(); if (m_rfAssembler->getAuxiliaryES()) sap = m_rfAssembler->dataHeader.getEXSAP(); @@ -271,6 +284,12 @@ bool Data::process(uint8_t* data, uint32_t len) switch (sap) { case PDUSAP::ARP: { + if (m_rfPduUserDataLength < P25_PDU_ARP_PCKT_LENGTH) { + LogWarning(LOG_RF, P25_PDU_STR ", ignoring truncated ARP payload, length = %u", + m_rfPduUserDataLength); + break; + } + /* bryanb: quick and dirty ARP logging */ uint8_t arpPacket[P25_PDU_ARP_PCKT_LENGTH]; ::memset(arpPacket, 0x00U, P25_PDU_ARP_PCKT_LENGTH); @@ -321,9 +340,10 @@ bool Data::process(uint8_t* data, uint32_t len) m_rfAssembler->dataHeader.getBlocksToFollow()); } - processConvDataReg(m_rfPduUserData); - writeNet_PDU_User(m_rfAssembler->dataHeader, m_rfAssembler->getExtendedAddress(), m_rfAssembler->getAuxiliaryES(), - m_rfPduUserData); + if (processConvDataReg(m_rfPduUserData, m_rfPduUserDataLength)) { + writeNet_PDU_User(m_rfAssembler->dataHeader, m_rfAssembler->getExtendedAddress(), m_rfAssembler->getAuxiliaryES(), + m_rfPduUserData); + } } break; case PDUSAP::UNENC_KMM: @@ -511,9 +531,15 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint8_t currentBlock, uin // handle standard P25 service access points switch (sap) { - case PDUSAP::ARP: - { - /* bryanb: quick and dirty ARP logging */ + case PDUSAP::ARP: + { + if (m_netPduUserDataLength < P25_PDU_ARP_PCKT_LENGTH) { + LogWarning(LOG_NET, P25_PDU_STR ", ignoring truncated ARP payload, length = %u", + m_netPduUserDataLength); + break; + } + + /* bryanb: quick and dirty ARP logging */ uint8_t arpPacket[P25_PDU_ARP_PCKT_LENGTH]; ::memset(arpPacket, 0x00U, P25_PDU_ARP_PCKT_LENGTH); ::memcpy(arpPacket, m_netPduUserData, P25_PDU_ARP_PCKT_LENGTH); @@ -581,24 +607,6 @@ void Data::resetReceivedBlocks() m_netTotalBlocks = 0U; } -/* Helper to check if a logical link ID has registered with data services. */ - -bool Data::hasLLIdFNEReg(uint32_t llId) const -{ - // lookup dynamic FNE registration table entry - try { - ulong64_t tblIpAddr = m_fneRegTable.at(llId); - if (tblIpAddr != 0U) { - return true; - } - else { - return false; - } - } catch (...) { - return false; - } -} - /* Helper to write user data as a P25 PDU packet. */ void Data::writeRF_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData, bool imm) @@ -610,7 +618,8 @@ void Data::writeRF_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, uint32_t bitLength = 0U; UInt8Array data = m_rfAssembler->assemble(dataHeader, extendedAddress, auxiliaryES, pduUserData, &bitLength); - writeRF_PDU(data.get(), bitLength, imm); + bool trackRetry = dataHeader.getFormat() == PDUFormatType::CONFIRMED && dataHeader.getAckNeeded(); + writeRF_PDU(data.get(), bitLength, imm, false, dataHeader.getLLId(), trackRetry); } /* Helper to write user data as a P25 PDU packet. */ @@ -775,14 +784,11 @@ Data::Data(Control* p25, bool dumpPDUData, bool repeatPDU, bool debug, bool verb m_netReceivedBlocks(), m_netDataBlockCnt(0U), m_netTotalBlocks(0U), - m_retryPDUData(nullptr), - m_retryPDUBitLength(0U), - m_retryCount(0U), + m_retryPDUState(), m_rfPduUserData(nullptr), m_rfPduUserDataLength(0U), m_netPduUserData(nullptr), m_netPduUserDataLength(0U), - m_fneRegTable(), m_sndcpStateTable(), m_sndcpReadyTimers(), m_sndcpStandbyTimers(), @@ -810,8 +816,6 @@ Data::Data(Control* p25, bool dumpPDUData, bool repeatPDU, bool debug, bool verb m_netPduUserData = new uint8_t[P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U]; ::memset(m_netPduUserData, 0x00U, P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U); - m_fneRegTable.clear(); - m_sndcpStateTable.clear(); m_sndcpReadyTimers.clear(); m_sndcpStandbyTimers.clear(); @@ -826,74 +830,39 @@ Data::~Data() delete[] m_rfPDU; - if (m_retryPDUData != nullptr) - delete[] m_retryPDUData; - delete[] m_rfPduUserData; delete[] m_netPduUserData; } /* Helper used to process conventional data registration from PDU data. */ -bool Data::processConvDataReg(const uint8_t* pduUserData) +bool Data::processConvDataReg(const uint8_t* pduUserData, uint32_t length) { - uint8_t regType = (pduUserData[0U] >> 4) & 0x0F; - switch (regType) { - case PDURegType::CONNECT: - { - uint32_t llId = (pduUserData[1U] << 16) + (pduUserData[2U] << 8) + pduUserData[3U]; - uint32_t ipAddr = (pduUserData[8U] << 24) + (pduUserData[9U] << 16) + (pduUserData[10U] << 8) + pduUserData[11U]; - - if (m_verbose) { - LogInfoEx(LOG_RF, P25_PDU_STR ", CONNECT (Registration Request Connect), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); - } - - if (!acl::AccessControl::validateSrcId(llId)) { - LogWarning(LOG_RF, P25_PDU_STR ", DENY (Registration Response Deny), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); - writeRF_PDU_Reg_Response(PDURegType::DENY, llId, ipAddr); - } - else { - if (!hasLLIdFNEReg(llId)) { - // update dynamic FNE registration table entry - m_fneRegTable[llId] = ipAddr; - } - - if (m_verbose) { - LogInfoEx(LOG_RF, P25_PDU_STR ", ACCEPT (Registration Response Accept), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); - } - - writeRF_PDU_Reg_Response(PDURegType::ACCEPT, llId, ipAddr); - } + if (pduUserData == nullptr || length < ConventionalRegistration::DISCONNECT_LENGTH) { + LogError(LOG_RF, P25_PDU_STR ", truncated conventional registration payload, length = %u", length); + return false; } - break; - case PDURegType::DISCONNECT: - { - uint32_t llId = (pduUserData[1U] << 16) + (pduUserData[2U] << 8) + pduUserData[3U]; - if (m_verbose) { - LogInfoEx(LOG_RF, P25_PDU_STR ", DISCONNECT (Registration Request Disconnect), llId = %u", llId); - } + ConventionalRegistration registration; + if (!ConventionalRegistration::decode(pduUserData, length, registration)) { + LogError(LOG_RF, P25_PDU_STR ", invalid conventional registration payload"); + return false; + } - // acknowledge - writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfAssembler->dataHeader.getNs(), llId, false); + if (registration.type != PDURegType::CONNECT && + registration.type != PDURegType::DISCONNECT) + return false; - if (hasLLIdFNEReg(llId)) { - // remove dynamic FNE registration table entry - try { - m_fneRegTable.at(llId); - m_fneRegTable.erase(llId); - } - catch (...) { - // stub - } - } - } - break; - default: - LogError(LOG_RF, "P25 unhandled PDU registration type, regType = $%02X", regType); - break; + uint32_t headerLlId = m_rfAssembler->getExtendedAddress() ? + m_rfAssembler->dataHeader.getSrcLLId() : m_rfAssembler->dataHeader.getLLId(); + if (headerLlId != 0U && headerLlId != registration.llId) { + LogWarning(LOG_RF, P25_PDU_STR ", conventional registration identity mismatch, header = %u, payload = %u", + headerLlId, registration.llId); + return false; } + // authorization and the ACCEPT/DENY response are FNE-owned -- returning true + // here only permits this validated request to be forwarded to that service return true; } @@ -971,7 +940,8 @@ void Data::writeNetwork(const uint8_t currentBlock, const uint8_t* data, uint32_ /* Helper to write a P25 PDU packet. */ -void Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ackRetry) +void Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ackRetry, + uint32_t retryLlId, bool trackRetry) { assert(pdu != nullptr); assert(bitLength > 0U); @@ -981,22 +951,18 @@ void Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ac for (uint8_t i = 0U; i < 5U; i++) m_p25->writeRF_Nulls(); - if (!ackRetry) { - if (m_retryPDUData != nullptr) - delete m_retryPDUData; - - // store PDU for ACK RETRY logic - m_retryCount = 0U; - m_retryPDUBitLength = bitLength; + if (!ackRetry && trackRetry && retryLlId != 0U) { + // store confirmed transactions independently per logical link uint32_t retryByteLength = bitLength / 8U; - if ((retryByteLength % 8U) > 0U) + if ((bitLength % 8U) > 0U) retryByteLength++; - m_retryPDUData = new uint8_t[retryByteLength]; - ::memcpy(m_retryPDUData, pdu, retryByteLength); - } else { - LogInfoEx(LOG_RF, P25_PDU_STR ", OSP, ack retry, bitLength = %u", - m_retryPDUBitLength); + RetryPDUState& retry = m_retryPDUState[retryLlId]; + retry.data.assign(pdu, pdu + retryByteLength); + retry.bitLength = bitLength; + retry.retryCount = 0U; + } else if (ackRetry) { + LogInfoEx(LOG_RF, P25_PDU_STR ", OSP, ack retry, bitLength = %u", bitLength); } uint8_t data[P25_PDU_FRAME_LENGTH_BYTES + 2U]; @@ -1036,7 +1002,9 @@ void Data::writeNet_PDU_Buffered() { uint32_t bitLength = 0U; UInt8Array data = m_rfAssembler->assemble(m_netAssembler->dataHeader, m_netAssembler->getExtendedAddress(), m_netAssembler->getAuxiliaryES(), m_netPduUserData, &bitLength); - writeRF_PDU(data.get(), bitLength); + bool trackRetry = m_netAssembler->dataHeader.getFormat() == PDUFormatType::CONFIRMED && m_netAssembler->dataHeader.getAckNeeded(); + + writeRF_PDU(data.get(), bitLength, false, false, m_netAssembler->dataHeader.getLLId(), trackRetry); } /* Helper to re-write a received P25 PDU packet. */ @@ -1048,44 +1016,6 @@ void Data::writeRF_PDU_Buffered() writeRF_PDU(data.get(), bitLength); } -/* Helper to write a PDU registration response. */ - -void Data::writeRF_PDU_Reg_Response(uint8_t regType, uint32_t llId, uint32_t ipAddr) -{ - if ((regType != PDURegType::ACCEPT) && (regType != PDURegType::DENY)) - return; - - uint8_t pduUserData[P25_MAX_PDU_BLOCKS * P25_PDU_UNCONFIRMED_LENGTH_BYTES]; - ::memset(pduUserData, 0x00U, P25_MAX_PDU_BLOCKS * P25_PDU_UNCONFIRMED_LENGTH_BYTES); - - DataHeader rspHeader = DataHeader(); - rspHeader.setFormat(PDUFormatType::CONFIRMED); - rspHeader.setMFId(m_rfAssembler->dataHeader.getMFId()); - rspHeader.setAckNeeded(true); - rspHeader.setOutbound(true); - rspHeader.setSAP(PDUSAP::CONV_DATA_REG); - rspHeader.setSynchronize(true); - rspHeader.setLLId(llId); - rspHeader.setBlocksToFollow(1U); - - pduUserData[0U] = ((regType & 0x0FU) << 4); // Registration Type & Options - pduUserData[1U] = (llId >> 16) & 0xFFU; // Logical Link ID - pduUserData[2U] = (llId >> 8) & 0xFFU; - pduUserData[3U] = (llId >> 0) & 0xFFU; - if (regType == PDURegType::ACCEPT) { - pduUserData[8U] = (ipAddr >> 24) & 0xFFU; // IP Address - pduUserData[9U] = (ipAddr >> 16) & 0xFFU; - pduUserData[10U] = (ipAddr >> 8) & 0xFFU; - pduUserData[11U] = (ipAddr >> 0) & 0xFFU; - } - - if (m_dumpPDUData) - Utils::dump(1U, "P25, PDU Registration Response", pduUserData, 12U); - - rspHeader.calculateLength(12U); - writeRF_PDU_User(rspHeader, false, false, pduUserData); -} - /* Helper to write a PDU acknowledge response. */ void Data::writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, bool extendedAddress, uint32_t srcLlId) diff --git a/src/host/p25/packet/Data.h b/src/host/p25/packet/Data.h index 6c550270..2b4ad976 100644 --- a/src/host/p25/packet/Data.h +++ b/src/host/p25/packet/Data.h @@ -29,6 +29,7 @@ #include #include #include +#include namespace p25 { @@ -77,13 +78,6 @@ namespace p25 uint16_t totalBlocks); /** @} */ - /** - * @brief Helper to check if a logical link ID has registered with data services. - * @param llId Logical Link ID. - * @returns bool True, if ID has registered, otherwise false. - */ - bool hasLLIdFNEReg(uint32_t llId) const; - /** * @brief Helper to write user data as a P25 PDU packet. * @param dataHeader Instance of a PDU data header. @@ -142,17 +136,30 @@ namespace p25 uint16_t m_netDataBlockCnt; uint16_t m_netTotalBlocks; - uint8_t* m_retryPDUData; - uint32_t m_retryPDUBitLength; - uint8_t m_retryCount; + /** + * @brief Structure to maintain the retry state of a PDU. + */ + struct RetryPDUState { + /** + * @brief Buffer containing the PDU data. + */ + std::vector data; + /** + * @brief Length of the PDU in bits. + */ + uint32_t bitLength = 0U; + /** + * @brief Number of times the PDU has been retried. + */ + uint8_t retryCount = 0U; + }; + std::unordered_map m_retryPDUState; uint8_t* m_rfPduUserData; uint32_t m_rfPduUserDataLength; uint8_t* m_netPduUserData; uint32_t m_netPduUserDataLength; - std::unordered_map m_fneRegTable; - std::unordered_map m_sndcpStateTable; std::unordered_map m_sndcpReadyTimers; std::unordered_map m_sndcpStandbyTimers; @@ -184,7 +191,7 @@ namespace p25 * @param pduUserData Buffer containing user data to transmit. * @returns bool True, if conventional data registration data was processed, otherwise false. */ - bool processConvDataReg(const uint8_t* pduUserData); + bool processConvDataReg(const uint8_t* pduUserData, uint32_t length); /** * @brief Helper used to process SNDCP control data from PDU data. * @param pduUserData Buffer containing user data to transmit. @@ -213,7 +220,8 @@ namespace p25 * @param imm Flag indicating the PDU should be written to the immediate queue. * @param ackRetry Flag indicating the PDU is being sent as an acknowledged retry. */ - void writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm = false, bool ackRetry = false); + void writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm = false, + bool ackRetry = false, uint32_t retryLlId = 0U, bool trackRetry = false); /** * @brief Helper to write a network P25 PDU packet. * This will take buffered network PDU data and repeat it over the air. @@ -224,14 +232,6 @@ namespace p25 * This will take buffered received PDU data and repeat it over the air. */ void writeRF_PDU_Buffered(); - /** - * @brief Helper to write a PDU registration response. - * @param regType Registration Response. - * @param llId Logical Link ID. - * @param ipAddr - * @param mfId - */ - void writeRF_PDU_Reg_Response(uint8_t regType, uint32_t llId, uint32_t ipAddr); /** * @brief Helper to write a PDU acknowledge response. * @param ackClass Acknowledgement Class. diff --git a/tests/p25/P25_PacketDataState_Tests.cpp b/tests/p25/P25_PacketDataState_Tests.cpp new file mode 100644 index 00000000..5f550912 --- /dev/null +++ b/tests/p25/P25_PacketDataState_Tests.cpp @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * 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 "host/Defines.h" +#include "common/p25/P25Defines.h" +#include "common/p25/data/ConventionalRegistration.h" +#include "common/p25/data/ConventionalDataService.h" +#include "common/p25/data/IPConvergenceService.h" +#include "common/p25/data/IPv4Packet.h" +#include "common/p25/data/PacketDataState.h" + +using namespace p25::data; +using namespace p25::defines; + +#include + +#include +#include + +namespace +{ +std::array registrationPacket( + uint8_t type, uint32_t llId, uint32_t ipAddress, uint8_t options = 0U) +{ + ConventionalRegistration registration; + registration.type = type; + registration.options = options; + registration.llId = llId; + registration.ipAddress = ipAddress; + std::array data {}; + REQUIRE(registration.encode(data.data(), data.size())); + return data; +} // namespace + +std::vector ipv4Packet(uint32_t source, uint32_t destination, + uint16_t length = 20U) +{ + std::vector packet(length, 0U); + packet[0U] = 0x45U; + packet[2U] = uint8_t(length >> 8U); + packet[3U] = uint8_t(length); + packet[9U] = 17U; + packet[12U] = uint8_t(source >> 24U); + packet[13U] = uint8_t(source >> 16U); + packet[14U] = uint8_t(source >> 8U); + packet[15U] = uint8_t(source); + packet[16U] = uint8_t(destination >> 24U); + packet[17U] = uint8_t(destination >> 16U); + packet[18U] = uint8_t(destination >> 8U); + packet[19U] = uint8_t(destination); + return packet; +} +} + +TEST_CASE("P25 conventional registration codec is length safe", "[p25][packet-data][conventional][security]") +{ + ConventionalRegistration original; + original.type = PDURegType::CONNECT; + original.options = 0x05U; + original.llId = 0x54369FU; + original.ipAddress = 0x0A0A010AU; + + std::array encoded {}; + REQUIRE(original.encode(encoded.data(), encoded.size())); + + ConventionalRegistration decoded; + REQUIRE(ConventionalRegistration::decode(encoded.data(), encoded.size(), decoded)); + CHECK(decoded.type == original.type); + CHECK(decoded.options == original.options); + CHECK(decoded.llId == original.llId); + CHECK(decoded.ipAddress == original.ipAddress); + + for (uint32_t length = 0U; length < ConventionalRegistration::LENGTH; length++) + CHECK_FALSE(ConventionalRegistration::decode(encoded.data(), length, decoded)); + + CHECK_FALSE(ConventionalRegistration::decode(nullptr, encoded.size(), decoded)); + CHECK_FALSE(original.encode(nullptr, encoded.size())); + CHECK_FALSE(original.encode(encoded.data(), encoded.size() - 1U)); + + encoded[0U] = 0xF0U; + CHECK_FALSE(ConventionalRegistration::decode(encoded.data(), encoded.size(), decoded)); + + ConventionalRegistration disconnect; + disconnect.type = PDURegType::DISCONNECT; + disconnect.llId = original.llId; + std::array disconnectData {}; + REQUIRE(disconnect.encode(disconnectData.data(), disconnectData.size())); + REQUIRE(ConventionalRegistration::decode(disconnectData.data(), disconnectData.size(), decoded)); + CHECK(decoded.type == PDURegType::DISCONNECT); + CHECK(decoded.ipAddress == 0U); +} + +TEST_CASE("P25 packet data bindings preserve authorization and reject collisions", + "[p25][packet-data][state]") +{ + DataBindingRegistry registry; + + IPBinding first; + first.link.llId = 1001U; + first.ipAddress = 0x0A000001U; + first.accessMode = DataAccessMode::CONVENTIONAL; + first.convergenceMode = DataConvergenceMode::SCEP; + first.origin = DataBindingOrigin::CONVENTIONAL_REGISTRATION; + first.authorized = true; + REQUIRE(registry.upsert(first)); + + const IPBinding* byLink = registry.findByLLId(1001U); + REQUIRE(byLink != nullptr); + CHECK(byLink->ipAddress == first.ipAddress); + CHECK(byLink->authorized); + REQUIRE(registry.findByIPAddress(first.ipAddress) != nullptr); + + IPBinding collision = first; + collision.link.llId = 1002U; + CHECK_FALSE(registry.upsert(collision)); + + IPBinding invalid = first; + invalid.ipAddress = 0U; + CHECK_FALSE(registry.upsert(invalid)); + + CHECK(registry.erase(1001U)); + CHECK(registry.findByLLId(1001U) == nullptr); +} + +TEST_CASE("P25 LLC state is isolated by LLID", "[p25][packet-data][llc]") +{ + DataLinkManager links; + bool synchronize = false; + + CHECK(links.nextSendSequence(1001U, synchronize) == 0U); + CHECK(synchronize); + CHECK(links.nextSendSequence(2002U, synchronize) == 0U); + CHECK(links.nextSendSequence(1001U, synchronize) == 1U); + + for (uint8_t sequence = 2U; sequence < 8U; sequence++) + CHECK(links.nextSendSequence(1001U, synchronize) == sequence); + CHECK(links.nextSendSequence(1001U, synchronize) == 0U); + CHECK_FALSE(synchronize); + + uint8_t expected = 0U; + CHECK(links.acceptReceiveSequence(1001U, 0U, true, 11U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(links.acceptReceiveSequence(1001U, 0U, false, 11U, expected) == ReceiveSequenceResult::DUPLICATE); + CHECK(links.acceptReceiveSequence(1001U, 1U, false, 12U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(links.acceptReceiveSequence(1001U, 7U, false, 13U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(expected == 2U); + + links.setReady(1001U, false); + CHECK_FALSE(links.isReady(1001U)); + CHECK(links.isReady(2002U)); + links.erase(1001U); + CHECK_FALSE(links.hasState(1001U)); +} + +TEST_CASE("P25 LLC receive sequence wraps and suppresses duplicates", "[p25][packet-data][llc]") +{ + DataLinkManager links; + uint8_t expected = 0U; + + REQUIRE(links.acceptReceiveSequence(77U, 7U, false, 21U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(links.acceptReceiveSequence(77U, 0U, false, 22U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(links.acceptReceiveSequence(77U, 0U, false, 22U, expected) == ReceiveSequenceResult::DUPLICATE); + CHECK(links.acceptReceiveSequence(77U, 2U, false, 23U, expected) == ReceiveSequenceResult::ACCEPTED); + CHECK(expected == 1U); + + CHECK(links.acceptReceiveSequence(88U, 3U, true, 1U, expected) == ReceiveSequenceResult::OUT_OF_SEQUENCE); +} + +TEST_CASE("P25 IPv4 packet validation bounds declared length", "[p25][packet-data][ipv4][security]") +{ + std::array packet {}; + packet[0U] = 0x45U; + packet[2U] = 0x00U; + packet[3U] = 24U; + packet[9U] = 17U; + packet[12U] = 10U; + packet[15U] = 1U; + packet[16U] = 10U; + packet[19U] = 2U; + + IPv4Packet view; + REQUIRE(IPv4Packet::parse(packet.data(), packet.size(), view)); + CHECK(view.headerLength == 20U); + CHECK(view.totalLength == 24U); + CHECK(view.protocol == 17U); + CHECK(view.sourceAddress == 0x0A000001U); + CHECK(view.destinationAddress == 0x0A000002U); + + for (uint32_t length = 0U; length < 20U; length++) + CHECK_FALSE(IPv4Packet::parse(packet.data(), length, view)); + + packet[3U] = 25U; + CHECK_FALSE(IPv4Packet::parse(packet.data(), packet.size(), view)); + packet[3U] = 19U; + CHECK_FALSE(IPv4Packet::parse(packet.data(), packet.size(), view)); + packet[3U] = 24U; + packet[0U] = 0x46U; + CHECK_FALSE(IPv4Packet::parse(packet.data(), 20U, view)); + packet[0U] = 0x65U; + CHECK_FALSE(IPv4Packet::parse(packet.data(), packet.size(), view)); +} + +TEST_CASE("P25 conventional registration service owns binding transactions", + "[p25][packet-data][conventional][service]") +{ + DataBindingRegistry bindings; + DataLinkManager links; + ConventionalRegistrationPolicy policy; + policy.allowCMSScan = true; + policy.registrationLifetimeMs = 100U; + policy.allocateDynamicAddress = [](uint32_t llId, uint32_t) { + return 0x0A000000U | (llId & 0xFFU); + }; + ConventionalDataService service(bindings, links, policy); + + ConventionalRegistrationProvisioning staticProvisioning; + staticProvisioning.known = true; + staticProvisioning.enabled = true; + staticProvisioning.staticIPAddress = 0x0A000001U; + auto request = registrationPacket(PDURegType::CONNECT, 1U, 0U, 1U); + ConventionalRegistrationResult result = service.process(request.data(), request.size(), + 1U, staticProvisioning, 10U); + REQUIRE(result.decision == RegistrationDecision::ACCEPT); + CHECK(result.response.ipAddress == 0x0A000001U); + CHECK(result.response.options == 1U); + REQUIRE(bindings.findByLLId(1U) != nullptr); + + // Duplicate connects are idempotent and refresh the lifetime. + result = service.process(request.data(), request.size(), 1U, staticProvisioning, 50U); + CHECK(result.decision == RegistrationDecision::ACCEPT); + CHECK_FALSE(result.bindingChanged); + service.expire(120U); + CHECK(service.isRegistered(1U)); + service.expire(151U); + CHECK_FALSE(service.isRegistered(1U)); + + ConventionalRegistrationProvisioning dynamicProvisioning; + dynamicProvisioning.known = true; + dynamicProvisioning.enabled = true; + dynamicProvisioning.allowDynamicAddress = true; + request = registrationPacket(PDURegType::CONNECT, 2U, 0U); + result = service.process(request.data(), request.size(), 2U, dynamicProvisioning, 0U); + REQUIRE(result.decision == RegistrationDecision::ACCEPT); + CHECK(result.response.ipAddress == 0x0A000002U); + + // The same LLID may change its dynamic IP, but an IP cannot change owners. + request = registrationPacket(PDURegType::CONNECT, 2U, 0x0A000022U); + CHECK(service.process(request.data(), request.size(), 2U, dynamicProvisioning).decision == + RegistrationDecision::ACCEPT); + request = registrationPacket(PDURegType::CONNECT, 3U, 0x0A000022U); + CHECK(service.process(request.data(), request.size(), 3U, dynamicProvisioning).denyReason == + RegistrationDenyReason::ADDRESS_CONFLICT); + CHECK(bindings.findByLLId(3U) == nullptr); + + request = registrationPacket(PDURegType::DISCONNECT, 2U, 0U); + result = service.process(request.data(), request.size(), 2U, dynamicProvisioning); + CHECK(result.decision == RegistrationDecision::DISCONNECTED); + CHECK(result.bindingChanged); + result = service.process(request.data(), request.size(), 2U, dynamicProvisioning); + CHECK(result.decision == RegistrationDecision::DISCONNECTED); + CHECK_FALSE(result.bindingChanged); +} + +TEST_CASE("P25 conventional registration denies before changing shared state", + "[p25][packet-data][conventional][service][security]") +{ + DataBindingRegistry bindings; + DataLinkManager links; + ConventionalRegistrationPolicy policy; + policy.allocateDynamicAddress = [](uint32_t, uint32_t) { return 0U; }; + ConventionalDataService service(bindings, links, policy); + ConventionalRegistrationProvisioning provisioning; + auto request = registrationPacket(PDURegType::CONNECT, 10U, 0U); + + CHECK(service.process(request.data(), request.size(), 10U, provisioning).denyReason == + RegistrationDenyReason::NOT_PROVISIONED); + provisioning.known = true; + CHECK(service.process(request.data(), request.size(), 10U, provisioning).denyReason == + RegistrationDenyReason::DISABLED); + provisioning.enabled = true; + provisioning.allowDynamicAddress = true; + CHECK(service.process(request.data(), request.size(), 10U, provisioning).denyReason == + RegistrationDenyReason::ADDRESS_UNAVAILABLE); + + request = registrationPacket(PDURegType::CONNECT, 10U, 0x0A00000AU, 0x02U); + CHECK(service.process(request.data(), request.size(), 10U, provisioning).denyReason == + RegistrationDenyReason::UNSUPPORTED_OPTIONS); + CHECK(service.process(request.data(), request.size(), 11U, provisioning).denyReason == + RegistrationDenyReason::IDENTITY_MISMATCH); + CHECK(bindings.size() == 0U); + CHECK(service.registrationCount() == 0U); +} + +TEST_CASE("P25 SCEP enforces binding, IP ownership, MTU, and delivery policy", + "[p25][packet-data][scep][security]") +{ + DataBindingRegistry bindings; + DataLinkManager links; + ConventionalDataService registrations(bindings, links); + REQUIRE(registrations.installStatic(100U, 0x0A000064U)); + + SCEPPolicy policy; + policy.mtu = 100U; + policy.allowBroadcast = true; + policy.allowMulticast = false; + policy.unicastDelivery = DataDeliveryMode::CONFIRMED; + policy.broadcastDelivery = DataDeliveryMode::UNCONFIRMED; + SCEPService scep(bindings, policy); + + std::vector packet = ipv4Packet(0x0A000064U, 0x0A000065U); + IPDecodeResult decoded = scep.decode(packet.data(), packet.size()); + REQUIRE(decoded.result == ConvergenceResult::OK); + CHECK(scep.authorizeUplink(100U, decoded.packet) == ConvergenceResult::OK); + CHECK(scep.authorizeUplink(101U, decoded.packet) == ConvergenceResult::NO_BINDING); + + packet = ipv4Packet(0x0A000099U, 0x0A000065U); + decoded = scep.decode(packet.data(), packet.size()); + CHECK(scep.authorizeUplink(100U, decoded.packet) == ConvergenceResult::SOURCE_MISMATCH); + + REQUIRE(registrations.installStatic(101U, 0x0A000065U)); + IPEncodeResult encoded = scep.encode(packet.data(), packet.size()); + CHECK(encoded.result == ConvergenceResult::OK); + CHECK(encoded.llId == 101U); + CHECK(encoded.delivery == DataDeliveryMode::CONFIRMED); + + packet = ipv4Packet(0x0A000064U, 0xFFFFFFFFU); + encoded = scep.encode(packet.data(), packet.size()); + CHECK(encoded.result == ConvergenceResult::OK); + CHECK(encoded.llId == WUID_ALL); + CHECK(encoded.delivery == DataDeliveryMode::UNCONFIRMED); + + packet = ipv4Packet(0x0A000064U, 0xE0000001U); + CHECK(scep.encode(packet.data(), packet.size()).result == ConvergenceResult::POLICY_REJECTED); + decoded = scep.decode(packet.data(), packet.size()); + CHECK(scep.authorizeUplink(100U, decoded.packet) == ConvergenceResult::POLICY_REJECTED); + + packet = ipv4Packet(0x0A000064U, 0x0A000065U, 101U); + CHECK(scep.decode(packet.data(), packet.size()).result == ConvergenceResult::MTU_EXCEEDED); + + // ARP-like observations are not inputs to SCEP authorization; only typed + // provisioned or registered bindings are visible to this service. + DataBindingRegistry emptyBindings; + SCEPService noBinding(emptyBindings, policy); + packet = ipv4Packet(0x0A000064U, 0x0A000065U); + decoded = noBinding.decode(packet.data(), packet.size()); + CHECK(noBinding.authorizeUplink(100U, decoded.packet) == ConvergenceResult::NO_BINDING); + CHECK(noBinding.encode(packet.data(), packet.size()).result == ConvergenceResult::NO_BINDING); +}