// 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;
using fnecore.P25.kmm;
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;
///
/// Exteral Peer
///
public bool ExternalPeer;
///
/// Conventional Peer
///
public bool ConventionalPeer;
///
/// 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
///
/// Event called when a kmm key response is received
///
public class KeyResponseEvent : EventArgs
{
///
/// KMM Message Id
///
public byte MessageId { get; set; }
///
/// instance
///
public KmmModifyKey KmmKey { get; set; }
///
/// Raw Data
///
public byte[] Data { get; set; }
/*
** Methods
*/
public KeyResponseEvent(byte messageId, KmmModifyKey kmmKey, byte[] data) : base()
{
this.MessageId = messageId;
this.KmmKey = kmmKey;
this.Data = data;
}
}
///
/// 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;
///
/// Event action thats called when a key response is received
///
public event EventHandler KeyResponse;
///
/// 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);
}
///
/// Helper to fire the key response event
///
///
protected void FireKeyResponse(KeyResponseEvent e)
{
if (KeyResponse != null)
KeyResponse.Invoke(this, e);
}
} // public abstract class FneBase
} // namespace fnecore