From b0363704a54e774b02fbedc5f9e33c056527791f Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 12:58:51 +0800 Subject: [PATCH 1/8] add offset and count to senddata to reduce allocations --- src/SIPSorcery/net/SCTP/SctpAssociation.cs | 6 ++++-- src/SIPSorcery/net/SCTP/SctpDataSender.cs | 16 ++++++++++------ src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs | 2 +- src/SIPSorcery/net/WebRTC/RTCDataChannel.cs | 14 +++++++++----- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/SIPSorcery/net/SCTP/SctpAssociation.cs b/src/SIPSorcery/net/SCTP/SctpAssociation.cs index 994abcf1bf..21e4d0c383 100644 --- a/src/SIPSorcery/net/SCTP/SctpAssociation.cs +++ b/src/SIPSorcery/net/SCTP/SctpAssociation.cs @@ -589,7 +589,9 @@ public void SendData(ushort streamID, uint ppid, string message) /// The stream ID to sent the data on. /// The payload protocol ID for the data. /// The byte data to send. - public void SendData(ushort streamID, uint ppid, byte[] data) + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) { if (_wasAborted) { @@ -603,7 +605,7 @@ public void SendData(ushort streamID, uint ppid, byte[] data) } else { - _dataSender.SendData(streamID, ppid, data); + _dataSender.SendData(streamID, ppid, data, offset, count); } } diff --git a/src/SIPSorcery/net/SCTP/SctpDataSender.cs b/src/SIPSorcery/net/SCTP/SctpDataSender.cs index 4a2ffb2534..a37cef77d9 100644 --- a/src/SIPSorcery/net/SCTP/SctpDataSender.cs +++ b/src/SIPSorcery/net/SCTP/SctpDataSender.cs @@ -304,8 +304,12 @@ public void GotSack(SctpSackChunk sack) /// The stream ID to sent the data on. /// The payload protocol ID for the data. /// The byte data to send. - public void SendData(ushort streamID, uint ppid, byte[] data) + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1) { + int dataCount = count < 0 ? data.Length - offset : count; + lock (_sendQueue) { ushort seqnum = 0; @@ -323,17 +327,17 @@ public void SendData(ushort streamID, uint ppid, byte[] data) _streamSeqnums.Add(streamID, 0); } - for (int index = 0; index * _defaultMTU < data.Length; index++) + for (int index = 0; index * _defaultMTU < dataCount; index++) { - int offset = (index == 0) ? 0 : (index * _defaultMTU); - int payloadLength = (offset + _defaultMTU < data.Length) ? _defaultMTU : data.Length - offset; + int chunkOffset = index * _defaultMTU; + int payloadLength = (chunkOffset + _defaultMTU < dataCount) ? _defaultMTU : dataCount - chunkOffset; // Future TODO: Replace with slice when System.Memory is introduced as a dependency. byte[] payload = new byte[payloadLength]; - Buffer.BlockCopy(data, offset, payload, 0, payloadLength); + Buffer.BlockCopy(data, offset + chunkOffset, payload, 0, payloadLength); bool isBegining = index == 0; - bool isEnd = ((offset + payloadLength) >= data.Length) ? true : false; + bool isEnd = ((chunkOffset + payloadLength) >= dataCount) ? true : false; SctpDataChunk dataChunk = new SctpDataChunk( false, diff --git a/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs b/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs index 778da05345..1bdbe2a269 100644 --- a/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs +++ b/src/SIPSorcery/net/WebRTC/IRTCDataChannel.cs @@ -161,7 +161,7 @@ interface IRTCDataChannel string binaryType { get; set; } void send(string data); - void send(byte[] data); + void send(byte[] data, int offset = 0, int count = -1); }; public class RTCDataChannelInit diff --git a/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs b/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs index 32cbaed381..8a0d0b570c 100644 --- a/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs +++ b/src/SIPSorcery/net/WebRTC/RTCDataChannel.cs @@ -165,11 +165,15 @@ public void send(string message) /// Sends a binary data payload on the data channel. /// /// The data to send. - public void send(byte[] data) + /// The offset in at which to begin sending. Defaults to 0. + /// The number of bytes to send. Defaults to -1, meaning all bytes from to the end of the array. + public void send(byte[] data, int offset = 0, int count = -1) { - if (data.Length > _transport.maxMessageSize) + int effectiveCount = count < 0 ? data.Length - offset : count; + + if (effectiveCount > _transport.maxMessageSize) { - throw new ApplicationException($"Data channel {label} was requested to send data of length {data.Length} " + + throw new ApplicationException($"Data channel {label} was requested to send data of length {effectiveCount} " + $" that exceeded the maximum allowed message size of {_transport.maxMessageSize}."); } else if (_transport.state != RTCSctpTransportState.Connected) @@ -180,7 +184,7 @@ public void send(byte[] data) { lock (this) { - if (data?.Length == 0) + if (effectiveCount == 0) { _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), (uint)DataChannelPayloadProtocols.WebRTC_Binary_Empty, @@ -190,7 +194,7 @@ public void send(byte[] data) { _transport.RTCSctpAssociation.SendData(id.GetValueOrDefault(), (uint)DataChannelPayloadProtocols.WebRTC_Binary, - data); + data, offset, effectiveCount); } } } From 2cb0ebda49e2657e2c205cb68e8666b392a06ef3 Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 13:06:03 +0800 Subject: [PATCH 2/8] Support multiple matching Uris in header extensions --- .../RTPHeaderExtensions/RTPHeaderExtension.cs | 13 ++++++++++++- .../TransportWideCCExtension.cs | 19 ++++++++++++++++++- .../net/WebRTC/RTCPeerConnection.cs | 6 ++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs index 64c25733cd..65319d0ad2 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs @@ -69,11 +69,22 @@ public RTPHeaderExtension(int id, string uri, int extensionSize, RTPHeaderExtens } } + /// + /// Checks if the URI provided matches the URI of this extension + /// Override this method if the extension can have multiple URIs (for example TransportWideCCExtension) + /// + /// + /// + public virtual bool MatchesExtension(string uri) + { + return Uri.Equals(uri, StringComparison.InvariantCultureIgnoreCase); + } + // Id / "extmap" public int Id { get; internal set; } // Uri - public string Uri { get; } + public string Uri { get; set; } public int ExtensionSize { get; } diff --git a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs index 554a211f6a..cd6138f00a 100644 --- a/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs +++ b/src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs @@ -33,11 +33,28 @@ public class TransportWideCCExtension : RTPHeaderExtension // public const string RTP_HEADER_EXTENSION_URI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; - //public const string RTP_HEADER_EXTENSION_URI_ALT = "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02"; + + + public override bool MatchesExtension(string uri) + { + switch (uri.ToLower()) + { + case RTP_HEADER_EXTENSION_URI: + case "urn:ietf:params:rtp-hdrext:transport-wide-cc": //official urn registered with IANA + case "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02": + return true; + } + return false; + } internal const int RTP_HEADER_EXTENSION_SIZE = 2; // TWCC payload: 2 bytes for sequence number. + public TransportWideCCExtension(int id, string uri) + : base(id, uri, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte) + { + } + /// /// The TWCC sequence number. /// diff --git a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs index d5ab6f7409..3df0540deb 100644 --- a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs +++ b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs @@ -999,11 +999,12 @@ public RTCSessionDescriptionInit createAnswer(RTCAnswerOptions options = null) { foreach (var remoteExtension in remoteHeaderExtensions) { - var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.Uri == remoteExtension.Uri); + var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.MatchesExtension(remoteExtension.Uri)); if ((localExtension != null) && _rtpExtensionsUsed.ContainsKey(remoteExtension.Uri)) { // We must ensure to use same Id by extension localExtension.Id = _rtpExtensionsUsed[remoteExtension.Uri]; + localExtension.Uri = remoteExtension.Uri;// Keep same Uri as remote logger.LogDebug("[createAnswer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); ann.HeaderExtensions.Add(localExtension.Id, localExtension); @@ -1023,11 +1024,12 @@ public RTCSessionDescriptionInit createAnswer(RTCAnswerOptions options = null) { foreach (var remoteExtension in remoteHeaderExtensions) { - var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.Uri == remoteExtension.Uri); + var localExtension = localHeaderExtensions.FirstOrDefault(ext => ext.MatchesExtension(remoteExtension.Uri)); if ((localExtension != null) && _rtpExtensionsUsed.ContainsKey(remoteExtension.Uri)) { // We must ensure to use same Id by extension localExtension.Id = _rtpExtensionsUsed[remoteExtension.Uri]; + localExtension.Uri = remoteExtension.Uri; // Keep same Uri as remote logger.LogDebug("[createAnswer] - {Media}:[{MediaID}] - Add HeaderExtensions:[{Id} - {Uri}]", ann.Media, ann.MediaID, localExtension.Id, localExtension.Uri); ann.HeaderExtensions.Add(localExtension.Id, localExtension); From 53368a0622d84427b8aa29574792543652fd1743 Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 13:20:39 +0800 Subject: [PATCH 3/8] target net10 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 19789242d6..a98a0eaf06 100644 --- a/.gitignore +++ b/.gitignore @@ -413,3 +413,4 @@ MigrationBackup/ # Image files *.bmp +/src/SIPSorcery/Directory.Build.props From e1f9e14bbfdeea15796738295e1cfe919b372dab Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 13:21:01 +0800 Subject: [PATCH 4/8] remove un-needed packages --- src/SIPSorcery/SIPSorcery.csproj | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/SIPSorcery/SIPSorcery.csproj b/src/SIPSorcery/SIPSorcery.csproj index c45885c8ac..c6c584fc89 100644 --- a/src/SIPSorcery/SIPSorcery.csproj +++ b/src/SIPSorcery/SIPSorcery.csproj @@ -23,11 +23,9 @@ - - - + @@ -40,11 +38,11 @@ - netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0 + net10.0 latest true $(NoWarn);SYSLIB0050 - True + False true $(NoWarn);CS1591;CS1573;CS1587 From 73739eed3d48e963e8e019a15fe863367873661b Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 14:11:05 +0800 Subject: [PATCH 5/8] Add TURNS and STUNS support Fix DNS bug with immediate timeout --- src/SIPSorcery/net/ICE/IceServerResolver.cs | 12 +- src/SIPSorcery/net/ICE/RtpIceChannel.cs | 180 ++++++++++++++++-- .../net/WebRTC/RTCPeerConnection.cs | 4 + 3 files changed, 172 insertions(+), 24 deletions(-) diff --git a/src/SIPSorcery/net/ICE/IceServerResolver.cs b/src/SIPSorcery/net/ICE/IceServerResolver.cs index bdae4c967d..6f8bf3a8ff 100644 --- a/src/SIPSorcery/net/ICE/IceServerResolver.cs +++ b/src/SIPSorcery/net/ICE/IceServerResolver.cs @@ -68,9 +68,8 @@ public void InitialiseIceServers( continue; } - // Filter out TLS or policy excluded entries - if (stunUri.Scheme is STUNSchemesEnum.stuns or STUNSchemesEnum.turns || - (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun)) + // Filter out policy excluded entries (TURNS/STUNS are now supported via TLS) + if (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun) { logger.LogWarning("{caller} ignoring ICE server {stunUri} (scheme {scheme})", nameof(IceServerResolver), stunUri, stunUri.Scheme); continue; @@ -117,7 +116,12 @@ private void ScheduleDnsLookup(STUNUri key, IceServer server) if (_iceServers.ContainsKey(key)) { - _iceServers[key].DnsLookupSentAt = DateTime.UtcNow; + // NOTE: must use DateTime.Now (not UtcNow) because RtpIceChannel.CheckIceServers + // measures the elapsed time with DateTime.Now.Subtract(DnsLookupSentAt). Using + // UtcNow here makes the timeout fire ~immediately in any non-UTC timezone (the + // sign of the local offset determines whether the channel gives up before DNS + // resolves or never times out at all). + _iceServers[key].DnsLookupSentAt = DateTime.Now; } logger.LogDebug("{caller} starting DNS lookup for ICE server {Uri}", nameof(IceServerResolver), key); diff --git a/src/SIPSorcery/net/ICE/RtpIceChannel.cs b/src/SIPSorcery/net/ICE/RtpIceChannel.cs index c9f5708d57..26d0c3f07f 100644 --- a/src/SIPSorcery/net/ICE/RtpIceChannel.cs +++ b/src/SIPSorcery/net/ICE/RtpIceChannel.cs @@ -69,6 +69,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; @@ -439,6 +440,15 @@ public override void Close(string reason) internal IceServerResolver _iceServerResolver = new IceServerResolver(); + // Tracks one SslStream per TURNS/STUNS server URI so subsequent sends reuse the TLS session. + private ConcurrentDictionary _tlsStreams = new ConcurrentDictionary(); + + // Per-URI write gate. SslStream.Write is NOT thread-safe and throws + // "another write operation is pending" if two threads (e.g. ICE + // connectivity checks + TURN permission refreshes + framed SCTP data) + // race. We also use this to make the lazy handshake atomic. + private ConcurrentDictionary _tlsWriteLocks = new ConcurrentDictionary(); + private IceServer _activeIceServer; public RTCIceComponent Component { get; private set; } @@ -769,6 +779,13 @@ protected void StartTcpRtpReceiver() var stunUri = pair.Key; var tcpSocket = pair.Value; + // TURNS/STUNS use an SslStream owned by SendOverTCP/StartTlsReadLoop, so the + // plain TCP receiver loop must not also try to read from the raw socket. + if (stunUri != null && (stunUri.Scheme == STUNSchemesEnum.turns || stunUri.Scheme == STUNSchemesEnum.stuns)) + { + continue; + } + if (stunUri != null && !m_rtpTcpReceiverByUri.ContainsKey(stunUri) && tcpSocket != null) { var rtpTcpReceiver = new IceTcpReceiver(tcpSocket); @@ -1065,7 +1082,7 @@ private void RefreshTurn(Object state) { return; } - if (_activeIceServer._uri.Scheme != STUNSchemesEnum.turn || NominatedEntry.LocalCandidate.IceServer is null) + if ((_activeIceServer._uri.Scheme != STUNSchemesEnum.turn && _activeIceServer._uri.Scheme != STUNSchemesEnum.turns) || NominatedEntry.LocalCandidate.IceServer is null) { _refreshTurnTimer?.Dispose(); return; @@ -1155,8 +1172,8 @@ private void CheckIceServers(Object state) logger.LogDebug("RTP ICE Channel was not able to acquire an active ICE server, stopping ICE servers timer."); _processIceServersTimer.Dispose(); } - else if ((_activeIceServer._uri.Scheme == STUNSchemesEnum.turn && _activeIceServer.RelayEndPoint != null) || - (_activeIceServer._uri.Scheme == STUNSchemesEnum.stun && _activeIceServer.ServerReflexiveEndPoint != null)) + else if (((_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns) && _activeIceServer.RelayEndPoint != null) || + ((_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns) && _activeIceServer.ServerReflexiveEndPoint != null)) { // Successfully set up the ICE server. Do nothing. } @@ -1185,13 +1202,13 @@ private void CheckIceServers(Object state) _activeIceServer.Error = SocketError.TimedOut; } // Send STUN binding request. - else if (_activeIceServer.ServerReflexiveEndPoint == null && _activeIceServer._uri.Scheme == STUNSchemesEnum.stun) + else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns)) { logger.LogDebug("Sending STUN binding request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint); _activeIceServer.Error = SendStunBindingRequest(_activeIceServer); } // Send TURN binding request. - else if (_activeIceServer.ServerReflexiveEndPoint == null && _activeIceServer._uri.Scheme == STUNSchemesEnum.turn) + else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns)) { logger.LogDebug("Sending TURN allocate request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint); _activeIceServer.Error = SendTurnAllocateRequest(_activeIceServer); @@ -2411,27 +2428,79 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer) return e1.Port == e2.Port && e1.Address.Equals(e2.Address); }; - if (!sendSocket.Connected || !(sendSocket.RemoteEndPoint is IPEndPoint) || !equals(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint)) + bool isTls = iceServer._uri.Scheme == STUNSchemesEnum.turns || iceServer._uri.Scheme == STUNSchemesEnum.stuns; + if (isTls) { - if (sendSocket.Connected) + // --- TLS PATH (TURNS / STUNS) --- + // SslStream.Write is not thread-safe, and the lazy handshake/connect + // must also be atomic so two threads don't both create a stream. + var writeLock = _tlsWriteLocks.GetOrAdd(iceServer._uri, _ => new SemaphoreSlim(1, 1)); + writeLock.Wait(); + try { - logger.LogDebug("SendOverTCP request disconnect."); - sendSocket.Disconnect(true); - } - sendSocket.Connect(dstEndPoint); + if (!_tlsStreams.TryGetValue(iceServer._uri, out SslStream sslStream)) + { + // Connect the raw socket if needed + if (!sendSocket.Connected) + { + sendSocket.Connect(dstEndPoint); + } - logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint); - } + // Wrap in SslStream. Validation is left permissive here; tighten if needed. + sslStream = new SslStream(new NetworkStream(sendSocket, false), false, + (sender, cert, chain, errors) => true, null); - //Fix ReceiveFrom logic if any previous exception happens - m_rtpTcpReceiverByUri.TryGetValue(iceServer?._uri, out IceTcpReceiver rtpTcpReceiver); - if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed) - { - rtpTcpReceiver.BeginReceiveFrom(); + try + { + // Perform TLS handshake using the hostname from the URI for SNI/cert match. + sslStream.AuthenticateAsClient(iceServer._uri.Host); + + _tlsStreams.TryAdd(iceServer._uri, sslStream); + logger.LogDebug("TLS handshake successful for {Uri}", iceServer._uri); + + // Start a dedicated read loop for this SSL stream + _ = StartTlsReadLoop(iceServer._uri, sslStream, dstEndPoint); + } + catch (Exception tlsEx) + { + logger.LogError(tlsEx, "TLS handshake failed for {Uri}: {Message}", iceServer._uri, tlsEx.Message); + return SocketError.SocketError; + } + } + + // Write to the SSL stream (serialised by writeLock). + sslStream.Write(buffer); + return SocketError.Success; + } + finally + { + writeLock.Release(); + } } + else + { + if (!sendSocket.Connected || !(sendSocket.RemoteEndPoint is IPEndPoint) || !equals(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint)) + { + if (sendSocket.Connected) + { + logger.LogDebug("SendOverTCP request disconnect."); + sendSocket.Disconnect(true); + } + sendSocket.Connect(dstEndPoint); - sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket); - return SocketError.Success; + logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint); + } + + //Fix ReceiveFrom logic if any previous exception happens + m_rtpTcpReceiverByUri.TryGetValue(iceServer?._uri, out IceTcpReceiver rtpTcpReceiver); + if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed) + { + rtpTcpReceiver.BeginReceiveFrom(); + } + + sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket); + return SocketError.Success; + } } catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored. { @@ -2449,6 +2518,77 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer) } } + /// + /// Reads framed STUN / TURN traffic off a TLS stream (TURNS / STUNS). STUN and TURN + /// allocation messages share a 20-byte header where bytes 2-3 are the body length in + /// big-endian; TURN channel-data messages instead use a 4-byte header in the + /// 0x4000-0x7FFF channel-number range. We accumulate bytes until we have a full + /// message and then dispatch it via . + /// + private async Task StartTlsReadLoop(STUNUri uri, SslStream sslStream, IPEndPoint remoteEndPoint) + { + byte[] receiveBuffer = new byte[4096]; + List streamBuffer = new List(); + + logger.LogDebug("Starting TLS read loop for {Uri}", uri); + + try + { + while (!IsClosed && sslStream.CanRead) + { + int bytesRead = await sslStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length).ConfigureAwait(false); + + if (bytesRead == 0) + { + logger.LogWarning("TLS stream closed remotely for {Uri}", uri); + break; + } + + streamBuffer.AddRange(new ArraySegment(receiveBuffer, 0, bytesRead)); + + // Process complete packets from the stream buffer + while (streamBuffer.Count >= 4) // Minimum header size + { + int bodyLength = (streamBuffer[2] << 8) | streamBuffer[3]; + int totalPacketLength = 20 + bodyLength; // STUN/TURN header (20) + body + + // TURN Channel Data has a 4-byte header; channel range is 0x4000 -> 0x7FFF + if (streamBuffer[0] >= 0x40 && streamBuffer[0] <= 0x7F) + { + totalPacketLength = 4 + bodyLength; + } + + if (streamBuffer.Count >= totalPacketLength) + { + byte[] packetBytes = streamBuffer.GetRange(0, totalPacketLength).ToArray(); + streamBuffer.RemoveRange(0, totalPacketLength); + OnRTPPacketReceived(null, 0, remoteEndPoint, packetBytes); + } + else + { + // Not enough data yet, wait for the next ReadAsync. + break; + } + } + } + } + catch (Exception ex) + { + if (!IsClosed) + { + logger.LogError(ex, "TLS read loop exception for {Uri}: {Message}", uri, ex.Message); + } + } + finally + { + _tlsStreams.TryRemove(uri, out _); + if (_tlsWriteLocks.TryRemove(uri, out var removedLock)) + { + removedLock.Dispose(); + } + } + } + protected virtual void EndSendToTCP(IAsyncResult ar) { try diff --git a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs index 3df0540deb..6492d62fe2 100644 --- a/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs +++ b/src/SIPSorcery/net/WebRTC/RTCPeerConnection.cs @@ -347,6 +347,10 @@ public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0, PortR { _configuration = configuration; + //test turns: + //_configuration.iceTransportPolicy = RTCIceTransportPolicy.relay; + //_configuration.iceServers = _configuration.iceServers.Where(p => p.urls.StartsWith("turns")).ToList(); + if (!InitializeCertificates(configuration)) { logger.LogDebug("No DTLS certificate is provided in the configuration"); From 560d47c9d9d338b02b3a2dddd3cdeeef1cedd0e8 Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 14:18:48 +0800 Subject: [PATCH 6/8] Revert "target net10" This reverts commit 53368a0622d84427b8aa29574792543652fd1743. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a98a0eaf06..19789242d6 100644 --- a/.gitignore +++ b/.gitignore @@ -413,4 +413,3 @@ MigrationBackup/ # Image files *.bmp -/src/SIPSorcery/Directory.Build.props From 9699ad2036014f625de0f441dd7af0cb6f7a91e6 Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 14:19:17 +0800 Subject: [PATCH 7/8] Revert "remove un-needed packages" This reverts commit e1f9e14bbfdeea15796738295e1cfe919b372dab. --- src/SIPSorcery/SIPSorcery.csproj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/SIPSorcery/SIPSorcery.csproj b/src/SIPSorcery/SIPSorcery.csproj index c6c584fc89..c45885c8ac 100644 --- a/src/SIPSorcery/SIPSorcery.csproj +++ b/src/SIPSorcery/SIPSorcery.csproj @@ -23,9 +23,11 @@ + - + + @@ -38,11 +40,11 @@ - net10.0 + netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0 latest true $(NoWarn);SYSLIB0050 - False + True true $(NoWarn);CS1591;CS1573;CS1587 From 9d568571d317f6cb59c484e1c4ce5d6cd2fe264c Mon Sep 17 00:00:00 2001 From: sean t Date: Sun, 24 May 2026 14:29:37 +0800 Subject: [PATCH 8/8] fix xml comment --- src/SIPSorcery/net/ICE/RtpIceChannel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIPSorcery/net/ICE/RtpIceChannel.cs b/src/SIPSorcery/net/ICE/RtpIceChannel.cs index 26d0c3f07f..9c1edd22e7 100644 --- a/src/SIPSorcery/net/ICE/RtpIceChannel.cs +++ b/src/SIPSorcery/net/ICE/RtpIceChannel.cs @@ -2523,7 +2523,7 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer) /// allocation messages share a 20-byte header where bytes 2-3 are the body length in /// big-endian; TURN channel-data messages instead use a 4-byte header in the /// 0x4000-0x7FFF channel-number range. We accumulate bytes until we have a full - /// message and then dispatch it via . + /// message and then dispatch it via OnRTPPacketReceived (inherited from RTPChannel). /// private async Task StartTlsReadLoop(STUNUri uri, SslStream sslStream, IPEndPoint remoteEndPoint) {