From 76fca7abcb3ce12331a6746b5d45ca9218990b1f Mon Sep 17 00:00:00 2001 From: Haves Irfan <20160532+ha-ves@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:21:21 +0900 Subject: [PATCH 1/2] extract transport classes --- src/net/ICE/RtpIceChannel.cs | 271 +---------------------- src/net/ICE/Transport/IceTcpReceiver.cs | 281 ++++++++++++++++++++++++ src/net/RTP/RTPChannel.cs | 241 -------------------- src/net/RTP/Transport/UdpReceiver.cs | 251 +++++++++++++++++++++ 4 files changed, 533 insertions(+), 511 deletions(-) create mode 100644 src/net/ICE/Transport/IceTcpReceiver.cs create mode 100644 src/net/RTP/Transport/UdpReceiver.cs diff --git a/src/net/ICE/RtpIceChannel.cs b/src/net/ICE/RtpIceChannel.cs index 6baad2535..d682031ec 100755 --- a/src/net/ICE/RtpIceChannel.cs +++ b/src/net/ICE/RtpIceChannel.cs @@ -112,7 +112,7 @@ namespace SIPSorcery.Net /// action needs to be taken to update the status of the ICE server or checklist entry /// check. /// - public class RtpIceChannel : RTPChannel + partial class RtpIceChannel : RTPChannel { private const int ICE_UFRAG_LENGTH = 4; private const int ICE_PASSWORD_LENGTH = 24; @@ -126,275 +126,6 @@ public class RtpIceChannel : RTPChannel public static List DefaultNameServers { get; set; } - public class IceTcpReceiver : UdpReceiver - { - protected const int REVEIVE_TCP_BUFFER_SIZE = RECEIVE_BUFFER_SIZE * 2; - - protected int m_recvOffset; - public IceTcpReceiver(Socket socket, int mtu = REVEIVE_TCP_BUFFER_SIZE) : base(socket, mtu) - { - m_recvOffset = 0; - } - - /// - /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to - /// return any data received. - /// - public override void BeginReceiveFrom() - { - //Prevent call BeginReceiveFrom if it is already running or into invalid state - if ((m_isClosed || !m_socket.Connected) && m_isRunningReceive) - { - m_isRunningReceive = false; - } - if (m_isRunningReceive || m_isClosed || !m_socket.Connected) - { - return; - } - - try - { - m_isRunningReceive = true; - EndPoint recvEndPoint = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - var recvLength = m_recvBuffer.Length - m_recvOffset; - //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values - if (recvLength <= 0 || m_recvOffset < 0) - { - m_recvOffset = 0; - recvLength = m_recvBuffer.Length; - } - m_socket.BeginReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref recvEndPoint, EndReceiveFrom, null); - } - // Thrown when socket is closed. Can be safely ignored. - // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. - // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the - // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. - catch (ObjectDisposedException) - { - m_isRunningReceive = false; - } - catch (SocketException sockExcp) - { - m_isRunningReceive = false; - logger.LogWarning(sockExcp, "Socket error {SocketErrorCode} in IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - //Close(sockExcp.Message); - } - catch (Exception excp) - { - m_isRunningReceive = false; - // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 - // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that - // case the socket can be considered to be unusable and there's no point trying another receive. - logger.LogError(excp, "Exception IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - } - - /// - /// Handler for end of the begin receive call. - /// - /// Contains the results of the receive. - protected override void EndReceiveFrom(IAsyncResult ar) - { - try - { - // When socket is closed the object will be disposed of in the middle of a receive. - if (!m_isClosed) - { - EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - int bytesRead = m_socket.EndReceiveFrom(ar, ref remoteEP); - - if (bytesRead > 0) - { - ProcessRawBuffer(bytesRead + m_recvOffset, remoteEP as IPEndPoint); - } - } - - // If there is still data available it should be read now. This is more efficient than calling - // BeginReceiveFrom which will incur the overhead of creating the callback and then immediately firing it. - // It also avoids the situation where if the application cannot keep up with the network then BeginReceiveFrom - // will be called synchronously (if data is available it calls the callback method immediately) which can - // create a very nasty stack. - if (!m_isClosed && m_socket.Available > 0) - { - while (!m_isClosed && m_socket.Available > 0) - { - EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - var recvLength = m_recvBuffer.Length - m_recvOffset; - //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values - if (recvLength <= 0 || m_recvOffset < 0) - { - m_recvOffset = 0; - recvLength = m_recvBuffer.Length; - } - int bytesReadSync = m_socket.ReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref remoteEP); - - if (bytesReadSync > 0) - { - if (ProcessRawBuffer(bytesReadSync + m_recvOffset, remoteEP as IPEndPoint) == 0) - { - break; - } - } - else - { - break; - } - } - } - } - catch (SocketException resetSockExcp) when (resetSockExcp.SocketErrorCode == SocketError.ConnectionReset) - { - // Thrown when close is called on a socket from this end. Safe to ignore. - } - catch (SocketException sockExcp) - { - // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during - // normal RTP operation. For example: - // - the RTP connection may start sending before the remote socket starts listening, - // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old - // or new socket during the transition. - // It also seems that once a UDP socket pair have exchanged packets and the remote party closes the socket exception will occur - // in the BeginReceive method (very handy). Follow-up, this doesn't seem to be the case, the socket exception can occur in - // BeginReceive before any packets have been exchanged. This means it's not safe to close if BeginReceive gets an ICMP - // error since the remote party may not have initialised their socket yet. - logger.LogWarning(sockExcp, "SocketException IceTcpReceiver.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { } - catch (Exception excp) - { - logger.LogError(excp, "Exception IceTcpReceiver.EndReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - finally - { - m_isRunningReceive = false; - if (!m_isClosed) - { - BeginReceiveFrom(); - } - } - } - - // TODO: If we miss any package because slow internet connection - // and initial byte in buffer is not a STUNHeader (starts with 0x00 0x00) - // and our receive buffer is full, we need a way to discard whole buffer - // or check for 0x00 0x00 start again. - protected virtual int ProcessRawBuffer(int bytesRead, IPEndPoint remoteEP) - { - var extractCount = 0; - if (bytesRead > 0) - { - // During experiments IPPacketInformation wasn't getting set on Linux. Without it the local IP address - // cannot be determined when a listener was bound to IPAddress.Any (or IPv6 equivalent). If the caller - // is relying on getting the local IP address on Linux then something may fail. - //if (packetInfo != null && packetInfo.Address != null) - //{ - // localEndPoint = new IPEndPoint(packetInfo.Address, localEndPoint.Port); - //} - - //Try extract all StunMessages from current receive buffer - var isFragmented = true; - var recvRemainingSegment = new ArraySegment(m_recvBuffer, 0, bytesRead); - - while (recvRemainingSegment.Count > STUNHeader.STUN_HEADER_LENGTH) - { - isFragmented = false; - STUNHeader header = null; - try - { - header = STUNHeader.ParseSTUNHeader(recvRemainingSegment); - } - catch - { - header = null; - } - if (header != null) - { - int stunMsgBytes = STUNHeader.STUN_HEADER_LENGTH + header.MessageLength; - if (stunMsgBytes % 4 != 0) - { - stunMsgBytes = stunMsgBytes - (stunMsgBytes % 4) + 4; - } - - //We have the packet count all inside current receiving buffer - if (recvRemainingSegment.Count >= stunMsgBytes) - { - extractCount++; - m_recvOffset = recvRemainingSegment.Offset + recvRemainingSegment.Count; - - byte[] packetBuffer = new byte[stunMsgBytes]; - Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, packetBuffer, 0, stunMsgBytes); - - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP, packetBuffer); - - var newOffset = recvRemainingSegment.Offset + stunMsgBytes; - var newCount = recvRemainingSegment.Count - stunMsgBytes; - if (newCount > STUNHeader.STUN_HEADER_LENGTH && newOffset >= 0) - { - recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); - } - else - { - if (newCount > 0 && newOffset >= 0) - { - recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); - isFragmented = true; - } - else - { - recvRemainingSegment = new ArraySegment(); - isFragmented = false; - } - break; - } - } - //We have a fragmentation but the header is intact, we need to cache the fragmentation for the next receive cycle - else - { - isFragmented = true; - break; - } - } - //Save Remaining Buffer in start of m_recvBuffer - else - { - isFragmented = true; - break; - } - } - - if (isFragmented) - { - m_recvOffset = recvRemainingSegment.Count; - Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, m_recvBuffer, 0, recvRemainingSegment.Count); - } - else - { - m_recvOffset = 0; - } - } - - return extractCount; - } - - /// - /// Closes the socket and stops any new receives from being initiated. - /// - public override void Close(string reason) - { - if (!m_isClosed) - { - if (m_socket != null && m_socket.Connected) - { - m_socket?.Disconnect(false); - } - base.Close(reason); - } - } - } - /// /// ICE transaction spacing interval in milliseconds. /// diff --git a/src/net/ICE/Transport/IceTcpReceiver.cs b/src/net/ICE/Transport/IceTcpReceiver.cs new file mode 100644 index 000000000..d2b9bcfba --- /dev/null +++ b/src/net/ICE/Transport/IceTcpReceiver.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace SIPSorcery.Net +{ + public partial class RtpIceChannel + { + public class IceTcpReceiver : UdpReceiver + { + protected const int REVEIVE_TCP_BUFFER_SIZE = RECEIVE_BUFFER_SIZE * 2; + + protected int m_recvOffset; + public IceTcpReceiver(Socket socket, int mtu = REVEIVE_TCP_BUFFER_SIZE) : base(socket, mtu) + { + m_recvOffset = 0; + } + + /// + /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to + /// return any data received. + /// + public override void BeginReceiveFrom() + { + //Prevent call BeginReceiveFrom if it is already running or into invalid state + if ((m_isClosed || !m_socket.Connected) && m_isRunningReceive) + { + m_isRunningReceive = false; + } + if (m_isRunningReceive || m_isClosed || !m_socket.Connected) + { + return; + } + + try + { + m_isRunningReceive = true; + EndPoint recvEndPoint = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + var recvLength = m_recvBuffer.Length - m_recvOffset; + //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values + if (recvLength <= 0 || m_recvOffset < 0) + { + m_recvOffset = 0; + recvLength = m_recvBuffer.Length; + } + m_socket.BeginReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref recvEndPoint, EndReceiveFrom, null); + } + // Thrown when socket is closed. Can be safely ignored. + // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. + // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the + // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. + catch (ObjectDisposedException) + { + m_isRunningReceive = false; + } + catch (SocketException sockExcp) + { + m_isRunningReceive = false; + logger.LogWarning(sockExcp, "Socket error {SocketErrorCode} in IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); + //Close(sockExcp.Message); + } + catch (Exception excp) + { + m_isRunningReceive = false; + // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 + // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that + // case the socket can be considered to be unusable and there's no point trying another receive. + logger.LogError(excp, "Exception IceTcpReceiver.BeginReceiveFrom. {ErrorMessage}", excp.Message); + Close(excp.Message); + } + } + + /// + /// Handler for end of the begin receive call. + /// + /// Contains the results of the receive. + protected override void EndReceiveFrom(IAsyncResult ar) + { + try + { + // When socket is closed the object will be disposed of in the middle of a receive. + if (!m_isClosed) + { + EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + int bytesRead = m_socket.EndReceiveFrom(ar, ref remoteEP); + + if (bytesRead > 0) + { + ProcessRawBuffer(bytesRead + m_recvOffset, remoteEP as IPEndPoint); + } + } + + // If there is still data available it should be read now. This is more efficient than calling + // BeginReceiveFrom which will incur the overhead of creating the callback and then immediately firing it. + // It also avoids the situation where if the application cannot keep up with the network then BeginReceiveFrom + // will be called synchronously (if data is available it calls the callback method immediately) which can + // create a very nasty stack. + if (!m_isClosed && m_socket.Available > 0) + { + while (!m_isClosed && m_socket.Available > 0) + { + EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + var recvLength = m_recvBuffer.Length - m_recvOffset; + //Discard fragmentation buffer as seems that we will have an incorrect result based in cached values + if (recvLength <= 0 || m_recvOffset < 0) + { + m_recvOffset = 0; + recvLength = m_recvBuffer.Length; + } + int bytesReadSync = m_socket.ReceiveFrom(m_recvBuffer, m_recvOffset, recvLength, SocketFlags.None, ref remoteEP); + + if (bytesReadSync > 0) + { + if (ProcessRawBuffer(bytesReadSync + m_recvOffset, remoteEP as IPEndPoint) == 0) + { + break; + } + } + else + { + break; + } + } + } + } + catch (SocketException resetSockExcp) when (resetSockExcp.SocketErrorCode == SocketError.ConnectionReset) + { + // Thrown when close is called on a socket from this end. Safe to ignore. + } + catch (SocketException sockExcp) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + // It also seems that once a UDP socket pair have exchanged packets and the remote party closes the socket exception will occur + // in the BeginReceive method (very handy). Follow-up, this doesn't seem to be the case, the socket exception can occur in + // BeginReceive before any packets have been exchanged. This means it's not safe to close if BeginReceive gets an ICMP + // error since the remote party may not have initialised their socket yet. + logger.LogWarning(sockExcp, "SocketException IceTcpReceiver.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); + } + catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. + { } + catch (Exception excp) + { + logger.LogError(excp, "Exception IceTcpReceiver.EndReceiveFrom. {ErrorMessage}", excp.Message); + Close(excp.Message); + } + finally + { + m_isRunningReceive = false; + if (!m_isClosed) + { + BeginReceiveFrom(); + } + } + } + + // TODO: If we miss any package because slow internet connection + // and initial byte in buffer is not a STUNHeader (starts with 0x00 0x00) + // and our receive buffer is full, we need a way to discard whole buffer + // or check for 0x00 0x00 start again. + protected virtual int ProcessRawBuffer(int bytesRead, IPEndPoint remoteEP) + { + var extractCount = 0; + if (bytesRead > 0) + { + // During experiments IPPacketInformation wasn't getting set on Linux. Without it the local IP address + // cannot be determined when a listener was bound to IPAddress.Any (or IPv6 equivalent). If the caller + // is relying on getting the local IP address on Linux then something may fail. + //if (packetInfo != null && packetInfo.Address != null) + //{ + // localEndPoint = new IPEndPoint(packetInfo.Address, localEndPoint.Port); + //} + + //Try extract all StunMessages from current receive buffer + var isFragmented = true; + var recvRemainingSegment = new ArraySegment(m_recvBuffer, 0, bytesRead); + + while (recvRemainingSegment.Count > STUNHeader.STUN_HEADER_LENGTH) + { + isFragmented = false; + STUNHeader header = null; + try + { + header = STUNHeader.ParseSTUNHeader(recvRemainingSegment); + } + catch + { + header = null; + } + if (header != null) + { + int stunMsgBytes = STUNHeader.STUN_HEADER_LENGTH + header.MessageLength; + if (stunMsgBytes % 4 != 0) + { + stunMsgBytes = stunMsgBytes - (stunMsgBytes % 4) + 4; + } + + //We have the packet count all inside current receiving buffer + if (recvRemainingSegment.Count >= stunMsgBytes) + { + extractCount++; + m_recvOffset = recvRemainingSegment.Offset + recvRemainingSegment.Count; + + byte[] packetBuffer = new byte[stunMsgBytes]; + Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, packetBuffer, 0, stunMsgBytes); + + CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP, packetBuffer); + + var newOffset = recvRemainingSegment.Offset + stunMsgBytes; + var newCount = recvRemainingSegment.Count - stunMsgBytes; + if (newCount > STUNHeader.STUN_HEADER_LENGTH && newOffset >= 0) + { + recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); + } + else + { + if (newCount > 0 && newOffset >= 0) + { + recvRemainingSegment = new ArraySegment(recvRemainingSegment.Array, newOffset, newCount); + isFragmented = true; + } + else + { + recvRemainingSegment = new ArraySegment(); + isFragmented = false; + } + break; + } + } + //We have a fragmentation but the header is intact, we need to cache the fragmentation for the next receive cycle + else + { + isFragmented = true; + break; + } + } + //Save Remaining Buffer in start of m_recvBuffer + else + { + isFragmented = true; + break; + } + } + + if (isFragmented) + { + m_recvOffset = recvRemainingSegment.Count; + Buffer.BlockCopy(recvRemainingSegment.Array, recvRemainingSegment.Offset, m_recvBuffer, 0, recvRemainingSegment.Count); + } + else + { + m_recvOffset = 0; + } + } + + return extractCount; + } + + /// + /// Closes the socket and stops any new receives from being initiated. + /// + public override void Close(string reason) + { + if (!m_isClosed) + { + if (m_socket != null && m_socket.Connected) + { + m_socket?.Disconnect(false); + } + base.Close(reason); + } + } + } + } +} diff --git a/src/net/RTP/RTPChannel.cs b/src/net/RTP/RTPChannel.cs index a6dbb423b..92a36d43f 100755 --- a/src/net/RTP/RTPChannel.cs +++ b/src/net/RTP/RTPChannel.cs @@ -27,247 +27,6 @@ namespace SIPSorcery.Net { public delegate void PacketReceivedDelegate(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet); - /// - /// A basic UDP socket manager. The RTP channel may need both an RTP and Control socket. This class encapsulates - /// the common logic for UDP socket management. - /// - /// - /// .NET Framework Socket source: - /// https://referencesource.microsoft.com/#system/net/system/net/Sockets/Socket.cs - /// .NET Core Socket source: - /// https://github.com/dotnet/runtime/blob/master/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs - /// Mono Socket source: - /// https://github.com/mono/mono/blob/master/mcs/class/System/System.Net.Sockets/Socket.cs - /// - public class UdpReceiver - { - /// - /// MTU is 1452 bytes so this should be heaps [AC 03 Nov 2024: turns out it's not when considering UDP fragmentation can - /// result in a max UDP payload of 65535 - 8 (header) = 65527 bytes]. - /// An issue was reported with a real World WeBRTC implementation producing UDP packet sizes of 2144 byes #1045. Consequently - /// updated from 2048 to 3000. - /// - protected const int RECEIVE_BUFFER_SIZE = 3000; - - protected static ILogger logger = Log.Logger; - - protected readonly Socket m_socket; - protected byte[] m_recvBuffer; - protected bool m_isClosed, m_isClosing; - protected bool m_isRunningReceive; - protected IPEndPoint m_localEndPoint; - protected AddressFamily m_addressFamily; - - public virtual bool IsClosed - { - get - { - return m_isClosed; - } - protected set - { - if (m_isClosed == value) - { - return; - } - m_isClosed = value; - } - } - - public virtual bool IsRunningReceive - { - get - { - return m_isRunningReceive; - } - protected set - { - if (m_isRunningReceive == value) - { - return; - } - m_isRunningReceive = value; - } - } - - /// - /// Fires when a new packet has been received on the UDP socket. - /// - public event PacketReceivedDelegate OnPacketReceived; - - /// - /// Fires when there is an error attempting to receive on the UDP socket. - /// - public event Action OnClosed; - - public UdpReceiver(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) - { - m_socket = socket; - m_localEndPoint = m_socket.LocalEndPoint as IPEndPoint; - m_recvBuffer = new byte[mtu]; - m_addressFamily = m_socket.LocalEndPoint.AddressFamily; - } - - /// - /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to - /// return any data received. - /// - public virtual void BeginReceiveFrom() - { - //Prevent call BeginReceiveFrom if it is already running - if(m_isClosed && m_isRunningReceive) - { - m_isRunningReceive = false; - } - if (m_isRunningReceive || m_isClosed || m_isClosing) - { - return; - } - - try - { - m_isRunningReceive = true; - EndPoint recvEndPoint = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - m_socket.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref recvEndPoint, EndReceiveFrom, null); - } - catch (ObjectDisposedException) - { - // Thrown when socket is closed. Can be safely ignored. - m_isRunningReceive = false; - } - catch (SocketException sockExcp) - { - // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. - // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the - // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. - - m_isRunningReceive = false; - logger.LogWarning("Socket error {SocketErrorCode} in UdpReceiver.BeginReceiveFrom. {Message}", sockExcp.SocketErrorCode, sockExcp.Message); - //Close(sockExcp.Message); - } - catch (Exception excp) - { - m_isRunningReceive = false; - // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 - // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that - // case the socket can be considered to be unusable and there's no point trying another receive. - logger.LogError(excp, "Exception UdpReceiver.BeginReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - } - - /// - /// Handler for end of the begin receive call. - /// - /// Contains the results of the receive. - protected virtual void EndReceiveFrom(IAsyncResult ar) - { - try - { - // When socket is closed the object will be disposed of in the middle of a receive. - if (!m_isClosed) - { - EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - int bytesRead = m_socket.EndReceiveFrom(ar, ref remoteEP); - - if (bytesRead > 0) - { - // During experiments IPPacketInformation wasn't getting set on Linux. Without it the local IP address - // cannot be determined when a listener was bound to IPAddress.Any (or IPv6 equivalent). If the caller - // is relying on getting the local IP address on Linux then something may fail. - //if (packetInfo != null && packetInfo.Address != null) - //{ - // localEndPoint = new IPEndPoint(packetInfo.Address, localEndPoint.Port); - //} - - byte[] packetBuffer = new byte[bytesRead]; - // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. - Buffer.BlockCopy(m_recvBuffer, 0, packetBuffer, 0, bytesRead); - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBuffer); - } - } - - // If there is still data available it should be read now. This is more efficient than calling - // BeginReceiveFrom which will incur the overhead of creating the callback and then immediately firing it. - // It also avoids the situation where if the application cannot keep up with the network then BeginReceiveFrom - // will be called synchronously (if data is available it calls the callback method immediately) which can - // create a very nasty stack. - if (!m_isClosed && m_socket.Available > 0) - { - while (!m_isClosed && m_socket.Available > 0) - { - EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); - int bytesReadSync = m_socket.ReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref remoteEP); - - if (bytesReadSync > 0) - { - byte[] packetBufferSync = new byte[bytesReadSync]; - // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. - Buffer.BlockCopy(m_recvBuffer, 0, packetBufferSync, 0, bytesReadSync); - CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBufferSync); - } - else - { - break; - } - } - } - } - catch (SocketException resetSockExcp) when (resetSockExcp.SocketErrorCode == SocketError.ConnectionReset) - { - // Thrown when close is called on a socket - m_isClosing = true; - } - catch (SocketException sockExcp) - { - // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during - // normal RTP operation. For example: - // - the RTP connection may start sending before the remote socket starts listening, - // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old - // or new socket during the transition. - // It also seems that once a UDP socket pair have exchanged packets and the remote party closes the socket exception will occur - // in the BeginReceive method (very handy). Follow-up, this doesn't seem to be the case, the socket exception can occur in - // BeginReceive before any packets have been exchanged. This means it's not safe to close if BeginReceive gets an ICMP - // error since the remote party may not have initialised their socket yet. - logger.LogWarning(sockExcp, "SocketException UdpReceiver.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); - } - catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. - { } - catch (Exception excp) - { - logger.LogError(excp, "Exception UdpReceiver.EndReceiveFrom. {ErrorMessage}", excp.Message); - Close(excp.Message); - } - finally - { - m_isRunningReceive = false; - if (!m_isClosed) - { - BeginReceiveFrom(); - } - } - } - - /// - /// Closes the socket and stops any new receives from being initiated. - /// - public virtual void Close(string reason) - { - if (!m_isClosed) - { - m_isClosed = true; - m_socket?.Close(); - - OnClosed?.Invoke(reason); - } - } - - protected virtual void CallOnPacketReceivedCallback(int localPort, IPEndPoint remoteEndPoint, byte[] packet) - { - OnPacketReceived?.Invoke(this, localPort, remoteEndPoint, packet); - } - } - public enum RTPChannelSocketsEnum { RTP = 0, diff --git a/src/net/RTP/Transport/UdpReceiver.cs b/src/net/RTP/Transport/UdpReceiver.cs new file mode 100644 index 000000000..f261fc8bc --- /dev/null +++ b/src/net/RTP/Transport/UdpReceiver.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using Microsoft.Extensions.Logging; +using SIPSorcery.Sys; + +namespace SIPSorcery.Net +{ + /// + /// A basic UDP socket manager. The RTP channel may need both an RTP and Control socket. This class encapsulates + /// the common logic for UDP socket management. + /// + /// + /// .NET Framework Socket source: + /// https://referencesource.microsoft.com/#system/net/system/net/Sockets/Socket.cs + /// .NET Core Socket source: + /// https://github.com/dotnet/runtime/blob/master/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs + /// Mono Socket source: + /// https://github.com/mono/mono/blob/master/mcs/class/System/System.Net.Sockets/Socket.cs + /// + public class UdpReceiver + { + /// + /// MTU is 1452 bytes so this should be heaps [AC 03 Nov 2024: turns out it's not when considering UDP fragmentation can + /// result in a max UDP payload of 65535 - 8 (header) = 65527 bytes]. + /// An issue was reported with a real World WeBRTC implementation producing UDP packet sizes of 2144 byes #1045. Consequently + /// updated from 2048 to 3000. + /// + protected const int RECEIVE_BUFFER_SIZE = 3000; + + protected static ILogger logger = Log.Logger; + + protected readonly Socket m_socket; + protected byte[] m_recvBuffer; + protected bool m_isClosed, m_isClosing; + protected bool m_isRunningReceive; + protected IPEndPoint m_localEndPoint; + protected AddressFamily m_addressFamily; + + public virtual bool IsClosed + { + get + { + return m_isClosed; + } + protected set + { + if (m_isClosed == value) + { + return; + } + m_isClosed = value; + } + } + + public virtual bool IsRunningReceive + { + get + { + return m_isRunningReceive; + } + protected set + { + if (m_isRunningReceive == value) + { + return; + } + m_isRunningReceive = value; + } + } + + /// + /// Fires when a new packet has been received on the UDP socket. + /// + public event PacketReceivedDelegate OnPacketReceived; + + /// + /// Fires when there is an error attempting to receive on the UDP socket. + /// + public event Action OnClosed; + + public UdpReceiver(Socket socket, int mtu = RECEIVE_BUFFER_SIZE) + { + m_socket = socket; + m_localEndPoint = m_socket.LocalEndPoint as IPEndPoint; + m_recvBuffer = new byte[mtu]; + m_addressFamily = m_socket.LocalEndPoint.AddressFamily; + } + + /// + /// Starts the receive. This method returns immediately. An event will be fired in the corresponding "End" event to + /// return any data received. + /// + public virtual void BeginReceiveFrom() + { + //Prevent call BeginReceiveFrom if it is already running + if (m_isClosed && m_isRunningReceive) + { + m_isRunningReceive = false; + } + if (m_isRunningReceive || m_isClosed || m_isClosing) + { + return; + } + + try + { + m_isRunningReceive = true; + EndPoint recvEndPoint = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + m_socket.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref recvEndPoint, EndReceiveFrom, null); + } + catch (ObjectDisposedException) + { + // Thrown when socket is closed. Can be safely ignored. + m_isRunningReceive = false; + } + catch (SocketException sockExcp) + { + // This exception can be thrown in response to an ICMP packet. The problem is the ICMP packet can be a false positive. + // For example if the remote RTP socket has not yet been opened the remote host could generate an ICMP packet for the + // initial RTP packets. Experience has shown that it's not safe to close an RTP connection based solely on ICMP packets. + + m_isRunningReceive = false; + logger.LogWarning("Socket error {SocketErrorCode} in UdpReceiver.BeginReceiveFrom. {Message}", sockExcp.SocketErrorCode, sockExcp.Message); + //Close(sockExcp.Message); + } + catch (Exception excp) + { + m_isRunningReceive = false; + // From https://github.com/dotnet/corefx/blob/e99ec129cfd594d53f4390bf97d1d736cff6f860/src/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L3262 + // the BeginReceiveFrom will only throw if there is an problem with the arguments or the socket has been disposed of. In that + // case the socket can be considered to be unusable and there's no point trying another receive. + logger.LogError(excp, "Exception UdpReceiver.BeginReceiveFrom. {ErrorMessage}", excp.Message); + Close(excp.Message); + } + } + + /// + /// Handler for end of the begin receive call. + /// + /// Contains the results of the receive. + protected virtual void EndReceiveFrom(IAsyncResult ar) + { + try + { + // When socket is closed the object will be disposed of in the middle of a receive. + if (!m_isClosed) + { + EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + int bytesRead = m_socket.EndReceiveFrom(ar, ref remoteEP); + + if (bytesRead > 0) + { + // During experiments IPPacketInformation wasn't getting set on Linux. Without it the local IP address + // cannot be determined when a listener was bound to IPAddress.Any (or IPv6 equivalent). If the caller + // is relying on getting the local IP address on Linux then something may fail. + //if (packetInfo != null && packetInfo.Address != null) + //{ + // localEndPoint = new IPEndPoint(packetInfo.Address, localEndPoint.Port); + //} + + byte[] packetBuffer = new byte[bytesRead]; + // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. + Buffer.BlockCopy(m_recvBuffer, 0, packetBuffer, 0, bytesRead); + CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBuffer); + } + } + + // If there is still data available it should be read now. This is more efficient than calling + // BeginReceiveFrom which will incur the overhead of creating the callback and then immediately firing it. + // It also avoids the situation where if the application cannot keep up with the network then BeginReceiveFrom + // will be called synchronously (if data is available it calls the callback method immediately) which can + // create a very nasty stack. + if (!m_isClosed && m_socket.Available > 0) + { + while (!m_isClosed && m_socket.Available > 0) + { + EndPoint remoteEP = m_addressFamily == AddressFamily.InterNetwork ? new IPEndPoint(IPAddress.Any, 0) : new IPEndPoint(IPAddress.IPv6Any, 0); + int bytesReadSync = m_socket.ReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref remoteEP); + + if (bytesReadSync > 0) + { + byte[] packetBufferSync = new byte[bytesReadSync]; + // TODO: When .NET Framework support is dropped switch to using a slice instead of a copy. + Buffer.BlockCopy(m_recvBuffer, 0, packetBufferSync, 0, bytesReadSync); + CallOnPacketReceivedCallback(m_localEndPoint.Port, remoteEP as IPEndPoint, packetBufferSync); + } + else + { + break; + } + } + } + } + catch (SocketException resetSockExcp) when (resetSockExcp.SocketErrorCode == SocketError.ConnectionReset) + { + // Thrown when close is called on a socket + m_isClosing = true; + } + catch (SocketException sockExcp) + { + // Socket errors do not trigger a close. The reason being that there are genuine situations that can cause them during + // normal RTP operation. For example: + // - the RTP connection may start sending before the remote socket starts listening, + // - an on hold, transfer, etc. operation can change the RTP end point which could result in socket errors from the old + // or new socket during the transition. + // It also seems that once a UDP socket pair have exchanged packets and the remote party closes the socket exception will occur + // in the BeginReceive method (very handy). Follow-up, this doesn't seem to be the case, the socket exception can occur in + // BeginReceive before any packets have been exchanged. This means it's not safe to close if BeginReceive gets an ICMP + // error since the remote party may not have initialised their socket yet. + logger.LogWarning(sockExcp, "SocketException UdpReceiver.EndReceiveFrom ({SocketErrorCode}). {ErrorMessage}", sockExcp.SocketErrorCode, sockExcp.Message); + } + catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. + { } + catch (Exception excp) + { + logger.LogError(excp, "Exception UdpReceiver.EndReceiveFrom. {ErrorMessage}", excp.Message); + Close(excp.Message); + } + finally + { + m_isRunningReceive = false; + if (!m_isClosed) + { + BeginReceiveFrom(); + } + } + } + + /// + /// Closes the socket and stops any new receives from being initiated. + /// + public virtual void Close(string reason) + { + if (!m_isClosed) + { + m_isClosed = true; + m_socket?.Close(); + + OnClosed?.Invoke(reason); + } + } + + protected virtual void CallOnPacketReceivedCallback(int localPort, IPEndPoint remoteEndPoint, byte[] packet) + { + OnPacketReceived?.Invoke(this, localPort, remoteEndPoint, packet); + } + } +} From 1504f41a6c9bfb223f0466295b68b347c01cdc84 Mon Sep 17 00:00:00 2001 From: Haves Irfan <20160532+ha-ves@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:24:53 +0900 Subject: [PATCH 2/2] sanitize, first pass --- src/net/ICE/RtpIceChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/ICE/RtpIceChannel.cs b/src/net/ICE/RtpIceChannel.cs index d682031ec..9fc1e5b7b 100755 --- a/src/net/ICE/RtpIceChannel.cs +++ b/src/net/ICE/RtpIceChannel.cs @@ -112,7 +112,7 @@ namespace SIPSorcery.Net /// action needs to be taken to update the status of the ICE server or checklist entry /// check. /// - partial class RtpIceChannel : RTPChannel + public partial class RtpIceChannel : RTPChannel { private const int ICE_UFRAG_LENGTH = 4; private const int ICE_PASSWORD_LENGTH = 24;