// SPDX-License-Identifier: AGPL-3.0-only /** * Digital Voice Modem - Fixed Network Equipment Core Library * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * @package DVM / Fixed Network Equipment Core Library * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2022-2023 Bryan Biedenkapp, N2PLL * */ using System; using System.Net; using System.Security.Cryptography; using fnecore.DMR; using fnecore.P25; using fnecore.NXDN; using fnecore.EDAC; namespace fnecore { /// /// Structure containing detailed information about a connected peer. /// public class PeerDetails { /// /// Identity /// public string Identity; /// /// Receive Frequency /// public uint RxFrequency; /// /// Transmit Frequency /// public uint TxFrequency; /// /// Software Identifier /// public string Software; /* ** System Information */ /// /// Latitude /// public double Latitude; /// /// Longitude /// public double Longitude; /// /// Height /// public int Height; /// /// Location /// public string Location; /* ** Channel Data */ /// /// Transmit Offset (Mhz) /// public float TxOffsetMhz; /// /// Channel Bandwidth (Khz) /// public float ChBandwidthKhz; /// /// Channel ID /// public byte ChannelID; /// /// Channel Number /// public uint ChannelNo; /// /// Transmit Power /// public uint TxPower; /* ** REST API */ /// /// REST API Password /// public string Password; /// /// REST API Port /// public int Port; /* ** Methods */ /// /// Initializes a new instance of the class. /// public PeerDetails() { /* stub */ } } // public class PeerDetails /// /// Structure containing information about a connected peer. /// public class PeerInformation { /// /// Peer ID /// public uint PeerID; /// /// Stream ID /// public uint StreamID; /// /// RTP Packet Sequence /// public ushort PacketSequence; /// /// Next expected RTP Packet Sequence /// public ushort NextPacketSequence; /// /// Peer IP EndPoint /// public IPEndPoint EndPoint; /// /// Salt value used for authentication. /// public uint Salt; /// /// Connection State /// public ConnectionState State; /// /// Flag indicating peer is "connected". /// public bool Connection; /// /// Number of pings received. /// public int PingsReceived; /// /// Date/Time of last ping. /// public DateTime LastPing; /// /// Peer Details Structure /// public PeerDetails Details = null; /* ** Methods */ /// /// Initializes a new instance of the class. /// public PeerInformation() { Details = new PeerDetails(); } } // public class PeerInformation /// /// Callback used to validate incoming DMR data. /// /// Peer ID /// Source Address /// Destination Address /// Slot Number /// Call Type (Group or Private) /// Frame Type /// DMR Data Type /// Stream ID /// Raw message data /// True, if data stream is valid, otherwise false. public delegate bool DMRDataValidate(uint peerId, uint srcId, uint dstId, byte slot, CallType callType, FrameType frameType, DMRDataType dataType, uint streamId, byte[] message); /// /// Event used to process incoming DMR data. /// public class DMRDataReceivedEvent : EventArgs { /// /// Peer ID /// public uint PeerId { get; } /// /// Source Address /// public uint SrcId { get; } /// /// Destination Address /// public uint DstId { get; } /// /// Slot Number /// public byte Slot { get; } /// /// Call Type (Group or Private) /// public CallType CallType { get; } /// /// Frame Type /// public FrameType FrameType { get; } /// /// DMR Data Type /// public DMRDataType DataType { get; } /// /// /// public byte n { get; } /// /// RTP Packet Sequence /// public ushort PacketSequence { get; } /// /// Stream ID /// public uint StreamId { get; } /// /// Raw message data /// public byte[] Data { get; } /* ** Methods */ /// /// Initializes a new instance of the class. /// private DMRDataReceivedEvent() { /* stub */ } /// /// Initializes a new instance of the class. /// /// Peer ID /// Source Address /// Destination Address /// Slot Number /// Call Type (Group or Private) /// Frame Type /// DMR Data Type /// /// RTP Packet Sequence /// Stream ID /// Raw message data public DMRDataReceivedEvent(uint peerId, uint srcId, uint dstId, byte slot, CallType callType, FrameType frameType, DMRDataType dataType, byte n, ushort pktSeq, uint streamId, byte[] data) : base() { this.PeerId = peerId; this.SrcId = srcId; this.DstId = dstId; this.Slot = slot; this.CallType = callType; this.FrameType = frameType; this.DataType = dataType; this.n = n; this.PacketSequence = pktSeq; this.StreamId = streamId; this.Data = new byte[data.Length]; Buffer.BlockCopy(data, 0, Data, 0, data.Length); } } // public class DMRDataReceivedEvent : EventArgs /// /// Callback used to validate incoming P25 data. /// /// Peer ID /// Source Address /// Destination Address /// Call Type (Group or Private) /// P25 DUID /// Frame Type /// Stream ID /// Raw message data /// True, if data stream is valid, otherwise false. public delegate bool P25DataValidate(uint peerId, uint srcId, uint dstId, CallType callType, P25DUID duid, FrameType frameType, uint streamId, byte[] message); /// /// Event used to process incoming P25 data. /// public class P25DataReceivedEvent : EventArgs { /// /// Peer ID /// public uint PeerId { get; } /// /// Source Address /// public uint SrcId { get; } /// /// Destination Address /// public uint DstId { get; } /// /// Call Type (Group or Private) /// public CallType CallType { get; } /// /// P25 DUID /// public P25DUID DUID { get; } /// /// Frame Type /// public FrameType FrameType { get; } /// /// RTP Packet Sequence /// public ushort PacketSequence { get; } /// /// Stream ID /// public uint StreamId { get; } /// /// Raw message data /// public byte[] Data { get; } /* ** Methods */ /// /// Initializes a new instance of the class. /// private P25DataReceivedEvent() { /* stub */ } /// /// Initializes a new instance of the class. /// /// Peer ID /// Source Address /// Destination Address /// Call Type (Group or Private) /// P25 DUID /// Frame Type /// RTP Packet Sequence /// Stream ID /// Raw message data public P25DataReceivedEvent(uint peerId, uint srcId, uint dstId, CallType callType, P25DUID duid, FrameType frameType, ushort pktSeq, uint streamId, byte[] data) : base() { this.PeerId = peerId; this.SrcId = srcId; this.DstId = dstId; this.CallType = callType; this.DUID = duid; this.FrameType = frameType; this.PacketSequence = pktSeq; this.StreamId = streamId; this.Data = new byte[data.Length]; Buffer.BlockCopy(data, 0, Data, 0, data.Length); } } // public class P25DataReceivedEvent : EventArgs /// /// Callback used to validate incoming NXDN data. /// /// Peer ID /// Source Address /// Destination Address /// Call Type (Group or Private) /// NXDN Message Type /// Frame Type /// Stream ID /// Raw message data /// True, if data stream is valid, otherwise false. public delegate bool NXDNDataValidate(uint peerId, uint srcId, uint dstId, CallType callType, NXDNMessageType messageType, FrameType frameType, uint streamId, byte[] message); /// /// Event used to process incoming NXDN data. /// public class NXDNDataReceivedEvent : EventArgs { /// /// Peer ID /// public uint PeerId { get; } /// /// Source Address /// public uint SrcId { get; } /// /// Destination Address /// public uint DstId { get; } /// /// Call Type (Group or Private) /// public CallType CallType { get; } /// /// NXDN Message Type /// public NXDNMessageType MessageType { get; } /// /// Frame Type /// public FrameType FrameType { get; } /// /// RTP Packet Sequence /// public ushort PacketSequence { get; } /// /// Stream ID /// public uint StreamId { get; } /// /// Raw message data /// public byte[] Data { get; } /* ** Methods */ /// /// Initializes a new instance of the class. /// private NXDNDataReceivedEvent() { /* stub */ } /// /// Initializes a new instance of the class. /// /// Peer ID /// Source Address /// Destination Address /// Call Type (Group or Private) /// NXDN Message Type /// Frame Type /// RTP Packet Sequence /// Stream ID /// Raw message data public NXDNDataReceivedEvent(uint peerId, uint srcId, uint dstId, CallType callType, NXDNMessageType messageType, FrameType frameType, ushort pktSeq, uint streamId, byte[] data) : base() { this.PeerId = peerId; this.SrcId = srcId; this.DstId = dstId; this.CallType = callType; this.MessageType = messageType; this.FrameType = frameType; this.PacketSequence = pktSeq; this.StreamId = streamId; this.Data = new byte[data.Length]; Buffer.BlockCopy(data, 0, Data, 0, data.Length); } } // public class NXDNDataReceivedEvent : EventArgs /// /// Callback used to process whether or not a peer is being ignored for traffic. /// /// Peer ID /// Source Address /// Destination Address /// Slot Number /// Call Type (Group or Private) /// Frame Type /// DMR Data Type /// Stream ID /// True, if peer is ignored, otherwise false. public delegate bool PeerIgnored(uint peerId, uint srcId, uint dstId, byte slot, CallType callType, FrameType frameType, DMRDataType dataType, uint streamId); /// /// Event when a peer connects. /// public class PeerConnectedEvent : EventArgs { /// /// Peer ID /// public uint PeerId { get; } /// /// Peer Information /// public PeerInformation Information { get; } /* ** Methods */ /// /// Initializes a new instance of the class. /// /// Peer ID /// Peer Information public PeerConnectedEvent(uint peerId, PeerInformation peer) : base() { this.PeerId = peerId; this.Information = peer; } } // public class PeerConnectedEvent : EventArgs /// /// This class implements some base functionality for all other FNE network classes. /// public abstract class FneBase { protected readonly string systemName = string.Empty; protected readonly uint peerId = 0; protected static Random rand = null; protected bool isStarted = false; /* ** Properties */ /// /// Gets the system name for this . /// public string SystemName => systemName; /// /// Gets the peer ID for this . /// public uint PeerId => peerId; /// /// Flag indicating whether this is running. /// public bool IsStarted => isStarted; /// /// Gets/sets the interval that peers will need to ping the master. /// public int PingTime { get; set; } /// /// Get/sets the current logging level of the instance. /// public LogLevel LogLevel { get; set; } /// /// Get/sets a flag that enables dumping the raw recieved packets to the log. /// /// This will also require the be set to DEBUG. public bool RawPacketTrace { get; set; } /* ** Events/Callbacks */ /// /// Callback action that handles validating a DMR call stream. /// public DMRDataValidate DMRDataValidate = null; /// /// Event action that handles processing a DMR call stream. /// public event EventHandler DMRDataReceived; /// /// Callback action that handles validating a P25 call stream. /// public P25DataValidate P25DataValidate = null; /// /// Event action that handles preprocessing a P25 call stream. /// public event EventHandler P25DataPreprocess; /// /// Event action that handles processing a P25 call stream. /// public event EventHandler P25DataReceived; /// /// Callback action that handles validating a NXDN call stream. /// public NXDNDataValidate NXDNDataValidate = null; /// /// Event action that handles processing a NXDN call stream. /// public event EventHandler NXDNDataReceived; /// /// Callback action that handles verifying if a peer is ignored for a call stream. /// public PeerIgnored PeerIgnored = null; /// /// Event action that handles when a peer connects. /// public event EventHandler PeerConnected; /// /// Callback action that handles when a peer disconnects. /// public Action PeerDisconnected = null; /// /// Callback action that handles internal logging. /// public Action Logger; /* ** Methods */ /// /// Static initializer for the class. /// static FneBase() { int seed = 0; using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) { byte[] intBytes = new byte[4]; rng.GetBytes(intBytes); seed = BitConverter.ToInt32(intBytes, 0); } rand = new Random(seed); rand.Next(); } /// /// Initializes a new instance of the class. /// /// /// protected FneBase(string systemName, uint peerId) { this.systemName = systemName; this.peerId = peerId; // set a default "noop" logger Logger = (LogLevel level, string message) => { }; } /// /// Starts the main execution loop for this . /// public abstract void Start(); /// /// Stops the main execution loop for this . /// public abstract void Stop(); /// /// Helper to generate a new stream ID. /// /// public static uint CreateStreamID() { return (uint)rand.Next(int.MinValue, int.MaxValue); } /// /// Helper to just quickly generate opcode tuples (mainly for brevity). /// /// Function /// Sub-Function /// public static Tuple CreateOpcode(byte func, byte subFunc = Constants.NET_SUBFUNC_NOP) { return new Tuple(func, subFunc); } /// /// Helper to fire the logging action. /// /// /// protected void Log(LogLevel logLevel, string message) { byte level = (byte)logLevel; if (level <= (byte)LogLevel) Logger(logLevel, message); } /// /// Helper to read and process a FNE RTP frame. /// /// Raw UDP socket frame. /// Length of payload message. /// RTP Header. /// RTP FNE Header. protected byte[] ReadFrame(UdpFrame frame, out int messageLength, out RtpHeader rtpHeader, out RtpFNEHeader fneHeader) { int length = frame.Message.Length; messageLength = -1; rtpHeader = null; fneHeader = null; // read message from socket if (length > 0) { if (length < Constants.RtpHeaderLengthBytes + Constants.RtpExtensionHeaderLengthBytes) { Log(LogLevel.ERROR, $"Message received from network is malformed! " + $"{Constants.RtpHeaderLengthBytes + Constants.RtpExtensionHeaderLengthBytes} bytes != {frame.Message.Length} bytes"); return null; } // decode RTP header rtpHeader = new RtpHeader(); if (!rtpHeader.Decode(frame.Message)) { Log(LogLevel.ERROR, $"Invalid RTP packet received from network"); return null; } // ensure the RTP header has extension header (otherwise abort) if (!rtpHeader.Extension) { Log(LogLevel.ERROR, "Invalid RTP header received from network"); return null; } // ensure payload type is correct if ((rtpHeader.PayloadType != Constants.DVMRtpPayloadType) && (rtpHeader.PayloadType != Constants.DVMRtpPayloadType + 1)) { Log(LogLevel.ERROR, "Invalid RTP payload type received from network"); return null; } // decode FNE RTP header fneHeader = new RtpFNEHeader(); if (!fneHeader.Decode(frame.Message)) { Log(LogLevel.ERROR, "Invalid RTP packet received from network"); return null; } // copy message messageLength = (int)fneHeader.MessageLength; byte[] message = new byte[messageLength]; Buffer.BlockCopy(frame.Message, (int)(Constants.RtpHeaderLengthBytes + Constants.RtpExtensionHeaderLengthBytes + Constants.RtpFNEHeaderLengthBytes), message, 0, messageLength); ushort calc = CRC.CreateCRC16(message, (uint)(messageLength * 8)); if (calc != fneHeader.CRC) { Log(LogLevel.ERROR, "Failed CRC CCITT-162 check"); messageLength = -1; return null; } return message; } return null; } /// /// Helper to generate and write a FNE RTP frame. /// /// Payload message. /// Peer ID. /// Synchronization Source ID. /// FNE Network Opcode. /// RTP Packet Sequence. /// Stream ID. /// protected byte[] WriteFrame(byte[] message, uint peerId, uint ssrc, Tuple opcode, ushort pktSeq, uint streamId) { byte[] buffer = new byte[message.Length + Constants.RtpHeaderLengthBytes + Constants.RtpExtensionHeaderLengthBytes + Constants.RtpFNEHeaderLengthBytes]; FneUtils.Memset(buffer, 0, buffer.Length); RtpHeader header = new RtpHeader(); header.Extension = true; header.PayloadType = Constants.DVMRtpPayloadType; header.Sequence = pktSeq; header.SSRC = ssrc; header.Encode(ref buffer); RtpFNEHeader fneHeader = new RtpFNEHeader(); fneHeader.CRC = CRC.CreateCRC16(message, (uint)(message.Length * 8)); fneHeader.StreamID = streamId; fneHeader.PeerID = peerId; fneHeader.MessageLength = (uint)message.Length; fneHeader.Function = opcode.Item1; fneHeader.SubFunction = opcode.Item2; fneHeader.Encode(ref buffer); Buffer.BlockCopy(message, 0, buffer, (int)(Constants.RtpHeaderLengthBytes + Constants.RtpExtensionHeaderLengthBytes + Constants.RtpFNEHeaderLengthBytes), message.Length); return buffer; } /// /// Helper to fire the DMR data received event. /// /// instance protected void FireDMRDataReceived(DMRDataReceivedEvent e) { if (DMRDataReceived != null) DMRDataReceived.Invoke(this, e); } /// /// Helper to fire the P25 data pre-process event. /// /// instance protected void FireP25DataPreprocess(P25DataReceivedEvent e) { if (P25DataPreprocess != null) P25DataPreprocess.Invoke(this, e); } /// /// Helper to fire the P25 data received event. /// /// instance protected void FireP25DataReceived(P25DataReceivedEvent e) { if (P25DataReceived != null) P25DataReceived.Invoke(this, e); } /// /// Helper to fire the NXDN data received event. /// /// instance protected void FireNXDNDataReceived(NXDNDataReceivedEvent e) { if (NXDNDataReceived != null) NXDNDataReceived.Invoke(this, e); } /// /// Helper to fire the peer connected event. /// /// instance protected void FirePeerConnected(PeerConnectedEvent e) { if (PeerConnected != null) PeerConnected.Invoke(this, e); } } // public abstract class FneBase } // namespace fnecore