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__
|
||||
@ -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…
Reference in new issue