From d357736f241e653d39d07ddf8c92d28826be518f Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sun, 8 Nov 2026 21:24:28 -0500 Subject: [PATCH] add initial support to drop inbound network U2U calls at the host; --- configs/config.example.yml | 3 +++ src/common/network/Network.cpp | 3 ++- src/common/network/Network.h | 8 +++++- src/host/Host.Config.cpp | 4 ++- src/host/dmr/Slot.cpp | 4 +++ src/host/dmr/packet/Data.cpp | 6 +++++ src/host/dmr/packet/Voice.cpp | 14 ++++++++++ src/host/nxdn/packet/ControlSignaling.cpp | 17 ++++++++++++ src/host/nxdn/packet/Data.cpp | 6 +++++ src/host/nxdn/packet/Voice.cpp | 6 +++++ src/host/p25/Control.cpp | 33 +++++++++++++++++++++++ src/host/p25/Control.h | 1 + src/host/p25/packet/ControlSignaling.cpp | 12 +++++++++ src/host/p25/phase2/Slot.cpp | 11 ++++++++ 14 files changed, 125 insertions(+), 3 deletions(-) diff --git a/configs/config.example.yml b/configs/config.example.yml index 5b365fe5..7412817b 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -106,6 +106,9 @@ network: # Maximum allowable DMR network jitter. jitter: 360 + # Flag indicating whether inbound unit-to-unit calls from the network will be discarded. + disallowUnitToUnit: false + # Flag indicating whether DMR slot 1 traffic will be passed. slot1: true # Flag indicating whether DMR slot 2 traffic will be passed. diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index 168aa098..ad5b15ed 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -30,9 +30,10 @@ using namespace network; Network::Network(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, - bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup, bool disallowUnitToUnit) : BaseNetwork(peerId, duplex, debug, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, localPort), m_pktLastSeq(0U), + m_disallowUnitToUnit(disallowUnitToUnit), m_address(address), m_port(port), m_configuredAddress(address), diff --git a/src/common/network/Network.h b/src/common/network/Network.h index 5a9eaa7b..e804d7e1 100644 --- a/src/common/network/Network.h +++ b/src/common/network/Network.h @@ -163,10 +163,11 @@ namespace network * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. * @param allowDiagnosticTransfer Flag indicating that the system diagnostic logs will be sent to the network. * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. + * @param disallowUnitToUnit Flag indicating that inbound unit-to-unit traffic will be discarded. */ Network(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, - bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup, bool disallowUnitToUnit = false); /** * @brief Finalizes a instance of the Network class. */ @@ -332,6 +333,11 @@ namespace network */ DECLARE_RO_PROPERTY_PLAIN(uint16_t, pktLastSeq); + /** + * @brief Flag indicating that inbound unit-to-unit traffic is discarded. + */ + DECLARE_RO_PROPERTY_PLAIN(bool, disallowUnitToUnit); + protected: std::string m_address; uint16_t m_port; diff --git a/src/host/Host.Config.cpp b/src/host/Host.Config.cpp index ccc32994..c63014b3 100644 --- a/src/host/Host.Config.cpp +++ b/src/host/Host.Config.cpp @@ -870,6 +870,7 @@ bool Host::createNetwork() bool allowStatusTransfer = networkConf["allowStatusTransfer"].as(true); bool updateLookup = networkConf["updateLookups"].as(false); bool saveLookup = networkConf["saveLookups"].as(false); + bool disallowUnitToUnit = networkConf["disallowUnitToUnit"].as(false); bool packetDump = networkConf["packetDump"].as(false); bool debug = networkConf["debug"].as(false); @@ -995,6 +996,7 @@ bool Host::createNetwork() LogInfo(" Allow Status Transfer: %s", m_allowStatusTransfer ? "yes" : "no"); LogInfo(" Update Lookups: %s", updateLookup ? "yes" : "no"); LogInfo(" Save Network Lookups: %s", saveLookup ? "yes" : "no"); + LogInfo(" Disallow Network Unit-to-Unit: %s", disallowUnitToUnit ? "yes" : "no"); LogInfo(" Encrypted: %s", encrypted ? "yes" : "no"); LogInfo(" KMF Encrypted: %s", kmfEncrypted ? "yes" : "no"); @@ -1024,7 +1026,7 @@ bool Host::createNetwork() // initialize networking if (netEnable) { m_network = new Network(address, port, local, id, password, m_duplex, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, false, slot1, slot2, - allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup); + allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup, disallowUnitToUnit); m_network->setPacketDump(packetDump); m_network->setLookups(m_ridLookup, m_tidLookup); diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 7af6f777..5a680401 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -459,6 +459,10 @@ void Slot::processNetwork(const data::NetData& dmrData) bool unitToUnit = (dmrData.getControl() & network::NET_CTRL_U2U) == network::NET_CTRL_U2U; if (grantDemand) { + if (s_network->disallowUnitToUnit() && unitToUnit) { + break; + } + // validate source RID if (!acl::AccessControl::validateSrcId(dmrData.getSrcId())) { break; diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index afc68b47..1f1cbaa5 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -441,6 +441,12 @@ void Data::processNetwork(const data::NetData& dmrData) uint32_t srcId = m_netDataHeader.getSrcId(); uint32_t dstId = m_netDataHeader.getDstId(); + if (m_slot->s_network->disallowUnitToUnit() && !gi) { + LogWarning(LOG_NET, "DMR Slot %u, rejecting network individual data call, srcId = %u, dstId = %u", + m_slot->m_slotNo, srcId, dstId); + return; + } + if (!m_slot->s_authoritative && m_slot->m_permittedDstId != dstId) { return; } diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index 39efb928..e218ef94 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -781,6 +781,12 @@ void Voice::processNetwork(const data::NetData& dmrData) uint32_t dstId = lc->getDstId(); FLCO::E flco = lc->getFLCO(); + if (m_slot->s_network->disallowUnitToUnit() && flco == FLCO::PRIVATE) { + LogWarning(LOG_NET, "DMR Slot %u, rejecting network unit-to-unit voice call, srcId = %u, dstId = %u", + m_slot->m_slotNo, srcId, dstId); + return; + } + CHECK_NET_AUTHORITATIVE(dstId); if (checkNetTrafficCollision(dstId)) @@ -871,6 +877,10 @@ void Voice::processNetwork(const data::NetData& dmrData) if (m_slot->m_netState != RS_NET_AUDIO) { std::unique_ptr lc = std::make_unique(dmrData.getFLCO(), dmrData.getSrcId(), dmrData.getDstId()); + if (m_slot->s_network->disallowUnitToUnit() && lc->getFLCO() == FLCO::PRIVATE) { + return; + } + uint32_t srcId = lc->getSrcId(); uint32_t dstId = lc->getDstId(); @@ -978,6 +988,10 @@ void Voice::processNetwork(const data::NetData& dmrData) if (m_slot->m_netState == RS_NET_IDLE) { std::unique_ptr lc = std::make_unique(dmrData.getFLCO(), dmrData.getSrcId(), dmrData.getDstId()); + if (m_slot->s_network->disallowUnitToUnit() && lc->getFLCO() == FLCO::PRIVATE) { + return; + } + uint32_t dstId = lc->getDstId(); uint32_t srcId = lc->getSrcId(); diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index a0c713f9..abb961d6 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -289,6 +289,23 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option uint16_t srcId = rcch->getSrcId(); uint16_t dstId = rcch->getDstId(); + if (m_nxdn->m_network->disallowUnitToUnit()) { + switch (rcch->getMessageType()) { + case MessageType::RTCH_VCALL: + if (!rcch->getGroup()) { + LogWarning(LOG_NET, "NXDN, rejecting network unit-to-unit call request, srcId = %u, dstId = %u", + srcId, dstId); + return false; + } + break; + case MessageType::RCCH_VCALL_REC: + case MessageType::RCCH_VCALL_CONN: + return false; + default: + break; + } + } + // handle standard NXDN message opcodes switch (rcch->getMessageType()) { case MessageType::RTCH_VCALL: diff --git a/src/host/nxdn/packet/Data.cpp b/src/host/nxdn/packet/Data.cpp index ba4b31b2..dfb2e50e 100644 --- a/src/host/nxdn/packet/Data.cpp +++ b/src/host/nxdn/packet/Data.cpp @@ -240,6 +240,12 @@ bool Data::processNetwork(ChOption::E option, lc::RTCH& netLC, uint8_t* data, ui uint16_t srcId = lc.getSrcId(); bool group = lc.getGroup(); + if (m_nxdn->m_network->disallowUnitToUnit() && !group) { + LogWarning(LOG_NET, "NXDN, rejecting network individual data call, srcId = %u, dstId = %u", + srcId, dstId); + return false; + } + // overwrite the destination ID if the network message header and // decoded network LC data don't agree (this can happen if the network is dynamically // altering the destination ID in-flight) diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index ec0a72fc..c3a39397 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -694,6 +694,12 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& bool group = lc.getGroup(); bool encrypted = lc.getEncrypted(); + if (m_nxdn->m_network->disallowUnitToUnit() && !group) { + LogWarning(LOG_NET, "NXDN, rejecting network unit-to-unit voice call, srcId = %u, dstId = %u", + srcId, dstId); + return false; + } + // overwrite the destination ID if the network message header and // decoded network LC data don't agree (this can happen if the network is dynamically // altering the destination ID in-flight) diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 6cb74764..0b26eefc 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -107,6 +107,7 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_netState(RS_NET_IDLE), m_netLastDstId(0U), m_netLastSrcId(0U), + m_netRejectedU2UStream(false), m_permittedDstId(0U), m_permittedSrcId(0U), m_permittedU2U(false), @@ -1634,6 +1635,38 @@ void Control::processNetwork() control.setNetId(netId); control.setSysId(sysId); + if (m_network->disallowUnitToUnit()) { + const bool privateCall = lco == LCO::PRIVATE || lco == LCO::PRIVATE_EXT; + const bool voiceFrame = duid == DUID::HDU || duid == DUID::LDU1 || duid == DUID::LDU2; + const bool callEnd = duid == DUID::TDU || duid == DUID::TDULC; + + // continuation frames such as LDU2 do not carry a voice-user LCO, so + // retain rejection state until call end or a fresh group call header + if (m_netRejectedU2UStream) { + const bool newGroupCall = (duid == DUID::HDU || duid == DUID::LDU1) && !privateCall && !unitToUnit; + if (newGroupCall) { + m_netRejectedU2UStream = false; + } + else if (voiceFrame || callEnd) { + if (callEnd) + m_netRejectedU2UStream = false; + return; + } + } + + // reject network unit-to-unit calls if either a private call or unit-to-unit call is detected + if (privateCall || unitToUnit) { + if (voiceFrame) { + m_netRejectedU2UStream = true; + m_network->resetP25(); + } + + LogWarning(LOG_NET, "P25, rejecting network unit-to-unit call stream, lco = $%02X, srcId = %u, dstId = %u", + lco, srcId, dstId); + return; + } + } + lsd.setLSD1(lsd1); lsd.setLSD2(lsd2); diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index 1d73e08b..4dde5bf9 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -365,6 +365,7 @@ namespace p25 RPT_NET_STATE m_netState; uint32_t m_netLastDstId; uint32_t m_netLastSrcId; + bool m_netRejectedU2UStream; uint32_t m_permittedDstId; uint32_t m_permittedSrcId; diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index b10ef066..1527b868 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -872,6 +872,18 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr return false; } + if (m_p25->m_network->disallowUnitToUnit()) { + switch (tsbk->getLCO()) { + case TSBKO::IOSP_UU_VCH: + case TSBKO::IOSP_UU_ANS: + case TSBKO::OSP_UU_VCH_GRANT_UPD: + LogWarning(LOG_NET, P25_TSDU_STR ", rejecting network unit-to-unit signaling, lco = $%02X", tsbk->getLCO()); + return false; + default: + break; + } + } + // handle updating internal adjacent site information if (tsbk->getLCO() == TSBKO::OSP_ADJ_STS_BCAST) { if (!m_p25->m_enableControl) { diff --git a/src/host/p25/phase2/Slot.cpp b/src/host/p25/phase2/Slot.cpp index 4f1cdf66..73d5b078 100644 --- a/src/host/p25/phase2/Slot.cpp +++ b/src/host/p25/phase2/Slot.cpp @@ -444,6 +444,10 @@ bool Slot::processNetwork(uint8_t* data, uint32_t length, const lc::LC& control, { CHECK_NET_AUTHORITATIVE(control.getDstId()); + if (s_network->disallowUnitToUnit() && isVoiceDUID(duid) && (controlByte & network::NET_CTRL_U2U) == network::NET_CTRL_U2U) { + return false; + } + if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { ++m_netMissed; if (m_debug) @@ -1152,6 +1156,13 @@ bool Slot::processMAC(uint8_t* data, uint32_t length, defines::P2_DUID::E duid, if (net && !m_control.decodeVCH_MACPDU_OEMI(frame, sync)) return false; + if (net && s_network->disallowUnitToUnit() && + m_control.getLCO() == defines::P2_MAC_MCO::PRIVATE) { + LogWarning(LOG_NET, "P25 Phase 2 Slot %u, rejecting network unit-to-unit MAC PDU, srcId = %u, dstId = %u", + m_slotNo + 1U, m_control.getSrcId(), m_control.getDstId()); + return false; + } + // The transport discriminator and the discriminator protected inside the // MAC burst must agree. Do not let metadata reinterpret one coded form // as another after the modem has classified the logical channel.