Skip to content

Commit 3a544cf

Browse files
authored
Feature merge (Headers, Data Allocations and TURNS/STUNS) (sipsorcery-org#1635)
* add offset and count to senddata to reduce allocations * Support multiple matching Uris in header extensions * target net10 * remove un-needed packages * Add TURNS and STUNS support Fix DNS bug with immediate timeout * Revert "target net10" This reverts commit 53368a0. * Revert "remove un-needed packages" This reverts commit e1f9e14. * fix xml comment
1 parent 38b3af9 commit 3a544cf

9 files changed

Lines changed: 230 additions & 42 deletions

File tree

src/SIPSorcery/net/ICE/IceServerResolver.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@ public void InitialiseIceServers(
6868
continue;
6969
}
7070

71-
// Filter out TLS or policy excluded entries
72-
if (stunUri.Scheme is STUNSchemesEnum.stuns or STUNSchemesEnum.turns ||
73-
(policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun))
71+
// Filter out policy excluded entries (TURNS/STUNS are now supported via TLS)
72+
if (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun)
7473
{
7574
logger.LogWarning("{caller} ignoring ICE server {stunUri} (scheme {scheme})", nameof(IceServerResolver), stunUri, stunUri.Scheme);
7675
continue;
@@ -117,7 +116,12 @@ private void ScheduleDnsLookup(STUNUri key, IceServer server)
117116

118117
if (_iceServers.ContainsKey(key))
119118
{
120-
_iceServers[key].DnsLookupSentAt = DateTime.UtcNow;
119+
// NOTE: must use DateTime.Now (not UtcNow) because RtpIceChannel.CheckIceServers
120+
// measures the elapsed time with DateTime.Now.Subtract(DnsLookupSentAt). Using
121+
// UtcNow here makes the timeout fire ~immediately in any non-UTC timezone (the
122+
// sign of the local offset determines whether the channel gives up before DNS
123+
// resolves or never times out at all).
124+
_iceServers[key].DnsLookupSentAt = DateTime.Now;
121125
}
122126

123127
logger.LogDebug("{caller} starting DNS lookup for ICE server {Uri}", nameof(IceServerResolver), key);

src/SIPSorcery/net/ICE/RtpIceChannel.cs

Lines changed: 160 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
using System.Collections.Generic;
7070
using System.Linq;
7171
using System.Net;
72+
using System.Net.Security;
7273
using System.Net.Sockets;
7374
using System.Runtime.CompilerServices;
7475
using System.Text;
@@ -439,6 +440,15 @@ public override void Close(string reason)
439440

440441
internal IceServerResolver _iceServerResolver = new IceServerResolver();
441442

443+
// Tracks one SslStream per TURNS/STUNS server URI so subsequent sends reuse the TLS session.
444+
private ConcurrentDictionary<STUNUri, SslStream> _tlsStreams = new ConcurrentDictionary<STUNUri, SslStream>();
445+
446+
// Per-URI write gate. SslStream.Write is NOT thread-safe and throws
447+
// "another write operation is pending" if two threads (e.g. ICE
448+
// connectivity checks + TURN permission refreshes + framed SCTP data)
449+
// race. We also use this to make the lazy handshake atomic.
450+
private ConcurrentDictionary<STUNUri, SemaphoreSlim> _tlsWriteLocks = new ConcurrentDictionary<STUNUri, SemaphoreSlim>();
451+
442452
private IceServer _activeIceServer;
443453

444454
public RTCIceComponent Component { get; private set; }
@@ -769,6 +779,13 @@ protected void StartTcpRtpReceiver()
769779
var stunUri = pair.Key;
770780
var tcpSocket = pair.Value;
771781

782+
// TURNS/STUNS use an SslStream owned by SendOverTCP/StartTlsReadLoop, so the
783+
// plain TCP receiver loop must not also try to read from the raw socket.
784+
if (stunUri != null && (stunUri.Scheme == STUNSchemesEnum.turns || stunUri.Scheme == STUNSchemesEnum.stuns))
785+
{
786+
continue;
787+
}
788+
772789
if (stunUri != null && !m_rtpTcpReceiverByUri.ContainsKey(stunUri) && tcpSocket != null)
773790
{
774791
var rtpTcpReceiver = new IceTcpReceiver(tcpSocket);
@@ -1065,7 +1082,7 @@ private void RefreshTurn(Object state)
10651082
{
10661083
return;
10671084
}
1068-
if (_activeIceServer._uri.Scheme != STUNSchemesEnum.turn || NominatedEntry.LocalCandidate.IceServer is null)
1085+
if ((_activeIceServer._uri.Scheme != STUNSchemesEnum.turn && _activeIceServer._uri.Scheme != STUNSchemesEnum.turns) || NominatedEntry.LocalCandidate.IceServer is null)
10691086
{
10701087
_refreshTurnTimer?.Dispose();
10711088
return;
@@ -1155,8 +1172,8 @@ private void CheckIceServers(Object state)
11551172
logger.LogDebug("RTP ICE Channel was not able to acquire an active ICE server, stopping ICE servers timer.");
11561173
_processIceServersTimer.Dispose();
11571174
}
1158-
else if ((_activeIceServer._uri.Scheme == STUNSchemesEnum.turn && _activeIceServer.RelayEndPoint != null) ||
1159-
(_activeIceServer._uri.Scheme == STUNSchemesEnum.stun && _activeIceServer.ServerReflexiveEndPoint != null))
1175+
else if (((_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns) && _activeIceServer.RelayEndPoint != null) ||
1176+
((_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns) && _activeIceServer.ServerReflexiveEndPoint != null))
11601177
{
11611178
// Successfully set up the ICE server. Do nothing.
11621179
}
@@ -1185,13 +1202,13 @@ private void CheckIceServers(Object state)
11851202
_activeIceServer.Error = SocketError.TimedOut;
11861203
}
11871204
// Send STUN binding request.
1188-
else if (_activeIceServer.ServerReflexiveEndPoint == null && _activeIceServer._uri.Scheme == STUNSchemesEnum.stun)
1205+
else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.stun || _activeIceServer._uri.Scheme == STUNSchemesEnum.stuns))
11891206
{
11901207
logger.LogDebug("Sending STUN binding request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint);
11911208
_activeIceServer.Error = SendStunBindingRequest(_activeIceServer);
11921209
}
11931210
// Send TURN binding request.
1194-
else if (_activeIceServer.ServerReflexiveEndPoint == null && _activeIceServer._uri.Scheme == STUNSchemesEnum.turn)
1211+
else if (_activeIceServer.ServerReflexiveEndPoint == null && (_activeIceServer._uri.Scheme == STUNSchemesEnum.turn || _activeIceServer._uri.Scheme == STUNSchemesEnum.turns))
11951212
{
11961213
logger.LogDebug("Sending TURN allocate request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint);
11971214
_activeIceServer.Error = SendTurnAllocateRequest(_activeIceServer);
@@ -2411,27 +2428,79 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer)
24112428
return e1.Port == e2.Port && e1.Address.Equals(e2.Address);
24122429
};
24132430

2414-
if (!sendSocket.Connected || !(sendSocket.RemoteEndPoint is IPEndPoint) || !equals(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint))
2431+
bool isTls = iceServer._uri.Scheme == STUNSchemesEnum.turns || iceServer._uri.Scheme == STUNSchemesEnum.stuns;
2432+
if (isTls)
24152433
{
2416-
if (sendSocket.Connected)
2434+
// --- TLS PATH (TURNS / STUNS) ---
2435+
// SslStream.Write is not thread-safe, and the lazy handshake/connect
2436+
// must also be atomic so two threads don't both create a stream.
2437+
var writeLock = _tlsWriteLocks.GetOrAdd(iceServer._uri, _ => new SemaphoreSlim(1, 1));
2438+
writeLock.Wait();
2439+
try
24172440
{
2418-
logger.LogDebug("SendOverTCP request disconnect.");
2419-
sendSocket.Disconnect(true);
2420-
}
2421-
sendSocket.Connect(dstEndPoint);
2441+
if (!_tlsStreams.TryGetValue(iceServer._uri, out SslStream sslStream))
2442+
{
2443+
// Connect the raw socket if needed
2444+
if (!sendSocket.Connected)
2445+
{
2446+
sendSocket.Connect(dstEndPoint);
2447+
}
24222448

2423-
logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint);
2424-
}
2449+
// Wrap in SslStream. Validation is left permissive here; tighten if needed.
2450+
sslStream = new SslStream(new NetworkStream(sendSocket, false), false,
2451+
(sender, cert, chain, errors) => true, null);
24252452

2426-
//Fix ReceiveFrom logic if any previous exception happens
2427-
m_rtpTcpReceiverByUri.TryGetValue(iceServer?._uri, out IceTcpReceiver rtpTcpReceiver);
2428-
if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed)
2429-
{
2430-
rtpTcpReceiver.BeginReceiveFrom();
2453+
try
2454+
{
2455+
// Perform TLS handshake using the hostname from the URI for SNI/cert match.
2456+
sslStream.AuthenticateAsClient(iceServer._uri.Host);
2457+
2458+
_tlsStreams.TryAdd(iceServer._uri, sslStream);
2459+
logger.LogDebug("TLS handshake successful for {Uri}", iceServer._uri);
2460+
2461+
// Start a dedicated read loop for this SSL stream
2462+
_ = StartTlsReadLoop(iceServer._uri, sslStream, dstEndPoint);
2463+
}
2464+
catch (Exception tlsEx)
2465+
{
2466+
logger.LogError(tlsEx, "TLS handshake failed for {Uri}: {Message}", iceServer._uri, tlsEx.Message);
2467+
return SocketError.SocketError;
2468+
}
2469+
}
2470+
2471+
// Write to the SSL stream (serialised by writeLock).
2472+
sslStream.Write(buffer);
2473+
return SocketError.Success;
2474+
}
2475+
finally
2476+
{
2477+
writeLock.Release();
2478+
}
24312479
}
2480+
else
2481+
{
2482+
if (!sendSocket.Connected || !(sendSocket.RemoteEndPoint is IPEndPoint) || !equals(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint))
2483+
{
2484+
if (sendSocket.Connected)
2485+
{
2486+
logger.LogDebug("SendOverTCP request disconnect.");
2487+
sendSocket.Disconnect(true);
2488+
}
2489+
sendSocket.Connect(dstEndPoint);
24322490

2433-
sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket);
2434-
return SocketError.Success;
2491+
logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint);
2492+
}
2493+
2494+
//Fix ReceiveFrom logic if any previous exception happens
2495+
m_rtpTcpReceiverByUri.TryGetValue(iceServer?._uri, out IceTcpReceiver rtpTcpReceiver);
2496+
if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed)
2497+
{
2498+
rtpTcpReceiver.BeginReceiveFrom();
2499+
}
2500+
2501+
sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket);
2502+
return SocketError.Success;
2503+
}
24352504
}
24362505
catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored.
24372506
{
@@ -2449,6 +2518,77 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer)
24492518
}
24502519
}
24512520

2521+
/// <summary>
2522+
/// Reads framed STUN / TURN traffic off a TLS stream (TURNS / STUNS). STUN and TURN
2523+
/// allocation messages share a 20-byte header where bytes 2-3 are the body length in
2524+
/// big-endian; TURN channel-data messages instead use a 4-byte header in the
2525+
/// 0x4000-0x7FFF channel-number range. We accumulate bytes until we have a full
2526+
/// message and then dispatch it via OnRTPPacketReceived (inherited from RTPChannel).
2527+
/// </summary>
2528+
private async Task StartTlsReadLoop(STUNUri uri, SslStream sslStream, IPEndPoint remoteEndPoint)
2529+
{
2530+
byte[] receiveBuffer = new byte[4096];
2531+
List<byte> streamBuffer = new List<byte>();
2532+
2533+
logger.LogDebug("Starting TLS read loop for {Uri}", uri);
2534+
2535+
try
2536+
{
2537+
while (!IsClosed && sslStream.CanRead)
2538+
{
2539+
int bytesRead = await sslStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length).ConfigureAwait(false);
2540+
2541+
if (bytesRead == 0)
2542+
{
2543+
logger.LogWarning("TLS stream closed remotely for {Uri}", uri);
2544+
break;
2545+
}
2546+
2547+
streamBuffer.AddRange(new ArraySegment<byte>(receiveBuffer, 0, bytesRead));
2548+
2549+
// Process complete packets from the stream buffer
2550+
while (streamBuffer.Count >= 4) // Minimum header size
2551+
{
2552+
int bodyLength = (streamBuffer[2] << 8) | streamBuffer[3];
2553+
int totalPacketLength = 20 + bodyLength; // STUN/TURN header (20) + body
2554+
2555+
// TURN Channel Data has a 4-byte header; channel range is 0x4000 -> 0x7FFF
2556+
if (streamBuffer[0] >= 0x40 && streamBuffer[0] <= 0x7F)
2557+
{
2558+
totalPacketLength = 4 + bodyLength;
2559+
}
2560+
2561+
if (streamBuffer.Count >= totalPacketLength)
2562+
{
2563+
byte[] packetBytes = streamBuffer.GetRange(0, totalPacketLength).ToArray();
2564+
streamBuffer.RemoveRange(0, totalPacketLength);
2565+
OnRTPPacketReceived(null, 0, remoteEndPoint, packetBytes);
2566+
}
2567+
else
2568+
{
2569+
// Not enough data yet, wait for the next ReadAsync.
2570+
break;
2571+
}
2572+
}
2573+
}
2574+
}
2575+
catch (Exception ex)
2576+
{
2577+
if (!IsClosed)
2578+
{
2579+
logger.LogError(ex, "TLS read loop exception for {Uri}: {Message}", uri, ex.Message);
2580+
}
2581+
}
2582+
finally
2583+
{
2584+
_tlsStreams.TryRemove(uri, out _);
2585+
if (_tlsWriteLocks.TryRemove(uri, out var removedLock))
2586+
{
2587+
removedLock.Dispose();
2588+
}
2589+
}
2590+
}
2591+
24522592
protected virtual void EndSendToTCP(IAsyncResult ar)
24532593
{
24542594
try

src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,22 @@ public RTPHeaderExtension(int id, string uri, int extensionSize, RTPHeaderExtens
6969
}
7070
}
7171

72+
/// <summary>
73+
/// Checks if the URI provided matches the URI of this extension
74+
/// Override this method if the extension can have multiple URIs (for example TransportWideCCExtension)
75+
/// </summary>
76+
/// <param name="uri"></param>
77+
/// <returns></returns>
78+
public virtual bool MatchesExtension(string uri)
79+
{
80+
return Uri.Equals(uri, StringComparison.InvariantCultureIgnoreCase);
81+
}
82+
7283
// Id / "extmap"
7384
public int Id { get; internal set; }
7485

7586
// Uri
76-
public string Uri { get; }
87+
public string Uri { get; set; }
7788

7889
public int ExtensionSize { get; }
7990

src/SIPSorcery/net/RTP/RTPHeaderExtensions/TransportWideCCExtension.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,28 @@ public class TransportWideCCExtension : RTPHeaderExtension
3333
//
3434

3535
public const string RTP_HEADER_EXTENSION_URI = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01";
36-
//public const string RTP_HEADER_EXTENSION_URI_ALT = "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02";
36+
37+
38+
public override bool MatchesExtension(string uri)
39+
{
40+
switch (uri.ToLower())
41+
{
42+
case RTP_HEADER_EXTENSION_URI:
43+
case "urn:ietf:params:rtp-hdrext:transport-wide-cc": //official urn registered with IANA
44+
case "http://www.webrtc.org/experiments/rtp-hdrext/transport-wide-cc-02":
45+
return true;
46+
}
47+
return false;
48+
}
3749

3850

3951
internal const int RTP_HEADER_EXTENSION_SIZE = 2; // TWCC payload: 2 bytes for sequence number.
4052

53+
public TransportWideCCExtension(int id, string uri)
54+
: base(id, uri, RTP_HEADER_EXTENSION_SIZE, RTPHeaderExtensionType.OneByte)
55+
{
56+
}
57+
4158
/// <summary>
4259
/// The TWCC sequence number.
4360
/// </summary>

src/SIPSorcery/net/SCTP/SctpAssociation.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,9 @@ public void SendData(ushort streamID, uint ppid, string message)
589589
/// <param name="streamID">The stream ID to sent the data on.</param>
590590
/// <param name="ppid">The payload protocol ID for the data.</param>
591591
/// <param name="data">The byte data to send.</param>
592-
public void SendData(ushort streamID, uint ppid, byte[] data)
592+
/// <param name="offset">The offset in <paramref name="data"/> at which to begin sending. Defaults to 0.</param>
593+
/// <param name="count">The number of bytes to send. Defaults to -1, meaning all bytes from <paramref name="offset"/> to the end of the array.</param>
594+
public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1)
593595
{
594596
if (_wasAborted)
595597
{
@@ -603,7 +605,7 @@ public void SendData(ushort streamID, uint ppid, byte[] data)
603605
}
604606
else
605607
{
606-
_dataSender.SendData(streamID, ppid, data);
608+
_dataSender.SendData(streamID, ppid, data, offset, count);
607609
}
608610
}
609611

0 commit comments

Comments
 (0)