refactor how TrafficNetwork and MetadataNetwork handle packet NET_FUNC opcodes;

pull/128/head
Bryan Biedenkapp 3 months ago
parent c731b9113a
commit a5bebe87f8

@ -16,6 +16,8 @@ file(GLOB dvmfne_SRC
"src/fne/network/callhandler/packetdata/*.cpp"
"src/fne/network/influxdb/*.h"
"src/fne/network/influxdb/*.cpp"
"src/fne/network/packet/*.h"
"src/fne/network/packet/*.cpp"
"src/fne/network/*.h"
"src/fne/network/*.cpp"
"src/fne/restapi/*.h"

File diff suppressed because it is too large Load Diff

@ -34,6 +34,12 @@ class HOST_SW_API HostFNE;
namespace network
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const uint32_t TIMEOUT_MAX_REPL = 5000U; // 5 seconds
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
@ -161,6 +167,78 @@ namespace network
*/
static void erasePacketBufferEntry(PacketBufferMap& pktMap, uint32_t peerId);
using PacketHandlerFunc = void (*)(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Implements the packet handler functions for the MetadataNetwork class.
*/
class PacketHandler {
public:
/**
* @brief Handles NET_FUNC::TRANSFER packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void transfer(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Handles NET_FUNC::ANNOUNCE packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void announce(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Handles NET_FUNC::KEYS_INVENTORY packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void keysInventory(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Handles NET_FUNC::KEYS_UPDATE packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void keysUpdate(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Handles NET_FUNC::REPL packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void replication(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
/**
* @brief Handles NET_FUNC::NET_TREE packets.
* @param network Instance of the TrafficNetwork class.
* @param mdNetwork Instance of the MetadataNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID of the packet.
* @param ssrc SSRC of the packet.
* @param streamId Stream ID of the packet.
*/
static void networkTree(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId);
};
/**
* @brief Entry point to process a given network packet.
* @param req Instance of the NetPacketRequest structure.

File diff suppressed because it is too large Load Diff

@ -48,6 +48,7 @@
#include <unordered_map>
#include <memory>
#include <mutex>
#include <array>
// ---------------------------------------------------------------------------
// Class Prototypes
@ -95,6 +96,9 @@ namespace network
#define INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS "illegal/unknown RID attempted access"
#define INFLUXDB_ERRSTR_CALL_NOT_PERMITTED "call not permitted for talkgroup"
const uint32_t MAX_HARD_CONN_CAP = 250U;
const size_t PEER_STATE_LOCK_STRIPES = 256U;
// ---------------------------------------------------------------------------
// Class Prototypes
// ---------------------------------------------------------------------------
@ -452,12 +456,139 @@ namespace network
bool m_reportPeerPing;
bool m_verbose;
static std::array<std::mutex, PEER_STATE_LOCK_STRIPES> s_peerStateLocks;
/**
* @brief Gets the mutex for a specific peer ID.
* @param peerId The ID of the peer.
* @return A reference to the mutex associated with the peer ID.
*/
static std::mutex& getPeerStateLock(uint32_t peerId)
{
return s_peerStateLocks[peerId % PEER_STATE_LOCK_STRIPES];
}
/**
* @brief Entry point to parrot handler thread.
* @param arg Instance of the thread_t structure.
* @returns void* (Ignore)
*/
static void* threadParrotHandler(void* arg);
using PacketHandlerFunc = void (*)(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Implements the packet handler functions for the MetadataNetwork class.
*/
class PacketHandler {
public:
/**
* @brief Handles NET_FUNC::PROTOCOL packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void protocol(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::RPTL packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void repeaterLogin(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::RPTK packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void repeaterAuth(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::RPTC packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void repeaterConfig(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::RPT_DISC packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void repeaterDisconnect(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::PING packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void ping(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::GRANT_REQ packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void grantRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::INCALL_CTRL packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void inCallControl(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::KEY_REQ packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void keyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
/**
* @brief Handles NET_FUNC::KEY_LLA_REQ packets.
* @param network Instance of the TrafficNetwork class.
* @param req Instance of the NetPacketRequest structure.
* @param peerId Peer ID.
* @param ssrc RTP synchronization source ID.
* @param streamId Stream ID.
* @param now Current time in milliseconds.
*/
static void llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now);
};
/**
* @brief Entry point to process a given network packet.
* @param req Instance of the NetPacketRequest structure.

@ -0,0 +1,340 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::ANNOUNCE packets. */
void MetadataNetwork::PacketHandler::announce(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)mdNetwork;
// process incoming message subfunction opcodes
switch (req->fneHeader.getSubFunction()) {
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL: // Announce Group Affiliation
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
uint32_t dstId = GET_UINT24(req->buffer, 3U); // Destination Address
aff->groupUnaff(srcId);
aff->groupAff(srcId, dstId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_AFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG: // Announce Unit Registration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitReg(srcId, ssrc);
network->m_globalAff->unitReg(srcId, ssrc);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true, 0U, ssrc);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG: // Announce Unit Deregistration
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->unitDereg(srcId);
network->m_globalAff->unitDereg(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_DEREG },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL: // Announce Group Affiliation Removal
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && aff != nullptr) {
uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address
aff->groupUnaff(srcId);
network->m_globalAff->groupUnaff(srcId);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_GRP_UNAFFIL },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_AFFILS: // Announce Update All Affiliations
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
if (aff != nullptr) {
aff->clearGroupAff(0U, true);
// update TGID lists
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t srcId = GET_UINT24(req->buffer, offs);
uint32_t dstId = GET_UINT24(req->buffer, offs + 4U);
aff->groupAff(srcId, dstId);
network->m_globalAff->groupAff(srcId, dstId);
offs += 8U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u affiliations", peerId, connection->identWithQualifier().c_str(), len);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_AFFILS },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REGS: // Announce Update All Unit Registrations
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::shared_ptr<fne_lookups::AffiliationLookup> aff = network->getPeerAffiliations(peerId);
if (aff == nullptr) {
LogError(LOG_MASTER, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identWithQualifier().c_str());
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID);
}
if (aff != nullptr) {
aff->clearUnitReg();
// update unit registration lists
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t srcId = GET_UINT24(req->buffer, offs);
aff->unitReg(srcId, ssrc);
network->m_globalAff->unitReg(srcId, ssrc);
offs += 3U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u unit registrations", peerId, connection->identWithQualifier().c_str(), len);
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REGS },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
case NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC: // Announce Site VCs
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
std::vector<uint32_t> vcPeers;
// update peer association
uint32_t len = GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t vcPeerId = GET_UINT32(req->buffer, offs);
if (vcPeerId > 0 && (network->m_peers.find(vcPeerId) != network->m_peers.end())) {
FNEPeerConnection* vcConnection = network->m_peers[vcPeerId];
if (vcConnection != nullptr) {
vcConnection->ccPeerId(peerId);
vcPeers.push_back(vcPeerId);
}
}
offs += 4U;
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) announced %u VCs", peerId, connection->identWithQualifier().c_str(), len);
network->m_ccPeerMap[peerId] = vcPeers;
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_SITE_VC },
req->buffer, req->length, req->rtpHeader.getSequence(), streamId, true);
}
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
default:
{
LogWarning(LOG_MASTER, "PEER %u, unknown/unsupported announcement opcode %u", peerId, req->fneHeader.getSubFunction());
if (network->m_debug)
Utils::dump("Unknown/unsupported announcement opcode from the peer", req->buffer, req->length);
}
break;
}
}

@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/TagDMRData.h"
#include "network/callhandler/TagP25Data.h"
#include "network/callhandler/TagNXDNData.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
using namespace network::callhandler;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::GRANT_REQ packets. */
void TrafficNetwork::PacketHandler::grantRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
uint32_t srcId = GET_UINT24(req->buffer, 11U); // Source Address
uint32_t dstId = GET_UINT24(req->buffer, 15U); // Destination Address
uint8_t slot = req->buffer[19U];
bool unitToUnit = (req->buffer[19U] & 0x80U) == 0x80U;
DVM_STATE state = (DVM_STATE)req->buffer[20U]; // DVM Mode State
switch (state) {
case STATE_DMR:
if (network->m_dmrEnabled) {
if (network->m_tagDMR != nullptr) {
network->m_tagDMR->processGrantReq(srcId, dstId, slot, unitToUnit, peerId, req->rtpHeader.getSequence(), streamId);
} else {
network->writePeerNAK(peerId, streamId, TAG_DMR_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
break;
case STATE_P25:
if (network->m_p25Enabled) {
if (network->m_tagP25 != nullptr) {
network->m_tagP25->processGrantReq(srcId, dstId, unitToUnit, peerId, req->rtpHeader.getSequence(), streamId);
} else {
network->writePeerNAK(peerId, streamId, TAG_P25_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
break;
case STATE_NXDN:
if (network->m_nxdnEnabled) {
if (network->m_tagNXDN != nullptr) {
network->m_tagNXDN->processGrantReq(srcId, dstId, unitToUnit, peerId, req->rtpHeader.getSequence(), streamId);
} else {
network->writePeerNAK(peerId, streamId, TAG_NXDN_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
break;
default:
network->writePeerNAK(peerId, streamId, TAG_REPEATER_GRANT, NET_CONN_NAK_ILLEGAL_PACKET);
Utils::dump("Unknown state for grant request from the peer", req->buffer, req->length);
break;
}
}
}
}
}

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::INCALL_CTRL packets. */
void TrafficNetwork::PacketHandler::inCallControl(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (network->m_disallowInCallCtrl)
return;
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
NET_ICC::ENUM command = (NET_ICC::ENUM)req->buffer[10U];
uint32_t dstId = GET_UINT24(req->buffer, 11U);
uint8_t slot = req->buffer[14U];
network->processInCallCtrl(command, req->fneHeader.getSubFunction(), dstId, slot, peerId, ssrc, streamId);
}
}
}
}

@ -0,0 +1,202 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/p25/kmm/KMMFactory.h"
#include "common/AESCrypto.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEY_REQ packets. */
void TrafficNetwork::PacketHandler::keyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
using namespace p25::defines;
using namespace p25::kmm;
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
// is this peer allowed to request keys?
if (network->m_peerListLookup->getACL()) {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
return;
} else {
if (!peerEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but is not allowed, no response", peerId, connection->identWithQualifier().c_str());
return;
}
}
}
std::unique_ptr<KMMFrame> frame = KMMFactory::create(req->buffer + 11U);
if (frame == nullptr) {
LogWarning(LOG_MASTER, "PEER %u (%s), undecodable KMM frame from peer", peerId, connection->identWithQualifier().c_str());
return;
}
switch (frame->getMessageId()) {
case P25DEF::KMM_MessageType::MODIFY_KEY_CMD:
{
KMMModifyKey* modifyKey = static_cast<KMMModifyKey*>(frame.get());
if (modifyKey->getAlgId() > 0U && modifyKey->getKId() > 0U) {
uint32_t requestingRid = modifyKey->getSrcLLId();
if (requestingRid > 0U) {
lookups::RadioId ridEntry = network->m_ridLookup->find(requestingRid);
if (ridEntry.radioDefault()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u has no key policy entry, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
return;
}
if (!ridEntry.radioEnabled()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u is disabled, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
return;
}
if (!ridEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but RID %u cannot request keys, no response",
peerId, connection->identWithQualifier().c_str(), requestingRid);
return;
}
std::vector<uint16_t> allowedKIds = ridEntry.allowedKIds();
// check if this RID is allowed to request the KID in question
if (!allowedKIds.empty() && std::find(allowedKIds.begin(), allowedKIds.end(), modifyKey->getKId()) == allowedKIds.end()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key kID = $%04X but RID %u is not permitted for that key, no response",
peerId, connection->identWithQualifier().c_str(), modifyKey->getKId(), requestingRid);
return;
}
}
else {
if (!network->m_kmfAllowRID0 && connection->peerClass() != PEER_CONN_CLASS_NEIGHBOR) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key with RID 0 but such requests are not allowed, no response",
peerId, connection->identWithQualifier().c_str());
return;
}
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) requested enc. key, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
::EKCKeyItem keyItem = network->m_cryptoLookup->find(modifyKey->getKId());
if (!keyItem.isInvalid()) {
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);
if (network->m_debug) {
LogDebugEx(LOG_HOST, "TrafficNetwork::threadedNetworkRx()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::taskNetworkRx(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) local enc. key, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
// if configured to encrypt the key with a preshared key, do that now
if (network->m_kmfEncKeyRequest) {
uint8_t* encryptedKey = nullptr;
if (network->m_kmfPresharedKey != nullptr) {
crypto::AES aes = crypto::AES(crypto::AESKeyLength::AES_256);
encryptedKey = aes.encryptECB(key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES, network->m_kmfPresharedKey);
if (encryptedKey != nullptr) {
::memcpy(key, encryptedKey, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
keyLength = P25DEF::MAX_ENC_KEY_LENGTH_BYTES;
delete[] encryptedKey;
}
}
if (network->m_debug) {
LogDebugEx(LOG_HOST, "TrafficNetwork::threadedNetworkRx()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::taskNetworkRx(), Encrypted Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
}
// build response buffer
uint8_t buffer[DATA_PACKET_LENGTH];
::memset(buffer, 0x00U, DATA_PACKET_LENGTH);
KMMModifyKey modifyKeyRsp = KMMModifyKey();
modifyKeyRsp.setDecryptInfoFmt(network->m_kmfEncKeyRequest ? KMM_DECRYPT_PEER_ENC : KMM_DECRYPT_INSTRUCT_NONE);
modifyKeyRsp.setAlgId(modifyKey->getAlgId());
modifyKeyRsp.setKId(0U);
KeysetItem ks = KeysetItem();
ks.keysetId(1U);
ks.algId(modifyKey->getAlgId());
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(key, keyLength);
ks.push_back(ki);
modifyKeyRsp.setKeysetItem(ks);
modifyKeyRsp.encode(buffer + 11U);
network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U,
RTP_END_OF_CALL_SEQ, network->createStreamId());
} else {
// attempt to forward KMM key request to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
bool locked = network->s_keyQueueMutex.try_lock_for(std::chrono::milliseconds(60));
network->m_peerReplicaKeyQueue[peerId] = modifyKey->getKId();
if (locked)
network->s_keyQueueMutex.unlock();
peer.second->writeMaster({ NET_FUNC::KEY_REQ, NET_SUBFUNC::NOP },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false);
}
}
}
} else {
LogError(LOG_MASTER, "PEER %u (%s) no local key or container and no replica masters to forward request to, no response, algId = $%02X, kID = $%04X", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), modifyKey->getKId());
}
}
}
}
break;
default:
break;
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_REPEATER_KEY, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}

@ -0,0 +1,211 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/edac/SHA256.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEYS_INVENTORY packets. */
void MetadataNetwork::PacketHandler::keysInventory(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)ssrc;
if (!network->m_host->m_cryptoLookup->isRemoteAccessEnabled()) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory, but remote access is disabled, no response", peerId);
return;
}
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory but is not allowed, no response", peerId);
return;
} else {
if (!peerEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory but is not allowed, no response", peerId);
return;
}
}
// keys inventory operates differently from the rest of the network opcodes...and does not require
// an established connection to the master, so we will not validate the peer connection state here
if (peerId > 0 && !peerEntry.peerDefault()) {
if (req->length < 80) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory, but payload length was invalid (%u bytes), no response", peerId, req->length);
return;
}
// scope intentional
{
// get the peer password hash from the frame message
DECLARE_UINT8_ARRAY(peerHash, 32U);
::memcpy(peerHash, req->buffer + 8U, 32U);
uint8_t peerSalt[4U];
::memset(peerSalt, 0x00U, 4U);
::memcpy(peerSalt, req->buffer + 40U, 4U);
std::string passwordForPeer = network->m_password;
// check if the peer is in the peer ACL list
bool validAcl = true;
if (network->m_peerListLookup->getACL()) {
if (!network->m_peerListLookup->isPeerAllowed(peerId) && !network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "PEER %u RPTK, failed peer ACL check", peerId);
validAcl = false;
} else {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
validAcl = false; // default peer IDs are a no-no as they have no data thus fail ACL check
} else {
passwordForPeer = peerEntry.peerPassword();
if (passwordForPeer.length() == 0) {
passwordForPeer = network->m_password;
}
}
}
if (network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "Peer List ACL enabled, but we have an empty peer list? Passing all peers.");
validAcl = true;
}
}
if (validAcl) {
size_t size = passwordForPeer.size();
uint8_t* in = new uint8_t[size + sizeof(uint32_t)];
::memcpy(in, peerSalt, sizeof(uint32_t));
for (size_t i = 0U; i < size; i++)
in[i + sizeof(uint32_t)] = passwordForPeer.at(i);
uint8_t out[32U];
edac::SHA256 sha256;
sha256.buffer(in, (uint32_t)(size + sizeof(uint32_t)), out);
delete[] in;
// validate hash
bool validHash = false;
if (req->length >= 80) {
validHash = true;
for (uint8_t i = 0; i < 32U; i++) {
if (peerHash[i] != out[i]) {
validHash = false;
break;
}
}
}
if (!validHash) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory, but had invalid authentication, no response", peerId);
return;
}
} else {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory, but had invalid ACL, no response", peerId);
return;
}
}
// scope intentional
{
// get remote access password hash from the frame message
DECLARE_UINT8_ARRAY(remoteAccessHash, 32U);
::memcpy(remoteAccessHash, req->buffer + 44U, 32U);
uint8_t remoteSalt[4U];
::memset(remoteSalt, 0x00U, 4U);
::memcpy(remoteSalt, req->buffer + 76U, 4U);
std::string remoteAccessPassword = network->m_host->m_cryptoLookup->getRemotePassword();
size_t size = remoteAccessPassword.size();
uint8_t* in = new uint8_t[size + sizeof(uint32_t)];
::memcpy(in, remoteSalt, sizeof(uint32_t));
for (size_t i = 0U; i < size; i++)
in[i + sizeof(uint32_t)] = remoteAccessPassword.at(i);
uint8_t out[32U];
edac::SHA256 sha256;
sha256.buffer(in, (uint32_t)(size + sizeof(uint32_t)), out);
delete[] in;
// validate hash
bool validHash = false;
if (req->length >= 80) {
validHash = true;
for (uint8_t i = 0; i < 32U; i++) {
if (remoteAccessHash[i] != out[i]) {
validHash = false;
break;
}
}
}
if (!validHash) {
LogError(LOG_MASTER, "PEER %u requested enc. key inventory, but had invalid access authentication, no response", peerId);
return;
}
}
// scope intentional
{
// read entire file into buffer
std::stringstream b;
std::ifstream stream(network->m_host->m_cryptoLookup->filename(), std::ios::in | std::ios::binary);
uint32_t len = 0U;
UInt8Array bufferUInt8Array = nullptr;
uint8_t* buffer = nullptr;
if (stream.is_open()) {
stream.seekg(0, std::ios::end);
len = (uint32_t)stream.tellg();
stream.seekg(0, std::ios::beg);
bufferUInt8Array = std::make_unique<uint8_t[]>(len);
buffer = bufferUInt8Array.get();
::memset(buffer, 0x00U, len);
uint32_t i = 0U;
while (stream.peek() != EOF) {
buffer[i] = (uint8_t)stream.get();
i++;
}
stream.close();
}
PacketBuffer pkt(true, "Remote EKC, Key Inventory");
pkt.encode((uint8_t*)buffer, len);
LogInfoEx(LOG_REPL, "PEER %u Remote EKC, Key Inventory, blocks %u, streamId = %u", peerId, pkt.fragments.size(), streamId);
if (pkt.fragments.size() > 0U) {
for (auto frag : pkt.fragments) {
// violate most handling rules for responding to packets -- we need to directly respond to the calling peer as
// they may not be logged in as a standard peer
mdNetwork->m_frameQueue->write(frag.second->data, FRAG_SIZE, streamId, peerId, network->m_peerId, { NET_FUNC::KEYS_INVENTORY, NET_SUBFUNC::NOP },
0U, req->address, req->addrLen);
Thread::sleep(60U); // pace block transmission
}
}
pkt.clear();
}
}
}

@ -0,0 +1,265 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/edac/SHA256.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEYS_UPDATE packets. */
void MetadataNetwork::PacketHandler::keysUpdate(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)ssrc;
if (!network->m_host->m_cryptoLookup->isRemoteAccessEnabled()) {
LogError(LOG_MASTER, "PEER %u requested enc. key update, but remote access is disabled, no response", peerId);
return;
}
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
LogError(LOG_MASTER, "PEER %u requested enc. key update but is not allowed, no response", peerId);
return;
} else {
if (!peerEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u requested enc. key update but is not allowed, no response", peerId);
return;
}
}
// keys update operates differently from the rest of the network opcodes...and does not require
// an established connection to the master, so we will not validate the peer connection state here.
// update is a two-phase flow: (1) auth request frame (80 bytes), then (2) chunked PacketBuffer frames.
if (peerId > 0 && !peerEntry.peerDefault()) {
if (mdNetwork->m_peerKeyUpdatePkt.find(peerId) == mdNetwork->m_peerKeyUpdatePkt.end()) {
if (req->length < 80) {
LogError(LOG_MASTER, "PEER %u requested enc. key update, but payload length was invalid (%u bytes), no response", peerId, req->length);
return;
}
// scope intentional
{
// get the peer password hash from the frame message
DECLARE_UINT8_ARRAY(peerHash, 32U);
::memcpy(peerHash, req->buffer + 8U, 32U);
uint8_t peerSalt[4U];
::memset(peerSalt, 0x00U, 4U);
::memcpy(peerSalt, req->buffer + 40U, 4U);
std::string passwordForPeer = network->m_password;
// check if the peer is in the peer ACL list
bool validAcl = true;
if (network->m_peerListLookup->getACL()) {
if (!network->m_peerListLookup->isPeerAllowed(peerId) && !network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "PEER %u RPTK, failed peer ACL check", peerId);
validAcl = false;
} else {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
validAcl = false; // default peer IDs are a no-no as they have no data thus fail ACL check
} else {
passwordForPeer = peerEntry.peerPassword();
if (passwordForPeer.length() == 0) {
passwordForPeer = network->m_password;
}
}
}
if (network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "Peer List ACL enabled, but we have an empty peer list? Passing all peers.");
validAcl = true;
}
}
if (validAcl) {
size_t size = passwordForPeer.size();
uint8_t* in = new uint8_t[size + sizeof(uint32_t)];
::memcpy(in, peerSalt, sizeof(uint32_t));
for (size_t i = 0U; i < size; i++)
in[i + sizeof(uint32_t)] = passwordForPeer.at(i);
uint8_t out[32U];
edac::SHA256 sha256;
sha256.buffer(in, (uint32_t)(size + sizeof(uint32_t)), out);
delete[] in;
// validate hash
bool validHash = false;
if (req->length >= 80) {
validHash = true;
for (uint8_t i = 0; i < 32U; i++) {
if (peerHash[i] != out[i]) {
validHash = false;
break;
}
}
}
if (!validHash) {
LogError(LOG_MASTER, "PEER %u requested enc. key update, but had invalid authentication, no response", peerId);
return;
}
} else {
LogError(LOG_MASTER, "PEER %u requested enc. key update, but had invalid ACL, no response", peerId);
return;
}
}
// scope intentional
{
// get remote access password hash from the frame message
DECLARE_UINT8_ARRAY(remoteAccessHash, 32U);
::memcpy(remoteAccessHash, req->buffer + 44U, 32U);
uint8_t remoteSalt[4U];
::memset(remoteSalt, 0x00U, 4U);
::memcpy(remoteSalt, req->buffer + 76U, 4U);
std::string remoteAccessPassword = network->m_host->m_cryptoLookup->getRemotePassword();
size_t size = remoteAccessPassword.size();
uint8_t* in = new uint8_t[size + sizeof(uint32_t)];
::memcpy(in, remoteSalt, sizeof(uint32_t));
for (size_t i = 0U; i < size; i++)
in[i + sizeof(uint32_t)] = remoteAccessPassword.at(i);
uint8_t out[32U];
edac::SHA256 sha256;
sha256.buffer(in, (uint32_t)(size + sizeof(uint32_t)), out);
delete[] in;
// validate hash
bool validHash = false;
if (req->length >= 80) {
validHash = true;
for (uint8_t i = 0; i < 32U; i++) {
if (remoteAccessHash[i] != out[i]) {
validHash = false;
break;
}
}
}
if (!validHash) {
LogError(LOG_MASTER, "PEER %u requested enc. key update, but had invalid access authentication, no response", peerId);
return;
}
}
PacketBufferEntryPtr pkt = findOrCreatePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId, "Remote EKC, Key Update", streamId);
if (pkt == nullptr || !pkt->buffer) {
LogError(LOG_REPL, "PEER %u Remote EKC, Key Update, failed to initialize packet buffer", peerId);
erasePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
return;
}
LogInfoEx(LOG_REPL, "PEER %u Remote EKC, Key Update, authenticated transfer streamId = %u", peerId, streamId);
return;
}
// scope intentional
{
if (req->length < FRAG_SIZE) {
LogWarning(LOG_REPL, "PEER %u Remote EKC, Key Update, ignoring short data phase frame (%u bytes)", peerId, req->length);
return;
}
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "MetadataNetwork::taskNetworkRx(), KEYS_UPDATE, Raw Payload", rawPayload, req->length);
PacketBufferEntryPtr pkt = findPacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
if (pkt == nullptr || !pkt->buffer) {
return;
}
std::unique_lock<std::mutex> pktLock(pkt->mutex, std::defer_lock);
uint32_t timeout = 0U;
while (!pktLock.try_lock() && timeout < TIMEOUT_MAX_REPL) {
timeout++;
Thread::sleep(1U);
}
if (!pktLock.owns_lock()) {
LogError(LOG_STP, "PEER %u Remote EKC, Key Update, timeout waiting for packet buffer to unlock", peerId);
if (pkt->buffer) {
pkt->buffer->clear();
pkt->buffer.reset();
}
pkt->streamId = 0U;
erasePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
return;
}
if (pkt->streamId != streamId) {
LogError(LOG_REPL, "PEER %u Remote EKC, Key Update, stream ID mismatch, expected %u, got %u", peerId, pkt->streamId, streamId);
if (pkt->buffer) {
pkt->buffer->clear();
pkt->buffer.reset();
}
pkt->streamId = 0U;
erasePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
return;
}
uint32_t decompressedLen = 0U;
uint8_t* decompressed = nullptr;
if (pkt->buffer->decode(rawPayload, &decompressed, &decompressedLen)) {
std::ostringstream s;
s << network->m_cryptoLookup->filename();
std::string filename = s.str();
std::ofstream file(filename, std::ofstream::out);
if (file.fail()) {
LogError(LOG_PEER, "Cannot open the crypto container file - %s", filename.c_str());
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
return;
}
for (uint32_t i = 0U; i < decompressedLen; i++) {
file << (char)decompressed[i];
}
file.close();
network->m_cryptoLookup->stop(true);
network->m_cryptoLookup->reload();
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerKeyUpdatePkt, peerId);
}
}
}
}

@ -0,0 +1,173 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/p25/kmm/KMMFactory.h"
#include "common/AESCrypto.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::KEY_LLA_REQ packets. */
void TrafficNetwork::PacketHandler::llaKeyRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
using namespace p25::defines;
using namespace p25::kmm;
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
// is this peer allowed to request keys?
if (network->m_peerListLookup->getACL()) {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
return;
} else {
if (!peerEntry.canRequestKeys()) {
LogError(LOG_MASTER, "PEER %u (%s) requested enc. key but is not allowed, no response", peerId, connection->identWithQualifier().c_str());
return;
}
}
}
std::unique_ptr<KMMFrame> frame = KMMFactory::create(req->buffer + 11U);
if (frame == nullptr) {
LogWarning(LOG_MASTER, "PEER %u (%s), undecodable KMM frame from peer", peerId, connection->identWithQualifier().c_str());
return;
}
switch (frame->getMessageId()) {
case P25DEF::KMM_MessageType::MODIFY_KEY_CMD:
{
KMMModifyKey* modifyKey = static_cast<KMMModifyKey*>(frame.get());
if (modifyKey->getAlgId() == ALGO_AES_128 && modifyKey->getDstLLId() > 0U) {
if (network->m_debug)
LogDebugEx(LOG_MASTER, "TrafficNetwork::taskNetworkRx()", "PEER %u (%s) LLA enc. key request received, dstLLId = %u, algId = %u, kId = %u", peerId, connection->identWithQualifier().c_str(),
modifyKey->getDstLLId(), modifyKey->getAlgId(), modifyKey->getKId());
uint32_t requestingRid = modifyKey->getDstLLId();
LogInfoEx(LOG_MASTER, "PEER %u (%s) requested LLA enc. key, rsi = %u", peerId, connection->identWithQualifier().c_str(),
requestingRid);
::EKCKeyItem keyItem = network->m_cryptoLookup->findLLA(requestingRid);
if (!keyItem.isInvalid()) {
uint8_t key[P25DEF::MAX_ENC_KEY_LENGTH_BYTES];
::memset(key, 0x00U, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
uint8_t keyLength = keyItem.getKey(key);
if (network->m_debug) {
LogDebugEx(LOG_HOST, "TrafficNetwork::threadedNetworkRx()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::taskNetworkRx(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
LogInfoEx(LOG_MASTER, "PEER %u (%s) local enc. key, algId = $%02X, rsi = %u", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), requestingRid);
// if configured to encrypt the key with a preshared key, do that now
if (network->m_kmfEncKeyRequest) {
uint8_t* encryptedKey = nullptr;
if (network->m_kmfPresharedKey != nullptr) {
crypto::AES aes = crypto::AES(crypto::AESKeyLength::AES_256);
encryptedKey = aes.encryptECB(key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES, network->m_kmfPresharedKey);
if (encryptedKey != nullptr) {
::memcpy(key, encryptedKey, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
keyLength = P25DEF::MAX_ENC_KEY_LENGTH_BYTES;
delete[] encryptedKey;
}
}
if (network->m_debug) {
LogDebugEx(LOG_HOST, "TrafficNetwork::threadedNetworkRx()", "keyLength = %u", keyLength);
Utils::dump(1U, "TrafficNetwork::taskNetworkRx(), Encrypted Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES);
}
}
// build response buffer
uint8_t buffer[DATA_PACKET_LENGTH];
::memset(buffer, 0x00U, DATA_PACKET_LENGTH);
KMMModifyKey modifyKeyRsp = KMMModifyKey();
modifyKeyRsp.setDecryptInfoFmt(network->m_kmfEncKeyRequest ? KMM_DECRYPT_PEER_ENC : KMM_DECRYPT_INSTRUCT_NONE);
modifyKeyRsp.setAlgId(modifyKey->getAlgId());
modifyKeyRsp.setKId(0U);
modifyKeyRsp.setSrcLLId(WUID_FNE);
modifyKeyRsp.setDstLLId(requestingRid);
KeysetItem ks = KeysetItem();
ks.keysetId(1U);
ks.algId(modifyKey->getAlgId());
ks.keyLength(keyLength);
p25::kmm::KeyItem ki = p25::kmm::KeyItem();
ki.keyFormat(KEY_FORMAT_TEK);
ki.kId((uint16_t)keyItem.kId());
ki.sln((uint16_t)keyItem.sln());
ki.setKey(key, keyLength);
ks.push_back(ki);
modifyKeyRsp.setKeysetItem(ks);
modifyKeyRsp.encode(buffer + 11U);
network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_LLA_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U,
RTP_END_OF_CALL_SEQ, network->createStreamId());
} else {
// attempt to forward KMM key request to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto& peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
LogInfoEx(LOG_PEER, "PEER %u (%s) no local key or container, requesting key from upstream master, algId = $%02X, rsi = %u", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), requestingRid);
bool locked = network->s_llaKeyQueueMutex.try_lock_for(std::chrono::milliseconds(60));
network->m_peerReplicaLLAKeyQueue[peerId] = modifyKey->getDstLLId();
if (locked)
network->s_llaKeyQueueMutex.unlock();
peer.second->writeMaster({ NET_FUNC::KEY_LLA_REQ, NET_SUBFUNC::NOP },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, false);
}
}
}
} else {
LogError(LOG_MASTER, "PEER %u (%s) requested LLA enc. key with no local key and no upstream masters to query, algId = $%02X, rsi = %u, no response", peerId, connection->identWithQualifier().c_str(),
modifyKey->getAlgId(), requestingRid);
}
}
}
}
break;
default:
break;
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_REPEATER_KEY, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}

@ -0,0 +1,147 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::NET_TREE packets. */
void MetadataNetwork::PacketHandler::networkTree(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)ssrc;
if (!network->m_enableSpanningTree)
return;
if (req->fneHeader.getSubFunction() == NET_SUBFUNC::NET_TREE_LIST) { // FNE Network Tree List
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR) {
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "MetadataNetwork::taskNetworkRx(), NET_TREE_LIST, Raw Payload", rawPayload, req->length);
PacketBufferEntryPtr pkt = findOrCreatePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId, "Network Tree, Tree List", streamId);
if (pkt == nullptr || !pkt->buffer) {
LogError(LOG_STP, "PEER %u (%s) Network Tree, Tree List, failed to initialize packet buffer", peerId,
connection->identWithQualifier().c_str());
erasePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId);
return;
}
std::unique_lock<std::mutex> pktLock(pkt->mutex, std::defer_lock);
uint32_t timeout = 0U;
while (!pktLock.try_lock() && timeout < TIMEOUT_MAX_REPL) {
timeout++;
Thread::sleep(1U);
}
if (!pktLock.owns_lock()) {
LogError(LOG_STP, "PEER %u (%s) Network Tree, Tree List, timeout waiting for packet buffer to unlock", peerId,
connection->identWithQualifier().c_str());
if (pkt->buffer) {
pkt->buffer->clear();
pkt->buffer.reset();
}
pkt->streamId = 0U;
erasePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId);
return;
}
if (pkt->streamId != streamId) {
LogError(LOG_STP, "PEER %u (%s) Network Tree, Tree List, stream ID mismatch, expected %u, got %u", peerId,
connection->identWithQualifier().c_str(), pkt->streamId, streamId);
pkt->buffer->clear();
pkt->streamId = streamId;
}
uint32_t decompressedLen = 0U;
uint8_t* decompressed = nullptr;
if (pkt->buffer->decode(rawPayload, &decompressed, &decompressedLen)) {
std::string payload(decompressed + 8U, decompressed + decompressedLen);
// parse JSON body
json::value v;
std::string err = json::parse(v, payload);
if (!err.empty()) {
LogError(LOG_STP, "PEER %u (%s) error parsing network tree list, %s", peerId, connection->identWithQualifier().c_str(), err.c_str());
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId);
return;
}
else {
// ensure parsed JSON is an array
if (!v.is<json::array>()) {
LogError(LOG_STP, "PEER %u (%s) error parsing network tree list, data was not valid", peerId, connection->identWithQualifier().c_str());
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId);
return;
}
else {
json::array arr = v.get<json::array>();
LogInfoEx(LOG_STP, "PEER %u (%s) Network Tree, Tree List, updating %u peer entries", peerId, connection->identWithQualifier().c_str(), arr.size());
std::lock_guard<std::mutex> guard(network->m_treeLock);
std::vector<uint32_t> duplicatePeers;
SpanningTree::deserializeTree(arr, network->m_treeRoot, &duplicatePeers);
network->logSpanningTree(connection);
if (duplicatePeers.size() > 0U) {
for (auto dupPeerId : duplicatePeers) {
LogWarning(LOG_STP, "PEER %u (%s) Network Tree, Tree Change, disconnecting duplicate peer connection for PEER %u to prevent network loop",
peerId, connection->identWithQualifier().c_str(), dupPeerId);
network->writeTreeDisconnect(peerId, dupPeerId);
}
}
}
}
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerTreeListPkt, peerId);
}
}
else {
network->writePeerNAK(peerId, 0U, TAG_PEER_REPLICA, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
}

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::PING packets. */
void TrafficNetwork::PacketHandler::ping(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
uint32_t pingsRx = connection->pingsReceived();
uint64_t lastPing = connection->lastPing();
pingsRx++;
connection->pingsReceived(pingsRx);
connection->lastPing(now);
uint8_t payload[8U];
::memset(payload, 0x00U, 8U);
// split ulong64_t (8 byte) value into bytes
payload[0U] = (uint8_t)((now >> 56) & 0xFFU);
payload[1U] = (uint8_t)((now >> 48) & 0xFFU);
payload[2U] = (uint8_t)((now >> 40) & 0xFFU);
payload[3U] = (uint8_t)((now >> 32) & 0xFFU);
payload[4U] = (uint8_t)((now >> 24) & 0xFFU);
payload[5U] = (uint8_t)((now >> 16) & 0xFFU);
payload[6U] = (uint8_t)((now >> 8) & 0xFFU);
payload[7U] = (uint8_t)((now >> 0) & 0xFFU);
network->writePeerCommand(peerId, { NET_FUNC::PONG, NET_SUBFUNC::NOP }, payload, 8U, streamId, false);
if (network->m_reportPeerPing) {
LogInfoEx(LOG_MASTER, "PEER %u (%s) ping, pingsReceived = %u, lastPing = %u, now = %u", peerId, connection->identWithQualifier().c_str(),
connection->pingsReceived(), lastPing, now);
}
// ensure STP sanity, when we receive a ping from a downstream leaf
// this check ensures a STP entry for a downstream leaf isn't accidentally blown off
// the tree during a fast reconnect
if (network->m_enableSpanningTree && connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR) {
std::lock_guard<std::mutex> guard(network->m_treeLock);
if ((connection->masterId() != peerId) && (connection->masterId() != 0U)) {
// check if this peer is already connected via another peer
SpanningTree* tree = SpanningTree::findByMasterID(connection->masterId());
if (tree == nullptr) {
LogWarning(LOG_STP, "PEER %u (%s) downstream server not announced in server tree, reinitializing STP entry, this is abnormal, peerId = %u, masterId = %u, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
peerId, connection->masterId(), connection->connectionState());
SpanningTree* node = new SpanningTree(peerId, connection->masterId(), network->m_treeRoot);
node->identity(connection->identity());
network->logSpanningTree(connection);
}
}
}
}
else {
network->writePeerNAK(peerId, streamId, TAG_REPEATER_PING);
}
}
}
}

@ -0,0 +1,197 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/TagDMRData.h"
#include "network/callhandler/TagP25Data.h"
#include "network/callhandler/TagNXDNData.h"
#include "network/callhandler/TagAnalogData.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
using namespace network::callhandler;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::PROTOCOL packets. */
void TrafficNetwork::PacketHandler::protocol(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
// process incoming message subfunction opcodes
switch (req->fneHeader.getSubFunction()) {
case NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR: // Encapsulated DMR data frame
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
connection->lastPing(now);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
if (network->m_dmrEnabled) {
if (network->m_tagDMR != nullptr) {
// check if jitter buffer is enabled for this peer
if (connection->jitterBufferEnabled() && req->rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
std::vector<BufferedFrame*> readyFrames;
connection->processJitterFrame(streamId, req->rtpHeader.getSequence(), req->buffer, req->length, readyFrames);
// process all frames that are now ready (in sequence order)
for (BufferedFrame* frame : readyFrames) {
network->m_tagDMR->processFrame(frame->data, frame->length, peerId, ssrc, frame->seq, streamId);
delete frame;
}
} else {
// zero-latency fast path: no jitter buffer
network->m_tagDMR->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId);
}
}
} else {
network->writePeerNAK(peerId, streamId, TAG_DMR_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
}
}
else {
network->writePeerNAK(peerId, TAG_DMR_DATA, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
}
}
break;
case NET_SUBFUNC::PROTOCOL_SUBFUNC_P25: // Encapsulated P25 data frame
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
connection->lastPing(now);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
if (network->m_p25Enabled) {
if (network->m_tagP25 != nullptr) {
// check if jitter buffer is enabled for this peer
if (connection->jitterBufferEnabled() && req->rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
std::vector<BufferedFrame*> readyFrames;
connection->processJitterFrame(streamId, req->rtpHeader.getSequence(), req->buffer, req->length, readyFrames);
// process all frames that are now ready (in sequence order)
for (BufferedFrame* frame : readyFrames) {
network->m_tagP25->processFrame(frame->data, frame->length, peerId, ssrc, frame->seq, streamId);
delete frame;
}
} else {
// zero-latency fast path: no jitter buffer
network->m_tagP25->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId);
}
}
} else {
network->writePeerNAK(peerId, streamId, TAG_P25_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
}
}
else {
network->writePeerNAK(peerId, TAG_P25_DATA, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
}
}
break;
case NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN: // Encapsulated NXDN data frame
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
connection->lastPing(now);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
if (network->m_nxdnEnabled) {
if (network->m_tagNXDN != nullptr) {
// check if jitter buffer is enabled for this peer
if (connection->jitterBufferEnabled() && req->rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
std::vector<BufferedFrame*> readyFrames;
connection->processJitterFrame(streamId, req->rtpHeader.getSequence(), req->buffer, req->length, readyFrames);
// process all frames that are now ready (in sequence order)
for (BufferedFrame* frame : readyFrames) {
network->m_tagNXDN->processFrame(frame->data, frame->length, peerId, ssrc, frame->seq, streamId);
delete frame;
}
} else {
// zero-latency fast path: no jitter buffer
network->m_tagNXDN->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId);
}
}
} else {
network->writePeerNAK(peerId, streamId, TAG_NXDN_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
}
}
else {
network->writePeerNAK(peerId, TAG_NXDN_DATA, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
}
}
break;
case NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG: // Encapsulated analog data frame
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
connection->lastPing(now);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
if (network->m_analogEnabled) {
if (network->m_tagAnalog != nullptr) {
// check if jitter buffer is enabled for this peer
if (connection->jitterBufferEnabled() && req->rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
std::vector<BufferedFrame*> readyFrames;
connection->processJitterFrame(streamId, req->rtpHeader.getSequence(), req->buffer, req->length, readyFrames);
// process all frames that are now ready (in sequence order)
for (BufferedFrame* frame : readyFrames) {
network->m_tagAnalog->processFrame(frame->data, frame->length, peerId, ssrc, frame->seq, streamId);
delete frame;
}
} else {
// zero-latency fast path: no jitter buffer
network->m_tagAnalog->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId);
}
}
} else {
network->writePeerNAK(peerId, streamId, TAG_ANALOG_DATA, NET_CONN_NAK_MODE_NOT_ENABLED);
}
}
}
}
else {
network->writePeerNAK(peerId, TAG_ANALOG_DATA, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
}
}
break;
default:
Utils::dump("Unknown protocol opcode from peer", req->buffer, req->length);
break;
}
}

@ -0,0 +1,133 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/edac/SHA256.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::RPTK packets. */
void TrafficNetwork::PacketHandler::repeaterAuth(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
std::lock_guard<std::mutex> peerGuard(TrafficNetwork::getPeerStateLock(peerId));
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
connection->lastPing(now);
if (connection->connectionState() == NET_STAT_WAITING_AUTHORISATION) {
// get the hash from the frame message
DECLARE_UINT8_ARRAY(hash, req->length - 8U);
::memcpy(hash, req->buffer + 8U, req->length - 8U);
// generate our own hash
uint8_t salt[4U];
::memset(salt, 0x00U, 4U);
SET_UINT32(connection->salt(), salt, 0U);
std::string passwordForPeer = network->m_password;
// check if the peer is in the peer ACL list
bool validAcl = true;
if (network->m_peerListLookup->getACL()) {
if (!network->m_peerListLookup->isPeerAllowed(peerId) && !network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "PEER %u RPTK, failed peer ACL check", peerId);
validAcl = false;
} else {
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (peerEntry.peerDefault()) {
validAcl = false; // default peer IDs are a no-no as they have no data thus fail ACL check
} else {
passwordForPeer = peerEntry.peerPassword();
if (passwordForPeer.length() == 0) {
passwordForPeer = network->m_password;
}
}
}
if (network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "Peer List ACL enabled, but we have an empty peer list? Passing all peers.");
validAcl = true;
}
}
if (validAcl) {
size_t size = passwordForPeer.size();
uint8_t* in = new uint8_t[size + sizeof(uint32_t)];
::memcpy(in, salt, sizeof(uint32_t));
for (size_t i = 0U; i < size; i++)
in[i + sizeof(uint32_t)] = passwordForPeer.at(i);
uint8_t out[32U];
edac::SHA256 sha256;
sha256.buffer(in, (uint32_t)(size + sizeof(uint32_t)), out);
delete[] in;
// validate hash
bool validHash = false;
if (req->length - 8U == 32U) {
validHash = true;
for (uint8_t i = 0; i < 32U; i++) {
if (hash[i] != out[i]) {
validHash = false;
break;
}
}
}
if (validHash) {
connection->connectionState(NET_STAT_WAITING_CONFIG);
network->writePeerACK(peerId, streamId);
LogInfoEx(LOG_MASTER, "PEER %u RPTK ACK, completed the login exchange", peerId);
network->m_peers[peerId] = connection;
}
else {
LogWarning(LOG_MASTER, "PEER %u RPTK NAK, failed the login exchange", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
} else {
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_PEER_ACL, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
}
else {
// perform source address/port validation
if (connection->address() != udp::Socket::address(req->address) ||
connection->port() != udp::Socket::port(req->address)) {
LogError(LOG_MASTER, "PEER %u RPTK NAK, IP address/port mismatch on RPTK attempt while in an incorrect state, old = %s:%u, new = %s:%u, connectionState = %u", peerId,
connection->address().c_str(), connection->port(), udp::Socket::address(req->address).c_str(), udp::Socket::port(req->address), connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
return;
}
LogWarning(LOG_MASTER, "PEER %u RPTK NAK, login exchange while in an incorrect state, connectionState = %u", peerId, connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
}
}
else {
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
network->erasePeer(peerId);
LogWarning(LOG_MASTER, "PEER %u RPTK NAK, having no connection", peerId);
}
}

@ -0,0 +1,273 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::RPTC packets. */
void TrafficNetwork::PacketHandler::repeaterConfig(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
std::lock_guard<std::mutex> peerGuard(TrafficNetwork::getPeerStateLock(peerId));
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
connection->lastPing(now);
if (connection->connectionState() == NET_STAT_WAITING_CONFIG) {
DECLARE_UINT8_ARRAY(rawPayload, req->length - 8U);
::memcpy(rawPayload, req->buffer + 8U, req->length - 8U);
std::string payload(rawPayload, rawPayload + (req->length - 8U));
// parse JSON body
json::value v;
std::string err = json::parse(v, payload);
if (!err.empty()) {
LogWarning(LOG_MASTER, "PEER %u RPTC NAK, supplied invalid configuration data", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_INVALID_CONFIG_DATA, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
else {
// ensure parsed JSON is an object
if (!v.is<json::object>()) {
LogWarning(LOG_MASTER, "PEER %u RPTC NAK, supplied invalid configuration data", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_INVALID_CONFIG_DATA, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
else {
connection->config(v.get<json::object>());
connection->connectionState(NET_STAT_RUNNING);
connection->connected(true);
connection->pingsReceived(0U);
connection->lastPing(now);
connection->missedMetadataUpdates(0U);
lookups::PeerId peerEntry = network->m_peerListLookup->find(peerId);
if (!peerEntry.peerDefault()) {
if (peerEntry.hasCallPriority()) {
connection->hasCallPriority(peerEntry.hasCallPriority());
LogInfoEx(LOG_MASTER, "PEER %u >> Has Call Priority", peerId);
}
}
network->m_peers[peerId] = connection;
// attach extra notification data to the RPTC ACK to notify the peer of
// the use of the alternate diagnostic port
uint8_t buffer[1U];
buffer[0U] = 0x80U; // this should really be a defined constant -- but
// because this is the only option and its *always* sent now
// we can just hardcode this for now
json::object peerConfig = connection->config();
std::string identity = "* UNK *";
if (peerConfig["identity"].is<std::string>()) {
identity = peerConfig["identity"].getDefault<std::string>("* UNK *");
connection->identity(identity);
LogInfoEx(LOG_MASTER, "PEER %u >> Identity [%8s]", peerId, identity.c_str());
}
if (peerConfig["software"].is<std::string>()) {
std::string software = peerConfig["software"].get<std::string>();
LogInfoEx(LOG_MASTER, "PEER %u >> Software Version [%s]", peerId, software.c_str());
}
/*
** bryanb: this is support for older configuration structs, newer peers should
** send the peerClass value
*/
{
// is the peer reporting it is a SysView peer?
if (peerConfig["sysView"].is<bool>()) {
bool sysView = peerConfig["sysView"].get<bool>();
if (sysView)
connection->peerClass(PEER_CONN_CLASS_SYSVIEW);
if (sysView)
LogInfoEx(LOG_MASTER, "PEER %u >> SysView Peer", peerId);
}
// is the peer reporting it is a downstream FNE neighbor peer?
if (peerConfig["externalPeer"].is<bool>()) {
bool externalPeer = peerConfig["externalPeer"].get<bool>();
if (externalPeer)
connection->peerClass(PEER_CONN_CLASS_NEIGHBOR);
}
}
// determine the peer class
if (peerConfig["peerClass"].is<uint32_t>()) {
uint32_t peerClass = peerConfig["peerClass"].get<uint32_t>();
if (peerClass >= PEER_CONN_CLASS_INVALID)
peerClass = PEER_CONN_CLASS_STANDARD;
connection->peerClass((PEER_CONN_CLASS)peerClass);
} else {
if (connection->peerClass() == PEER_CONN_CLASS_UNKNOWN) {
connection->peerClass(PEER_CONN_CLASS_STANDARD);
}
}
// is the peer reporting it is a conventional peer?
if (peerConfig["conventionalPeer"].is<bool>()) {
if (network->m_allowConvSiteAffOverride) {
bool convPeer = peerConfig["conventionalPeer"].get<bool>();
connection->isConventional(convPeer);
} else {
bool conventionalPeer = false;
peerConfig["conventionalPeer"].set<bool>(conventionalPeer);
}
} else {
bool conventionalPeer = false;
peerConfig["conventionalPeer"].set<bool>(conventionalPeer);
}
// report peer class in log
switch (connection->peerClass()) {
case PEER_CONN_CLASS_NEIGHBOR:
LogInfoEx(LOG_MASTER, "PEER %u >> Downstream Neighbor FNE Peer", peerId);
break;
case PEER_CONN_CLASS_SYSVIEW:
LogInfoEx(LOG_MASTER, "PEER %u >> SysView Peer", peerId);
connection->isReplica(true);
break;
case PEER_CONN_CLASS_CONSOLE:
LogInfoEx(LOG_MASTER, "PEER %u >> Console Peer", peerId);
break;
case PEER_CONN_CLASS_STANDARD:
default:
if (connection->isConventional())
LogInfoEx(LOG_MASTER, "PEER %u >> Conventional Peer", peerId);
else
LogInfoEx(LOG_MASTER, "PEER %u >> Standard Peer", peerId);
break;
}
// for the purposes of maintaining backward compat with REST API users,
// we will report the peer class via the old externalPeer and sysView boolean
// fields as well (consolePeer doesn't have a legacy field, but that's ok since
// it's only used for console connections which should be new enough to support
// the new peerClass field)
bool externalPeer = connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR;
peerConfig["externalPeer"].set<bool>(externalPeer);
bool sysViewPeer = connection->peerClass() == PEER_CONN_CLASS_SYSVIEW;
peerConfig["sysView"].set<bool>(sysViewPeer);
bool consolePeer = connection->peerClass() == PEER_CONN_CLASS_CONSOLE;
peerConfig["consolePeer"].set<bool>(consolePeer);
connection->config(peerConfig);
// is the peer reporting it is an downstream FNE neighbor peer?
/*
** bryanb: don't change externalPeer to neighborPeer -- this will break backward
** compat with older FNE versions (we're stuck with this naming :()
*/
if (connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR) {
uint32_t masterPeerId = 0U;
if (peerConfig["masterPeerId"].is<uint32_t>()) {
masterPeerId = peerConfig["masterPeerId"].get<uint32_t>();
connection->masterId(masterPeerId);
LogInfoEx(LOG_MASTER, "PEER %u >> Master Peer ID [%u]", peerId, masterPeerId);
}
// master peer ID should never be zero for an neighbor peer -- use the peer ID instead
if (masterPeerId == 0U) {
LogWarning(LOG_MASTER, "PEER %u reports to be a downstream FNE neighbor peer but has not supplied a valid masterPeerId, using own peerId as masterPeerId (old FNE perhaps?)", peerId);
masterPeerId = peerId;
}
// check if the peer a peer replication participant
lookups::PeerId peerEntry = network->m_peerListLookup->find(req->peerId);
if (!peerEntry.peerDefault()) {
if (peerEntry.peerReplica()) {
connection->isReplica(true);
LogInfoEx(LOG_MASTER, "PEER %u >> Participates in Peer Replication", peerId);
}
}
if (network->m_enableSpanningTree) {
network->m_treeLock.lock();
// check if this peer is already connected via another peer
SpanningTree* tree = SpanningTree::findByMasterID(masterPeerId);
if (tree != nullptr) {
// are we allowing a fast reconnect? (this happens when a connecting peer
// uses the same peer ID and master ID already announced in the tree, but
// the tree entry wasn't yet erased)
if ((tree->id() == peerId && tree->masterId() == masterPeerId) &&
network->m_spanningTreeFastReconnect) {
LogWarning(LOG_STP, "PEER %u (%s) server already announced in server tree, fast peer reconnect, peerId = %u, masterId = %u, treePeerId = %u, treeMasterId = %u, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
peerId, masterPeerId, tree->id(), tree->masterId(), connection->connectionState());
if (identity != tree->identity()) {
LogWarning(LOG_STP, "PEER %u (%s) why has this server's announced identity changed? *big hmmmm*", peerId, connection->identWithQualifier().c_str());
}
SpanningTree::moveParent(tree, network->m_treeRoot);
network->logSpanningTree(connection);
} else {
LogWarning(LOG_STP, "PEER %u (%s) RPTC NAK, server already connected via PEER %u, duplicate connection denied, peerId = %u, masterId = %u, treePeerId = %u, treeMasterId = %u, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
peerId, masterPeerId, tree->id(), tree->masterId(), tree->id(), connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_CONFIG, NET_CONN_NAK_FNE_DUPLICATE_CONN, req->address, req->addrLen);
network->m_treeLock.unlock();
network->disconnectPeer(peerId, connection);
return;
}
} else {
SpanningTree* node = new SpanningTree(peerId, masterPeerId, network->m_treeRoot);
node->identity(identity);
network->logSpanningTree(connection);
}
network->m_treeLock.unlock();
}
}
network->writePeerACK(peerId, streamId, buffer, 1U);
LogInfoEx(LOG_MASTER, "PEER %u RPTC ACK, completed the configuration exchange", peerId);
// setup the affiliations list for this peer
std::stringstream peerName;
peerName << "PEER " << peerId;
network->createPeerAffiliations(peerId, peerName.str());
// spin up a thread and send metadata over to peer
network->peerMetadataUpdate(peerId);
}
}
}
else {
// perform source address/port validation
if (connection->address() != udp::Socket::address(req->address) ||
connection->port() != udp::Socket::port(req->address)) {
LogError(LOG_MASTER, "PEER %u (%s) RPTC NAK, IP address/port mismatch on RPTC attempt while in an incorrect state, old = %s:%u, new = %s:%u, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
connection->address().c_str(), connection->port(), udp::Socket::address(req->address).c_str(), udp::Socket::port(req->address), connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
return;
}
LogWarning(LOG_MASTER, "PEER %u (%s) RPTC NAK, login exchange while in an incorrect state, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_CONFIG, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
}
}
else {
network->writePeerNAK(peerId, TAG_REPEATER_CONFIG, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
LogWarning(LOG_MASTER, "PEER %u RPTC NAK, having no connection", peerId);
}
}

@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::RPT_DISC packets. */
void TrafficNetwork::PacketHandler::repeaterDisconnect(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
std::lock_guard<std::mutex> peerGuard(TrafficNetwork::getPeerStateLock(peerId));
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
LogInfoEx(LOG_MASTER, "PEER %u (%s) disconnected", peerId, connection->identWithQualifier().c_str());
network->disconnectPeer(peerId, connection);
}
}
}
}

@ -0,0 +1,115 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/TrafficNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::RPTL packets. */
void TrafficNetwork::PacketHandler::repeaterLogin(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now)
{
std::lock_guard<std::mutex> peerGuard(TrafficNetwork::getPeerStateLock(peerId));
if (peerId > 0 && (network->m_peers.find(peerId) == network->m_peers.end())) {
if (network->m_peers.size() >= MAX_HARD_CONN_CAP) {
LogError(LOG_MASTER, "PEER %u attempted to connect with no more connections available, currConnections = %u", peerId, network->m_peers.size());
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_FNE_MAX_CONN, req->address, req->addrLen);
return;
}
if (network->m_softConnLimit > 0U && network->m_peers.size() >= network->m_softConnLimit) {
LogError(LOG_MASTER, "PEER %u attempted to connect with no more connections available, maxConnections = %u, currConnections = %u", peerId, network->m_softConnLimit, network->m_peers.size());
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_FNE_MAX_CONN, req->address, req->addrLen);
return;
}
FNEPeerConnection* connection = new FNEPeerConnection(peerId, req->address, req->addrLen);
connection->lastPing(now);
network->applyJitterBufferConfig(peerId, connection);
network->setupRepeaterLogin(peerId, streamId, connection);
// check if the peer is in the peer ACL list
if (network->m_peerListLookup->getACL()) {
if (network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "Peer List ACL enabled, but we have an empty peer list? Passing all peers.");
}
if (!network->m_peerListLookup->isPeerAllowed(peerId) && !network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "PEER %u RPTL, failed peer ACL check", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_PEER_ACL, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
}
}
else {
// check if the peer is in our peer list -- if he is, and he isn't in a running state, reset
// the login sequence
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
if (connection->connectionState() == NET_STAT_RUNNING) {
LogInfoEx(LOG_MASTER, "PEER %u (%s) resetting peer connection, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
connection->connectionState());
network->disconnectPeer(peerId, connection);
connection = new FNEPeerConnection(peerId, req->address, req->addrLen);
connection->lastPing(now);
network->applyJitterBufferConfig(peerId, connection);
network->erasePeerAffiliations(peerId);
network->setupRepeaterLogin(peerId, streamId, connection);
// check if the peer is in the peer ACL list
if (network->m_peerListLookup->getACL()) {
if (network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "Peer List ACL enabled, but we have an empty peer list? Passing all peers.");
}
if (!network->m_peerListLookup->isPeerAllowed(peerId) && !network->m_peerListLookup->isPeerListEmpty()) {
LogWarning(LOG_MASTER, "PEER %u RPTL, failed peer ACL check", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_PEER_ACL, req->address, req->addrLen);
network->disconnectPeer(peerId, connection);
}
}
} else {
// perform source address validation
if (connection->address() != udp::Socket::address(req->address)) {
LogError(LOG_MASTER, "PEER %u RPTL NAK, IP address mismatch on RPTL attempt while not running, old = %s:%u, new = %s:%u, connectionState = %u", peerId,
connection->address().c_str(), connection->port(), udp::Socket::address(req->address).c_str(), udp::Socket::port(req->address), connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen);
return;
}
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
LogWarning(LOG_MASTER, "PEER %u (%s) RPTL NAK, bad connection state, connectionState = %u", peerId, connection->identWithQualifier().c_str(),
connection->connectionState());
network->disconnectPeer(peerId, connection);
}
} else {
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
network->erasePeer(peerId);
LogWarning(LOG_MASTER, "PEER %u RPTL NAK, having no connection", peerId);
}
}
}
}

@ -0,0 +1,211 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::REPL packets. */
void MetadataNetwork::PacketHandler::replication(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)ssrc;
if (req->fneHeader.getSubFunction() == NET_SUBFUNC::REPL_ACT_PEER_LIST) { // Peer Replication Active Peer List
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR &&
connection->isReplica()) {
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "MetadataNetwork::taskNetworkRx(), REPL_ACT_PEER_LIST, Raw Payload", rawPayload, req->length);
PacketBufferEntryPtr pkt = findOrCreatePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId, "Peer Replication, Active Peer List", streamId);
if (pkt == nullptr || !pkt->buffer) {
LogError(LOG_REPL, "PEER %u (%s) Peer Replication, Active Peer List, failed to initialize packet buffer", peerId,
connection->identWithQualifier().c_str());
erasePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId);
return;
}
std::unique_lock<std::mutex> pktLock(pkt->mutex, std::defer_lock);
uint32_t timeout = 0U;
while (!pktLock.try_lock() && timeout < TIMEOUT_MAX_REPL) {
timeout++;
Thread::sleep(1U);
}
if (!pktLock.owns_lock()) {
LogError(LOG_STP, "PEER %u (%s) Peer Replication, Active Peer List, timeout waiting for packet buffer to unlock", peerId,
connection->identWithQualifier().c_str());
if (pkt->buffer) {
pkt->buffer->clear();
pkt->buffer.reset();
}
pkt->streamId = 0U;
erasePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId);
return;
}
if (pkt->streamId != streamId) {
LogError(LOG_REPL, "PEER %u (%s) Peer Replication, Active Peer List, stream ID mismatch, expected %u, got %u", peerId,
connection->identWithQualifier().c_str(), pkt->streamId, streamId);
pkt->buffer->clear();
pkt->streamId = streamId;
}
uint32_t decompressedLen = 0U;
uint8_t* decompressed = nullptr;
if (pkt->buffer->decode(rawPayload, &decompressed, &decompressedLen)) {
std::string payload(decompressed + 8U, decompressed + decompressedLen);
// parse JSON body
json::value v;
std::string err = json::parse(v, payload);
if (!err.empty()) {
LogError(LOG_REPL, "PEER %u (%s) error parsing active peer list, %s", peerId, connection->identWithQualifier().c_str(), err.c_str());
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId);
return;
}
else {
// ensure parsed JSON is an array
if (!v.is<json::array>()) {
LogError(LOG_REPL, "PEER %u (%s) error parsing active peer list, data was not valid", peerId, connection->identWithQualifier().c_str());
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId);
return;
}
else {
json::array arr = v.get<json::array>();
LogInfoEx(LOG_REPL, "PEER %u (%s) Peer Replication, Active Peer List, updating %u peer entries", peerId, connection->identWithQualifier().c_str(), arr.size());
network->m_peerReplicaPeers[peerId] = arr;
}
}
pkt->buffer->clear();
pkt->buffer.reset();
pkt->streamId = 0U;
if (decompressed != nullptr) {
delete[] decompressed;
}
erasePacketBufferEntry(mdNetwork->m_peerReplicaActPkt, peerId);
}
}
else {
network->writePeerNAK(peerId, 0U, TAG_PEER_REPLICA, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
else if (req->fneHeader.getSubFunction() == NET_SUBFUNC::REPL_HA_PARAMS) { // Peer Replication HA Parameters
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR &&
connection->isReplica()) {
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
std::vector<HAParameters> receivedParams;
uint32_t len = GET_UINT32(rawPayload, 0U);
if (len > 0U) {
len /= HA_PARAMS_ENTRY_LEN;
}
uint8_t offs = 4U;
for (uint8_t i = 0U; i < len; i++, offs += HA_PARAMS_ENTRY_LEN) {
uint32_t peerId = GET_UINT32(rawPayload, offs);
uint32_t ipAddr = GET_UINT32(rawPayload, offs + 4U);
uint16_t port = GET_UINT16(rawPayload, offs + 8U);
receivedParams.push_back(HAParameters(peerId, ipAddr, port));
}
if (receivedParams.size() > 0U) {
for (auto rxEntry : receivedParams) {
auto it = std::find_if(network->m_peerReplicaHAParams.begin(), network->m_peerReplicaHAParams.end(),
[&](HAParameters& x)
{
if (x.peerId == rxEntry.peerId)
return true;
return false;
});
if (it != network->m_peerReplicaHAParams.end()) {
it->masterIP = rxEntry.masterIP;
it->masterPort = rxEntry.masterPort;
} else {
HAParameters param = rxEntry;
network->m_peerReplicaHAParams.push_back(param);
}
if (network->m_debug) {
std::string address = __IP_FROM_UINT(rxEntry.masterIP);
LogDebugEx(LOG_REPL, "MetadataNetwork::taskNetworkRx()", "PEER %u (%s) Peer Replication, HA Parameters, %s:%u", peerId, connection->identWithQualifier().c_str(),
address.c_str(), rxEntry.masterPort);
}
}
if (receivedParams.size() > 0) {
LogInfoEx(LOG_REPL, "PEER %u (%s) Peer Replication, HA Parameters, updating %u entries, %u entries", peerId, connection->identWithQualifier().c_str(), receivedParams.size(),
network->m_peerReplicaHAParams.size());
// send peer updates to replica peers
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
std::vector<HAParameters> haParams;
network->m_peerReplicaHAParams.lock(false);
for (auto entry : network->m_peerReplicaHAParams) {
haParams.push_back(entry);
}
network->m_peerReplicaHAParams.unlock();
peer.second->writeHAParams(haParams);
}
}
}
}
}
}
} else {
network->writePeerNAK(peerId, 0U, TAG_PEER_REPLICA, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
}

@ -0,0 +1,215 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "fne/Defines.h"
#include "common/Log.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Handles NET_FUNC::TRANSFER packets. */
void MetadataNetwork::PacketHandler::transfer(TrafficNetwork* network, MetadataNetwork* mdNetwork, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
(void)mdNetwork;
(void)ssrc;
(void)streamId;
// resolve peer ID (used for Activity Log and Status Transfer)
bool validPeerId = false;
uint32_t pktPeerId = 0U;
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
validPeerId = true;
pktPeerId = peerId;
} else {
if (peerId > 0) {
// this could be a replica transfer -- in which case, we need to check the SSRC of the packet not the peer ID
if (network->m_peers.find(req->rtpHeader.getSSRC()) != network->m_peers.end()) {
FNEPeerConnection* connection = network->m_peers[req->rtpHeader.getSSRC()];
if (connection != nullptr) {
if (connection->peerClass() == PEER_CONN_CLASS_NEIGHBOR && connection->isReplica()) {
validPeerId = true;
pktPeerId = req->rtpHeader.getSSRC();
}
}
}
}
}
// process incoming message subfunction opcodes
switch (req->fneHeader.getSubFunction()) {
case NET_SUBFUNC::TRANSFER_SUBFUNC_ACTIVITY: // Peer Activity Log Transfer
{
if (network->m_allowActivityTransfer) {
if (pktPeerId > 0 && validPeerId) {
FNEPeerConnection* connection = network->m_peers[pktPeerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
DECLARE_UINT8_ARRAY(rawPayload, req->length - 11U);
::memcpy(rawPayload, req->buffer + 11U, req->length - 11U);
std::string payload(rawPayload, rawPayload + (req->length - 11U));
::ActivityLog("%.9u (%8s) %s", pktPeerId, connection->identWithQualifier().c_str(), payload.c_str());
// report activity log to InfluxDB
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("activity")
.tag("peerId", std::to_string(pktPeerId))
.field("identity", connection->identity())
.field("msg", payload)
.timestamp(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count())
.requestAsync(network->m_influxServer);
}
// repeat traffic to the connected SysView peers
if (network->m_peers.size() > 0U) {
for (auto peer : network->m_peers) {
if (peer.second != nullptr) {
if (peer.second->peerClass() == PEER_CONN_CLASS_SYSVIEW) {
sockaddr_storage addr = peer.second->socketStorage();
uint32_t addrLen = peer.second->sockStorageLen();
network->m_frameQueue->write(req->buffer, req->length, network->createStreamId(), pktPeerId, network->m_peerId,
{ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_ACTIVITY }, RTP_END_OF_CALL_SEQ, addr, addrLen);
}
} else {
continue;
}
}
}
// attempt to repeat traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_ACTIVITY },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, true, pktPeerId);
}
}
}
}
}
else {
network->writePeerNAK(pktPeerId, network->createStreamId(), TAG_TRANSFER_ACT_LOG, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
}
break;
case NET_SUBFUNC::TRANSFER_SUBFUNC_DIAG: // Peer Diagnostic Log Transfer
{
if (network->m_allowDiagnosticTransfer) {
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
DECLARE_UINT8_ARRAY(rawPayload, req->length - 11U);
::memcpy(rawPayload, req->buffer + 11U, req->length - 11U);
std::string payload(rawPayload, rawPayload + (req->length - 11U));
bool currState = g_disableTimeDisplay;
g_disableTimeDisplay = true;
::Log(9999U, {nullptr, nullptr, 0U, nullptr}, "%.9u (%8s) %s", peerId, connection->identWithQualifier().c_str(), payload.c_str());
g_disableTimeDisplay = currState;
// report diagnostic log to InfluxDB
if (network->m_enableInfluxDB) {
influxdb::QueryBuilder()
.meas("diag")
.tag("peerId", std::to_string(peerId))
.field("identity", connection->identity())
.field("msg", payload)
.timestamp(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count())
.requestAsync(network->m_influxServer);
}
}
else {
network->writePeerNAK(peerId, network->createStreamId(), TAG_TRANSFER_DIAG_LOG, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
}
break;
case NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS: // Peer Status Transfer
{
if (pktPeerId > 0 && validPeerId) {
FNEPeerConnection* connection = network->m_peers[pktPeerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
if (network->m_peers.size() > 0U) {
// attempt to repeat status traffic to SysView clients
for (auto peer : network->m_peers) {
if (peer.second != nullptr) {
if (peer.second->peerClass() == PEER_CONN_CLASS_SYSVIEW) {
sockaddr_storage addr = peer.second->socketStorage();
uint32_t addrLen = peer.second->sockStorageLen();
if (network->m_debug) {
LogDebug(LOG_DIAG, "SysView, srcPeer = %u, dstPeer = %u, peer status message, len = %u",
pktPeerId, peer.first, req->length);
}
network->m_frameQueue->write(req->buffer, req->length, network->createStreamId(), pktPeerId, network->m_peerId,
{ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS }, RTP_END_OF_CALL_SEQ, addr, addrLen);
}
} else {
continue;
}
}
// attempt to repeat status traffic to replica masters
if (network->m_host->m_peerNetworks.size() > 0) {
for (auto peer : network->m_host->m_peerNetworks) {
if (peer.second != nullptr) {
if (peer.second->isEnabled() && peer.second->isReplica()) {
peer.second->writeMaster({ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS },
req->buffer, req->length, RTP_END_OF_CALL_SEQ, 0U, true, pktPeerId);
}
}
}
}
}
}
else {
network->writePeerNAK(pktPeerId, network->createStreamId(), TAG_TRANSFER_STATUS, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
break;
default:
{
LogWarning(LOG_MASTER, "PEER %u, unknown/unsupported transfer opcode %u", peerId, req->fneHeader.getSubFunction());
if (network->m_debug)
Utils::dump("Unknown/unsupported transfer opcode from the peer", req->buffer, req->length);
}
break;
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.