perform refactor for how convential CAI P25 Data and SCEP is handled;

pull/133/head
Bryan Biedenkapp 1 month ago
parent 10e681d87f
commit b0e2903fb6

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

@ -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 <cstdint>
#include <functional>
#include <unordered_map>
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<uint32_t(uint32_t, uint32_t)> 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<uint32_t, RegistrationState> 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__

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

@ -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 <cstdint>
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__

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

@ -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 <cstdint>
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__

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

@ -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 <cstdint>
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__

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

@ -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 <cstdint>
#include <unordered_map>
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<uint32_t, IPBinding> 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<uint32_t, DataLinkState> m_states;
};
} // namespace data
} // namespace p25
#endif // __P25_DATA__PACKETDATASTATE_H__

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

@ -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<const uint32_t, uint32_t> ArpTablePair;
std::unordered_map<uint32_t, uint32_t> m_arpTable;
typedef std::pair<const uint32_t, bool> ReadyForNextPktPair;
std::unordered_map<uint32_t, bool> m_readyForNextPkt;
std::unordered_map<uint32_t, uint8_t> m_suSendSeq; // V(S) send state variable per LLId
std::unordered_map<uint32_t, uint8_t> 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;

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

@ -29,6 +29,7 @@
#include <cstdio>
#include <string>
#include <unordered_map>
#include <vector>
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<uint8_t> 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<uint32_t, RetryPDUState> m_retryPDUState;
uint8_t* m_rfPduUserData;
uint32_t m_rfPduUserDataLength;
uint8_t* m_netPduUserData;
uint32_t m_netPduUserDataLength;
std::unordered_map<uint32_t, uint32_t> m_fneRegTable;
std::unordered_map<uint32_t, defines::SNDCPState::E> m_sndcpStateTable;
std::unordered_map<uint32_t, Timer> m_sndcpReadyTimers;
std::unordered_map<uint32_t, Timer> 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.

@ -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 <catch2/catch_test_macros.hpp>
#include <array>
#include <vector>
namespace
{
std::array<uint8_t, ConventionalRegistration::LENGTH> 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<uint8_t, ConventionalRegistration::LENGTH> data {};
REQUIRE(registration.encode(data.data(), data.size()));
return data;
} // namespace
std::vector<uint8_t> ipv4Packet(uint32_t source, uint32_t destination,
uint16_t length = 20U)
{
std::vector<uint8_t> 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<uint8_t, ConventionalRegistration::LENGTH> 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<uint8_t, ConventionalRegistration::DISCONNECT_LENGTH> 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<uint8_t, 24U> 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<uint8_t> 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);
}
Loading…
Cancel
Save

Powered by TurnKey Linux.