Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/SIPSorcery/net/ICE/IceServerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
180 changes: 160 additions & 20 deletions src/SIPSorcery/net/ICE/RtpIceChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<STUNUri, SslStream> _tlsStreams = new ConcurrentDictionary<STUNUri, SslStream>();

// 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<STUNUri, SemaphoreSlim> _tlsWriteLocks = new ConcurrentDictionary<STUNUri, SemaphoreSlim>();

private IceServer _activeIceServer;

public RTCIceComponent Component { get; private set; }
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
{
Expand All @@ -2449,6 +2518,77 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer)
}
}

/// <summary>
/// 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 OnRTPPacketReceived (inherited from RTPChannel).
/// </summary>
private async Task StartTlsReadLoop(STUNUri uri, SslStream sslStream, IPEndPoint remoteEndPoint)
{
byte[] receiveBuffer = new byte[4096];
List<byte> streamBuffer = new List<byte>();

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<byte>(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
Expand Down
13 changes: 12 additions & 1 deletion src/SIPSorcery/net/RTP/RTPHeaderExtensions/RTPHeaderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,22 @@ public RTPHeaderExtension(int id, string uri, int extensionSize, RTPHeaderExtens
}
}

/// <summary>
/// Checks if the URI provided matches the URI of this extension
/// Override this method if the extension can have multiple URIs (for example TransportWideCCExtension)
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
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; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +38 to +48
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimized in #1639



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)
{
}

/// <summary>
/// The TWCC sequence number.
/// </summary>
Expand Down
6 changes: 4 additions & 2 deletions src/SIPSorcery/net/SCTP/SctpAssociation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,9 @@ public void SendData(ushort streamID, uint ppid, string message)
/// <param name="streamID">The stream ID to sent the data on.</param>
/// <param name="ppid">The payload protocol ID for the data.</param>
/// <param name="data">The byte data to send.</param>
public void SendData(ushort streamID, uint ppid, byte[] data)
/// <param name="offset">The offset in <paramref name="data"/> at which to begin sending. Defaults to 0.</param>
/// <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>
public void SendData(ushort streamID, uint ppid, byte[] data, int offset = 0, int count = -1)
{
if (_wasAborted)
{
Expand All @@ -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);
}
}

Expand Down
Loading
Loading