diff --git a/src/net/ICE/IceChecklistEntry.cs b/src/net/ICE/IceChecklistEntry.cs
index 40cdff545..0c6fc954c 100644
--- a/src/net/ICE/IceChecklistEntry.cs
+++ b/src/net/ICE/IceChecklistEntry.cs
@@ -17,6 +17,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;
using SIPSorcery.Sys;
@@ -171,6 +172,8 @@ public ulong Priority
///
public DateTime LastCheckSentAt = DateTime.MinValue;
+ internal DateTime TcpLastCheckSentAt = DateTime.MinValue;
+
///
/// The number of checks that have been sent without a response.
///
@@ -212,11 +215,21 @@ public string RequestTransactionID
///
public int TurnPermissionsRequestSent { get; set; } = 0;
+ internal int TcpBindRequestSent { get; set; } = 0;
+
///
/// This field records the time a Create Permissions response was received.
///
public DateTime TurnPermissionsResponseAt { get; set; } = DateTime.MinValue;
+ public DateTime TurnConnectReportAt { get; internal set; } = DateTime.MinValue;
+
+ public DateTime TurnConnectBindedAt { get; internal set; } = DateTime.MinValue;
+
+ public int TurnConnectRequestSent { get; internal set; }
+
+ public uint TurnConnectionId { get; internal set; }
+
///
/// If a candidate has been nominated this field records the time the last
/// STUN binding response was received from the remote peer.
@@ -229,7 +242,7 @@ public string RequestTransactionID
/// Timestamp for the most recent binding request received from the remote peer.
///
public DateTime LastBindingRequestReceivedAt { get; set; }
-
+
///
/// Creates a new entry for the ICE session checklist.
///
@@ -286,7 +299,10 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
stunResponse.Attributes.First(x => x.AttributeType == STUNAttributeTypesEnum.ErrorCode) as
STUNErrorCodeAttribute;
if (errCodeAttribute.ErrorCode == IceServer.STUN_UNAUTHORISED_ERROR_CODE ||
- errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE)
+ errCodeAttribute.ErrorCode == IceServer.STUN_STALE_NONCE_ERROR_CODE
+ // TODO: shouldn't be here, right...
+ // || errCodeAttribute.ErrorCode == IceServer.STUN_CONNECTION_ALREADY_EXISTS
+ )
{
if (LocalCandidate.IceServer == null)
{
@@ -299,6 +315,11 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
retry = true;
}
}
+ else if (errCodeAttribute.ErrorCode == IceServer.STUN_CONNECTION_TIMEOUT_OR_FAILURE)
+ {
+ TurnConnectReportAt = DateTime.Now;
+ retry = true;
+ }
}
}
@@ -359,6 +380,43 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
TurnPermissionsResponseAt = DateTime.Now;
State = retry ? State : ChecklistEntryState.Failed;
}
+ else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.ConnectSuccess)
+ {
+ logger.LogDebug("A TURN Connect sucess response was received from ICE server {IceServer} (TxID: {TransactionId}).",
+ LocalCandidate.IceServer._uri, Encoding.ASCII.GetString(stunResponse.Header.TransactionId));
+
+ TurnConnectionId = stunResponse.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.ConnectionId)
+ is STUNConnectionIdAttribute connectionIdAttribute
+ ? connectionIdAttribute.ConnectionId
+ : 0;
+
+ TurnConnectReportAt = DateTime.Now;
+ TurnConnectRequestSent = 0;
+
+ //After connect request, need ConnectBind
+ if (State == ChecklistEntryState.InProgress)
+ {
+ State = ChecklistEntryState.Waiting;
+ //Clear CheckSentAt Time to force send it again
+ FirstCheckSentAt = DateTime.MinValue;
+ }
+ }
+ else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.ConnectionBindSuccess)
+ {
+ logger.LogDebug("A TURN ConnectionBind sucess response was received from ICE server {IceServer} (TxID: {TransactionId}).",
+ LocalCandidate.IceServer._uri, Encoding.ASCII.GetString(stunResponse.Header.TransactionId));
+
+ TurnConnectBindedAt = TurnConnectReportAt = DateTime.Now;
+ TurnConnectRequestSent = 0;
+
+ //After TCP ConnectBind, we need underlying STUN bind.
+ if (State == ChecklistEntryState.InProgress)
+ {
+ State = ChecklistEntryState.Waiting;
+ //Clear CheckSentAt Time to force send it again
+ FirstCheckSentAt = DateTime.MinValue;
+ }
+ }
else
{
logger.LogWarning("ICE RTP channel received an unexpected STUN response {MessageType} from {RemoteEndPoint}.", stunResponse.Header.MessageType, remoteEndPoint);
diff --git a/src/net/ICE/IceServer.cs b/src/net/ICE/IceServer.cs
index ab2867545..c74139c94 100644
--- a/src/net/ICE/IceServer.cs
+++ b/src/net/ICE/IceServer.cs
@@ -94,6 +94,15 @@ public class IceServer
///
internal const int STUN_STALE_NONCE_ERROR_CODE = 438;
+ internal const int STUN_CONNECTION_ALREADY_EXISTS = 446;
+
+ internal const int STUN_CONNECTION_TIMEOUT_OR_FAILURE = 447;
+
+ // 10 seconds is from https://datatracker.ietf.org/doc/html/rfc6062#section-4.3
+ internal static TimeSpan waittime => TimeSpan.FromSeconds(10);
+ internal static TimeSpan rttime => TimeSpan.FromSeconds(10 / (MAX_ERRORS));
+ //internal static readonly TimeSpan rttime = TimeSpan.FromSeconds(1);
+
internal STUNUri _uri;
internal string _username;
internal string _password;
@@ -177,7 +186,26 @@ public class IceServer
///
internal int ErrorResponseCount = 0;
+ ///
+ /// Transport protocol for connecting with the server.
+ ///
public ProtocolType Protocol { get { return _uri.Protocol; } }
+ // DevNote
+ //
+ /// Not to be confused of the protocol for allocated ICE relay candidate with TURN ().
+ //
+
+ ///
+ /// Protocol of the ICE relay candidate to be allocated.
+ ///
+ ///
+ /// Only affects TURN server usage.
+ /// Defaults to
+ ///
+ internal ProtocolType _reqIceProtocol { get; set; } = ProtocolType.Udp;
+ //public ProtocolType IceRelayProtocol { get; set; } = ProtocolType.Udp;
+
+ internal STUNUri _secondaryRelayUri;
///
/// Default constructor.
@@ -211,9 +239,8 @@ internal RTCIceCandidate GetCandidate(RTCIceCandidateInit init, RTCIceCandidateT
if (type == RTCIceCandidateType.srflx && ServerReflexiveEndPoint != null)
{
- // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport.
- //var srflxProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp;
- var srflxProtocol = RTCIceProtocol.udp;
+ var srflxProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp;
+
candidate.SetAddressProperties(srflxProtocol, ServerReflexiveEndPoint.Address, (ushort)ServerReflexiveEndPoint.Port,
type, null, 0);
candidate.IceServer = this;
@@ -222,9 +249,7 @@ internal RTCIceCandidate GetCandidate(RTCIceCandidateInit init, RTCIceCandidateT
}
else if (type == RTCIceCandidateType.relay && RelayEndPoint != null)
{
- // TODO: Currently implementation always use UDP candidates as we will only support TURN TCP Transport.
- //var relayProtocol = _uri.Protocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp;
- var relayProtocol = RTCIceProtocol.udp;
+ var relayProtocol = _reqIceProtocol == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp;
candidate.SetAddressProperties(relayProtocol, RelayEndPoint.Address, (ushort)RelayEndPoint.Port,
type, null, 0);
diff --git a/src/net/ICE/RTCIceCandidate.cs b/src/net/ICE/RTCIceCandidate.cs
index bfba165c0..9f1fd7985 100644
--- a/src/net/ICE/RTCIceCandidate.cs
+++ b/src/net/ICE/RTCIceCandidate.cs
@@ -31,6 +31,8 @@ public class RTCIceCandidate : IRTCIceCandidate
public const string REMOTE_PORT_KEY = "rport";
public const string CANDIDATE_PREFIX = "candidate";
+ public const ushort TCP_DISCARD_PORT = 9;
+
///
/// The ICE server (STUN or TURN) the candidate was generated from.
/// Will be null for non-ICE server candidates.
@@ -127,6 +129,7 @@ public RTCIceCandidate(RTCIceCandidateInit init)
component = iceCandidate.component;
address = iceCandidate.address;
port = iceCandidate.port;
+ protocol = iceCandidate.protocol;
type = iceCandidate.type;
tcpType = iceCandidate.tcpType;
relatedAddress = iceCandidate.relatedAddress;
diff --git a/src/net/ICE/RtpIceChannel.cs b/src/net/ICE/RtpIceChannel.cs
index d53cd7f3a..eb16caa20 100755
--- a/src/net/ICE/RtpIceChannel.cs
+++ b/src/net/ICE/RtpIceChannel.cs
@@ -67,15 +67,19 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
+using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices.ComTypes;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Bcpg;
using Org.BouncyCastle.Crypto.Digests;
using SIPSorcery.Sys;
@@ -593,6 +597,13 @@ internal int RTO
///
public Func> MdnsGetAddresses;
+ ///
+ /// STUN/TURN server connections socket by URI
+ ///
+ /// Also see .
+ /// Internal note:
+ /// (*) Used as control when is .
+ /// In such case the actual relay is where from
public Dictionary RtpTcpSocketByUri { get; private set; } = new Dictionary();
protected Dictionary m_rtpTcpReceiverByUri = new Dictionary();
@@ -604,7 +615,7 @@ internal int RTO
/// with ICE connectivity checks.
///
public RtpIceChannel() :
- this(null, RTCIceComponent.rtp)
+ this(null, RTCIceComponent.rtp, useTcp: false)
{ }
///
@@ -618,6 +629,7 @@ public RtpIceChannel() :
/// for cases where RTP and RTCP are multiplexed the component is set to RTP.
/// A list of STUN or TURN servers that can be used by this ICE agent.
/// Determines which ICE candidates can be used in this RTP ICE Channel.
+ /// If set to true then the RTP channel will use TCP instead of UDP.
/// If set to true then IP addresses from ALL local
/// interfaces will be used for host ICE candidates. If left as the default false value host
/// candidates will be restricted to the single interface that the OS routing table matches to
@@ -632,8 +644,10 @@ public RtpIceChannel(
RTCIceTransportPolicy policy = RTCIceTransportPolicy.all,
bool includeAllInterfaceAddresses = false,
int bindPort = 0,
- PortRange rtpPortRange = null) :
- base(false, bindAddress, bindPort, rtpPortRange)
+ PortRange rtpPortRange = null,
+ bool useTcp = false
+ )
+ : base(false, bindAddress, bindPort, rtpPortRange, useTcp)
{
_bindAddress = bindAddress;
Component = component;
@@ -653,16 +667,15 @@ public RtpIceChannel(
});
_localChecklistCandidate.SetAddressProperties(
- RTCIceProtocol.udp,
+ useTcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp,
base.RTPLocalEndPoint.Address,
+ //useTcp ? RTCIceCandidate.TCP_DISCARD_PORT :
(ushort)base.RTPLocalEndPoint.Port,
RTCIceCandidateType.host,
null,
0);
- // Create TCP Socket to implement TURN Control
- // Take a note that TURN Control will only use TCP for CreatePermissions/Allocate/BindRequests/Data
- // Ice Candidates returned by relay will always be UDP based.
+ // Create Socket for TURN Control over TCP
var tcpIceServers = _iceServers != null ?
_iceServers.FindAll(a =>
a != null &&
@@ -728,8 +741,12 @@ public void StartGathering()
_startedGatheringAt = DateTime.Now;
- // Start listening on the UDP socket.
- base.Start();
+ // TODO: Remove when TCP is implemented for direct RTP
+ if (RtpSocket.ProtocolType == ProtocolType.Udp)
+ {
+ // Start listening on the UDP socket.
+ base.Start();
+ }
StartTcpRtpReceiver();
IceGatheringState = RTCIceGatheringState.gathering;
@@ -777,15 +794,7 @@ protected void StartTcpRtpReceiver()
if (stunUri != null && !m_rtpTcpReceiverByUri.ContainsKey(stunUri) && tcpSocket != null)
{
- var rtpTcpReceiver = new IceTcpReceiver(tcpSocket);
-
- Action onClose = (reason) =>
- {
- CloseTcp(rtpTcpReceiver, reason);
- };
- rtpTcpReceiver.OnPacketReceived += OnRTPPacketReceived;
- rtpTcpReceiver.OnClosed += onClose;
- rtpTcpReceiver.BeginReceiveFrom();
+ var rtpTcpReceiver = GetIceTcpReceiver(tcpSocket);
m_rtpTcpReceiverByUri.Add(stunUri, rtpTcpReceiver);
}
@@ -798,6 +807,27 @@ protected void StartTcpRtpReceiver()
}
}
+ private IceTcpReceiver_ GetIceTcpReceiver(Socket tcpSocket, bool isrelay = false)
+ {
+ var rtpTcpReceiver = new IceTcpReceiver_(tcpSocket);
+
+ rtpTcpReceiver.OnPacketReceived +=
+ (_, localPort, remoteEndPoint, packet) =>
+ {
+ if (logger.IsEnabled(LogLevel.Trace))
+ {
+ logger.LogTrace("Received {bits} bytes at {lcl}",
+ packet.Length, tcpSocket.LocalEndPoint);
+ }
+
+ OnPacketReceived(localPort, remoteEndPoint, packet, isrelay);
+ };
+
+ rtpTcpReceiver.OnClosed += (reason) => CloseTcp(rtpTcpReceiver, reason);
+ rtpTcpReceiver.BeginReceiveFrom();
+ return rtpTcpReceiver;
+ }
+
protected void CloseTcp(string reason)
{
if (m_rtpTcpReceiverByUri != null)
@@ -894,9 +924,17 @@ public void AddRemoteCandidate(RTCIceCandidate candidate)
// This implementation currently only supports audio and video multiplexed on a single channel.
OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate only supports multiplexed media, excluding remote candidate with non-zero sdpMLineIndex of {candidate.sdpMLineIndex}.");
}
- else if (candidate.protocol != RTCIceProtocol.udp)
+ // This should be invalid conditional since even failed parsing of protocol at SDP will result in 0 = RTCIceProtocol.udp
+ // TODO: remove /or/ catch unsupported protocol at SDP/candidate signalling parsing /or/ start enum value at RTCIceProtocol.udp = 1
+ else if (candidate.protocol switch
+ {
+ RTCIceProtocol.udp => false,
+ RTCIceProtocol.tcp => false,
+ _ => true
+ }
+ )
{
- // This implementation currently only supports UDP for RTP communications.
+ // Unimplemented special cases Protocol is used. (TCP/UDP should be OK)
OnIceCandidateError?.Invoke(candidate, $"Remote ICE candidate has an unsupported transport protocol {candidate.protocol}.");
}
else if (IPAddress.TryParse(candidate.address, out var addr) &&
@@ -1044,11 +1082,16 @@ private List GetHostCandidates()
foreach (var localAddress in localAddresses)
{
var hostCandidate = new RTCIceCandidate(init);
- hostCandidate.SetAddressProperties(RTCIceProtocol.udp, localAddress, (ushort)base.RTPPort, RTCIceCandidateType.host, null, 0);
+
+ var iceProto = RtpSocket.ProtocolType == ProtocolType.Tcp ? RTCIceProtocol.tcp : RTCIceProtocol.udp;
+
+ hostCandidate.SetAddressProperties(iceProto, localAddress, (ushort)base.RTPPort, RTCIceCandidateType.host, null, 0);
// We currently only support a single multiplexed connection for all data streams and RTCP.
if (hostCandidate.component == RTCIceComponent.rtp && hostCandidate.sdpMLineIndex == SDP_MLINE_INDEX)
{
+ logger.LogTrace("Gathered icecandidate: {candidate}", hostCandidate.ToShortString());
+
hostCandidates.Add(hostCandidate);
OnIceCandidate?.Invoke(hostCandidate);
@@ -1104,6 +1147,12 @@ private void InitialiseIceServers(List iceServers)
logger.LogDebug("ICE server end point for {Uri} set to {EndPoint}.", iceServerState._uri, iceServerState.ServerEndPoint);
}
+ if (stunUri.Scheme == STUNSchemesEnum.turn && iceServer.X_ICERelayProtocol == RTCIceProtocol.tcp)
+ {
+ iceServerState._reqIceProtocol = ProtocolType.Tcp;
+ logger.LogDebug("Will request TCP relay candidate from ICE server {Uri}", iceServerState._uri);
+ }
+
_iceServerConnections.TryAdd(stunUri, iceServerState);
iceServerID++;
@@ -1196,6 +1245,7 @@ private void CheckIceServers(Object state)
{
logger.LogDebug("RTP ICE Channel all ICE server connection checks failed, stopping ICE servers timer.");
_processIceServersTimer.Dispose();
+ return;
}
else
{
@@ -1287,7 +1337,15 @@ private void CheckIceServers(Object state)
// Send TURN binding request.
else if (_activeIceServer.ServerReflexiveEndPoint == null && _activeIceServer._uri.Scheme == STUNSchemesEnum.turn)
{
+ if (_activeIceServer.Protocol == ProtocolType.Tcp
+ && DateTime.Now - _activeIceServer.LastRequestSentAt < IceServer.rttime
+ && _activeIceServer.LastResponseReceivedAt < _activeIceServer.LastRequestSentAt)
+ {
+ return;
+ }
+
logger.LogDebug("Sending TURN allocate request to ICE server {Uri} with address {EndPoint}.", _activeIceServer._uri, _activeIceServer.ServerEndPoint);
+
_activeIceServer.Error = SendTurnAllocateRequest(_activeIceServer);
}
else
@@ -1343,7 +1401,7 @@ private async Task AddCandidatesForIceServer(IceServer iceServer)
if (relayCandidate != null)
{
- logger.LogDebug("Adding relay ICE candidate for ICE server {Uri} and {EndPoint}.", iceServer._uri, iceServer.RelayEndPoint);
+ logger.LogDebug("Adding relay ICE candidate for ICE server {Uri} on {RelayProtocol}:{EndPoint}.", iceServer._uri, iceServer._reqIceProtocol, iceServer.RelayEndPoint);
_candidates.Add(relayCandidate);
OnIceCandidate?.Invoke(relayCandidate);
@@ -1559,6 +1617,11 @@ private void DoConnectivityCheck(Object stateInfo)
case RTCIceConnectionState.connected:
case RTCIceConnectionState.disconnected:
// Periodic checks on the nominated peer.
+ if (logger.IsEnabled(LogLevel.Trace))
+ {
+ logger.LogTrace("Sending periodic connectivity check for {lcl}->{rmt}",
+ NominatedEntry.LocalCandidate.ToShortString(), NominatedEntry.RemoteCandidate.ToShortString());
+ }
SendCheckOnConnectedPair(NominatedEntry);
break;
@@ -1576,6 +1639,8 @@ private void DoConnectivityCheck(Object stateInfo)
///
/// The scheduling mechanism for ICE is specified in https://tools.ietf.org/html/rfc8445#section-6.1.4.
///
+ // TODO: Might want to check all candidate pairs concurrently and use the one that is ready first,
+ // it will reduce the TTL, we already use async after all.
private async void ProcessChecklist()
{
if (!_closed && (IceConnectionState == RTCIceConnectionState.@new ||
@@ -1770,6 +1835,15 @@ private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCand
return;
}
+ if (candidatePair.LocalCandidate.protocol != candidatePair.RemoteCandidate.protocol)
+ {
+ logger.LogWarning("ICE local and remote candidate protocols do not match, state being set to failed: {LocalProtocol}->{RemoteProtocol}.",
+ candidatePair.LocalCandidate.protocol, candidatePair.RemoteCandidate.protocol);
+
+ candidatePair.State = ChecklistEntryState.Failed;
+ return;
+ }
+
if (candidatePair.FirstCheckSentAt == DateTime.MinValue)
{
candidatePair.FirstCheckSentAt = DateTime.Now;
@@ -1781,13 +1855,26 @@ private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCand
candidatePair.RequestTransactionID = Crypto.GetRandomString(STUNHeader.TRANSACTION_ID_LENGTH);
bool isRelayCheck = candidatePair.LocalCandidate.type == RTCIceCandidateType.relay;
- //bool isTcpProtocol = candidatePair.LocalCandidate.IceServer?.Protocol == ProtocolType.Tcp;
+ bool isProtoTcp = candidatePair.LocalCandidate.protocol == RTCIceProtocol.tcp;
- if (isRelayCheck && candidatePair.TurnPermissionsResponseAt == DateTime.MinValue)
+ if (isProtoTcp)
{
- if (candidatePair.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS)
+ // The connectionful of TCP have retransmission rate on its own,
+ // so we may need to wait more than one unit of timer cycle
+ if (DateTime.Now - candidatePair.TcpLastCheckSentAt >= IceServer.rttime ||
+ candidatePair.TurnPermissionsResponseAt >= candidatePair.TcpLastCheckSentAt
+ || candidatePair.TurnConnectReportAt >= candidatePair.TcpLastCheckSentAt
+ )
{
- logger.LogWarning("ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.", candidatePair.LocalCandidate.IceServer._uri, candidatePair.TurnPermissionsRequestSent);
+ candidatePair.TcpLastCheckSentAt = DateTime.Now;
+
+ // allow peer to send/connect to relay first
+ if (isRelayCheck && candidatePair.TurnPermissionsResponseAt == DateTime.MinValue)
+ {
+ if (candidatePair.TurnPermissionsRequestSent >= IceServer.MAX_ERRORS)
+ {
+ logger.LogWarning("ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.",
+ candidatePair.LocalCandidate.IceServer._uri, candidatePair.TurnPermissionsRequestSent);
candidatePair.State = ChecklistEntryState.Failed;
}
else
@@ -1795,25 +1882,226 @@ private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCand
// Send Create Permissions request to TURN server for remote candidate.
candidatePair.TurnPermissionsRequestSent++;
- logger.LogDebug("ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).", candidatePair.TurnPermissionsRequestSent, candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID);
+ logger.LogDebug("ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).",
+ candidatePair.TurnPermissionsRequestSent, candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID);
SendTurnCreatePermissionsRequest(candidatePair.RequestTransactionID, candidatePair.LocalCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint);
}
}
+ // https://datatracker.ietf.org/doc/html/rfc6062#section-4.3
+ else if (isRelayCheck && candidatePair.TurnConnectBindedAt == DateTime.MinValue)
+ {
+ // we either got ConnectAttempt Indication or we are initiator got connectionid,
+ // we should send a ConnectionBind request to the TURN server
+ if (candidatePair.TurnConnectionId != 0)
+ {
+ if (candidatePair.TurnConnectRequestSent >= IceServer.MAX_ERRORS)
+ {
+ logger.LogDebug("ICE RTP channel failed to request ConnectionBind to {RemoteEndPoint} after {TurnRequestSent} attempts.",
+ candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.TurnConnectRequestSent);
+
+ candidatePair.State = ChecklistEntryState.Failed;
+ }
+ else
+ {
+ logger.LogDebug("ICE RTP channel sending ConnectionBind request to server {IceServerUri} for peer {RemoteEndPoint} (TxID: {RequestTransactionID}).",
+ candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID);
+
+ candidatePair.TurnConnectRequestSent++;
+
+ SendTurnConnectBindRequest(candidatePair.RequestTransactionID, candidatePair.TurnConnectionId,
+ candidatePair.LocalCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint);
+
+ candidatePair.TurnConnectReportAt = DateTime.Now;
+ }
+ }
+ // we requested Allocate and we are controller (initiator), we should send a Connect request to the TURN server
+ else if (IsController)
+ {
+ if (DateTime.Now - candidatePair.TurnConnectReportAt >= IceServer.waittime)
+ {
+ if (candidatePair.TurnConnectRequestSent >= IceServer.MAX_ERRORS)
+ {
+ logger.LogDebug("ICE RTP channel failed to request Connect to {IceServerUri} after {TurnRequestSent} attempts.",
+ candidatePair.LocalCandidate.IceServer._uri, candidatePair.TurnConnectRequestSent);
+
+ candidatePair.State = ChecklistEntryState.Failed;
+ }
+ else
+ {
+ logger.LogDebug("ICE RTP channel sending Connect request to {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).",
+ candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID);
+
+ candidatePair.TurnConnectRequestSent++;
+
+ SendTurnConnectRequest(candidatePair.RequestTransactionID,
+ candidatePair.LocalCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint);
+
+ candidatePair.TurnConnectReportAt = DateTime.Now;
+ }
+
+ // Workaround to prevent being timed out by the checking loop since we have waittime.
+ // TODO: should probably provide way to decide time out when using TURN server with TCP relay.
+ candidatePair.FirstCheckSentAt = candidatePair.TurnConnectReportAt + IceServer.waittime;
+ }
+ }
+ // we are not controller (responder), we should wait for the controller to send Connect request
+ else
+ {
+ candidatePair.State = ChecklistEntryState.Frozen;
+ }
+ }
+ else
+ {
+ if (candidatePair.TcpBindRequestSent >= IceServer.MAX_REQUESTS)
+ {
+ logger.LogDebug("ICE RTP channel failed to bind with peer {peerEp} after {reqsents} attempts.",
+ candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.TcpBindRequestSent);
+
+ candidatePair.State = ChecklistEntryState.Failed;
+ }
+
+ candidatePair.TcpBindRequestSent++;
+
+ var localep = RtpTcpSocketByUri[candidatePair.LocalCandidate.IceServer._secondaryRelayUri].LocalEndPoint;
+ var relayep = candidatePair.LocalCandidate.IceServer.ServerEndPoint;
+
+ logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to relay at {RelayServerEndPoint} (use candidate {SetUseCandidate}).",
+ candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), localep, relayep, setUseCandidate);
+
+ SendSTUNBindingRequest(candidatePair, setUseCandidate);
+ }
+ }
+ }
+ // udp
else
{
+ // allow peer to send/connect to relay first
+ if (isRelayCheck && candidatePair.TurnPermissionsResponseAt == DateTime.MinValue)
+ {
+ if (candidatePair.TurnPermissionsRequestSent >= IceServer.MAX_REQUESTS)
+ {
+ logger.LogWarning("ICE RTP channel failed to get a Create Permissions response from {IceServerUri} after {TurnPermissionsRequestSent} attempts.",
+ candidatePair.LocalCandidate.IceServer._uri, candidatePair.TurnPermissionsRequestSent);
+
+ candidatePair.State = ChecklistEntryState.Failed;
+ }
+ else
+ {
+ // Send Create Permissions request to TURN server for remote candidate.
+ candidatePair.TurnPermissionsRequestSent++;
+
+ logger.LogDebug("ICE RTP channel sending TURN permissions request {TurnPermissionsRequestSent} to server {IceServerUri} for peer {RemoteCandidate} (TxID: {RequestTransactionID}).",
+ candidatePair.TurnPermissionsRequestSent, candidatePair.LocalCandidate.IceServer._uri, candidatePair.RemoteCandidate.DestinationEndPoint, candidatePair.RequestTransactionID);
+
+ SendTurnCreatePermissionsRequest(candidatePair.RequestTransactionID,
+ candidatePair.LocalCandidate.IceServer, candidatePair.RemoteCandidate.DestinationEndPoint);
+ }
+ }
+ else
+ {
if (candidatePair.LocalCandidate.type == RTCIceCandidateType.relay)
{
IPEndPoint relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint;
- logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to relay at {RelayServerEndPoint} (use candidate {SetUseCandidate}).", candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, relayServerEP, setUseCandidate);
+ logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to relay at {RelayServerEndPoint} (use candidate {SetUseCandidate}).",
+ candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, relayServerEP, setUseCandidate);
}
else
{
IPEndPoint remoteEndPoint = candidatePair.RemoteCandidate.DestinationEndPoint;
- logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to {RemoteEndPoint} (use candidate {SetUseCandidate}).", candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, remoteEndPoint, setUseCandidate);
+ logger.LogDebug("ICE RTP channel sending connectivity check for {LocalCandidate}->{RemoteCandidate} from {LocalEndPoint} to {RemoteEndPoint} (use candidate {SetUseCandidate}).",
+ candidatePair.LocalCandidate.ToShortString(), candidatePair.RemoteCandidate.ToShortString(), base.RTPLocalEndPoint, remoteEndPoint, setUseCandidate);
}
SendSTUNBindingRequest(candidatePair, setUseCandidate);
}
}
+ }
+
+ private void SendTurnConnectRequest(string requestTransactionID, IceServer iceServer, IPEndPoint destinationEndPoint)
+ {
+ var stunMsg = new STUNMessage(STUNMessageTypesEnum.Connect);
+ stunMsg.Header.TransactionId = Encoding.ASCII.GetBytes(requestTransactionID);
+
+ stunMsg.Attributes.Add(new STUNXORAddressAttribute(STUNAttributeTypesEnum.XORPeerAddress,
+ destinationEndPoint.Port, destinationEndPoint.Address,
+ stunMsg.Header.TransactionId));
+
+ byte[] stunMsgBytes;
+
+ if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null)
+ {
+ stunMsgBytes = GetAuthenticatedStunRequest(stunMsg, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce);
+ }
+ else
+ {
+ stunMsgBytes = stunMsg.ToByteBuffer(null, false);
+ }
+
+ var sendResult = SendOverTCP(iceServer, stunMsgBytes);
+
+ if (sendResult != SocketError.Success)
+ {
+ logger.LogWarning("Error sending TURN Connect request {OutstandingRequestsSent} for {Uri} to {RemoteEndPoint}. {SendResult}.",
+ iceServer.OutstandingRequestsSent, iceServer._uri, destinationEndPoint, sendResult);
+ }
+ else
+ {
+ OnStunMessageSent?.Invoke(stunMsg, iceServer.ServerEndPoint, false);
+ }
+ }
+
+ private void SendTurnConnectBindRequest(string requestTransactionID, uint turnConnectionId, IceServer iceServer, IPEndPoint destinationEndPoint)
+ {
+ // creates new TCP data relay channel to remote peer if not exists
+ var uri = STUNUri.ParseSTUNUri($"{destinationEndPoint.Address}:{destinationEndPoint.Port}?transport=tcp");
+ if (uri != null && !RtpTcpSocketByUri.ContainsKey(uri))
+ {
+ NetServices.CreateRtpSocket(false, ProtocolType.Tcp, null, 0, null, true, true, out var rtpTcpSocket, out _);
+
+ if (rtpTcpSocket == null)
+ {
+ throw new ApplicationException("The RTP channel was not able to create an RTP socket.");
+ }
+
+ RtpTcpSocketByUri.Add(uri, rtpTcpSocket);
+
+ if (uri != null && !m_rtpTcpReceiverByUri.ContainsKey(uri) && rtpTcpSocket != null)
+ {
+ var rtpTcpReceiver = GetIceTcpReceiver(rtpTcpSocket, true);
+
+ m_rtpTcpReceiverByUri.Add(uri, rtpTcpReceiver);
+ }
+
+ iceServer._secondaryRelayUri = uri;
+ }
+
+ var stunMsg = new STUNMessage(STUNMessageTypesEnum.ConnectionBind);
+ stunMsg.Header.TransactionId = Encoding.ASCII.GetBytes(requestTransactionID);
+
+ stunMsg.Attributes.Add(new STUNConnectionIdAttribute(turnConnectionId));
+
+ byte[] stunMsgBytes;
+
+ if (iceServer.Nonce != null && iceServer.Realm != null && iceServer._username != null && iceServer._password != null)
+ {
+ stunMsgBytes = GetAuthenticatedStunRequest(stunMsg, iceServer._username, iceServer.Realm, iceServer._password, iceServer.Nonce);
+ }
+ else
+ {
+ stunMsgBytes = stunMsg.ToByteBuffer(null, false);
+ }
+
+ var sendResult = SendOverTCP(uri, stunMsgBytes, iceServer.ServerEndPoint);
+
+ if (sendResult != SocketError.Success)
+ {
+ logger.LogWarning("Error sending TURN ConnectionBind request {OutstandingRequestsSent} to {RemoteEndPoint}. {SendResult}.",
+ iceServer.OutstandingRequestsSent, destinationEndPoint, sendResult);
+ }
+ else
+ {
+ OnStunMessageSent?.Invoke(stunMsg, destinationEndPoint, false);
+ }
+ }
///
/// Builds and sends a STUN binding request to a remote peer based on the candidate pair properties.
@@ -1846,9 +2134,16 @@ private void SendSTUNBindingRequest(ChecklistEntry candidatePair, bool setUseCan
if (candidatePair.LocalCandidate.type == RTCIceCandidateType.relay)
{
- IPEndPoint relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint;
- var protocol = candidatePair.LocalCandidate.IceServer.Protocol;
- SendRelay(protocol, candidatePair.RemoteCandidate.DestinationEndPoint, stunReqBytes, relayServerEP, candidatePair.LocalCandidate.IceServer);
+ if (candidatePair.LocalCandidate.protocol == RTCIceProtocol.tcp)
+ {
+ SendOverTCP(candidatePair.LocalCandidate.IceServer._secondaryRelayUri, stunReqBytes, candidatePair.LocalCandidate.IceServer.ServerEndPoint);
+ }
+ else
+ {
+ IPEndPoint relayServerEP = candidatePair.LocalCandidate.IceServer.ServerEndPoint;
+ var protocol = candidatePair.LocalCandidate.IceServer.Protocol;
+ SendRelay(protocol, candidatePair.RemoteCandidate.DestinationEndPoint, stunReqBytes, relayServerEP, candidatePair.LocalCandidate.IceServer);
+ }
}
else
{
@@ -1936,6 +2231,7 @@ private void SendCheckOnConnectedPair(ChecklistEntry candidatePair)
///
/// The STUN message received.
/// The remote end point the STUN packet was received from.
+ /// The message was encapsulated within TURN data connection.
public async Task ProcessStunMessage(STUNMessage stunMessage, IPEndPoint remoteEndPoint, bool wasRelayed)
{
if (_closed)
@@ -1949,6 +2245,7 @@ public async Task ProcessStunMessage(STUNMessage stunMessage, IPEndPoint remoteE
// Check if the STUN message is for an ICE server check.
var iceServer = GetIceServerForTransactionID(stunMessage.Header.TransactionId);
+
if (iceServer != null)
{
bool candidatesAvailable = iceServer.GotStunResponse(stunMessage, remoteEndPoint);
@@ -1968,6 +2265,35 @@ public async Task ProcessStunMessage(STUNMessage stunMessage, IPEndPoint remoteE
{
GotStunBindingRequest(stunMessage, remoteEndPoint, wasRelayed);
}
+ else if (stunMessage.Header.MessageType == STUNMessageTypesEnum.ConnectionAttemptIndication)
+ {
+ var connaddr = ((STUNXORAddressAttribute)stunMessage.Attributes
+ .FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.XORPeerAddress))
+ .GetIPEndPoint();
+
+ logger.LogDebug("A ConnectionAttempt Indication was received for remote {RemoteAdress}.", connaddr);
+
+ var bytes = stunMessage.Attributes.FirstOrDefault(x => x.AttributeType == STUNAttributeTypesEnum.ConnectionId)?.Value;
+
+ lock (_checklistLock)
+ {
+ var matchedPair = _checklist.FirstOrDefault(x => x.State == ChecklistEntryState.Frozen
+ && x.LocalCandidate.type == RTCIceCandidateType.relay
+ && x.LocalCandidate.IceServer.ServerEndPoint.Equals(remoteEndPoint)
+ && x.RemoteCandidate.DestinationEndPoint.Equals(connaddr));
+
+ if (matchedPair != null)
+ {
+ matchedPair.TurnConnectionId = BitConverter.IsLittleEndian ?
+ NetConvert.DoReverseEndian(BitConverter.ToUInt32(bytes, 0)) :
+ BitConverter.ToUInt32(bytes, 0);
+
+ matchedPair.FirstCheckSentAt = DateTime.MinValue;
+ matchedPair.State = ChecklistEntryState.Waiting;
+ matchedPair.TurnConnectReportAt = DateTime.Now;
+ }
+ }
+ }
else if (stunMessage.Header.MessageClass == STUNClassTypesEnum.ErrorResponse ||
stunMessage.Header.MessageClass == STUNClassTypesEnum.SuccessResponse)
{
@@ -2053,6 +2379,7 @@ private void ProcessNominateLogicAsController(ChecklistEntry possibleMatchingChe
possibleMatchingCheckEntry = betterOptionEntry;
findBetterOptionOrWait = false; //possibleMatchingCheckEntry.RemoteCandidate.type == RTCIceCandidateType.relay;
}
+ }
//if we still need to find a better option, we will search for matching entries with high priority that still processing
if (findBetterOptionOrWait)
@@ -2068,8 +2395,6 @@ private void ProcessNominateLogicAsController(ChecklistEntry possibleMatchingChe
possibleMatchingCheckEntry = null;
}
}
- }
- }
//Nominate Candidate if we pass in all heuristic checks from previous algorithm
if (possibleMatchingCheckEntry != null && possibleMatchingCheckEntry.State == ChecklistEntryState.Succeeded)
@@ -2078,6 +2403,7 @@ private void ProcessNominateLogicAsController(ChecklistEntry possibleMatchingChe
SendConnectivityCheck(possibleMatchingCheckEntry, true);
}
}
+ }
/*if (IsController && !_checklist.Any(x => x.Nominated))
{
@@ -2148,8 +2474,19 @@ private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remote
// - The entry that has a remote candidate with an end point that matches the endpoint this STUN request came from,
// - And if the STUN request was relayed through a TURN server then only match is the checklist local candidate is
// also a relay type. It is possible for the same remote end point to send STUN requests directly and via a TURN server.
- matchingChecklistEntry = _checklist.Where(x => x.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint) &&
+ matchingChecklistEntry = _checklist.Where(x =>
+ // TODO: remove one line below if host TCP candidate is supported, but need to modify the peer reflexive candidate acquiring below
+ x.RemoteCandidate.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint) &&
+ (
+ //x.RemoteCandidate.DestinationEndPoint.Equals(remoteEndPoint) &&
(!wasRelayed || x.LocalCandidate.type == RTCIceCandidateType.relay)
+ )
+ // - Local candidate is of type relay with direct data (TCP) and received from the relay server.
+ || (
+ x.LocalCandidate.type == RTCIceCandidateType.relay
+ && x.LocalCandidate.protocol == RTCIceProtocol.tcp
+ && x.LocalCandidate.IceServer.ServerEndPoint.Equals(remoteEndPoint)
+ )
).FirstOrDefault();
}
@@ -2158,6 +2495,7 @@ private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remote
{
// This STUN request has come from a socket not in the remote ICE candidates list.
// Add a new remote peer reflexive candidate.
+ // TODO: how to know if it should be TCP or UDP? or maybe just add both?
RTCIceCandidate peerRflxCandidate = new RTCIceCandidate(new RTCIceCandidateInit());
peerRflxCandidate.SetAddressProperties(RTCIceProtocol.udp, remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.prflx, null, 0);
peerRflxCandidate.SetDestinationEndPoint(remoteEndPoint);
@@ -2192,6 +2530,10 @@ private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remote
}
else
{
+ logger.LogTrace("Got STUN binding request for candidate {src}<-{dst}",
+ matchingChecklistEntry.LocalCandidate.DestinationEndPoint,
+ matchingChecklistEntry.RemoteCandidate.DestinationEndPoint);
+
// The UseCandidate attribute is only meant to be set by the "Controller" peer. This implementation
// will accept it irrespective of the peer roles. If the remote peer wants us to use a certain remote
// end point then so be it.
@@ -2219,10 +2561,21 @@ private void GotStunBindingRequest(STUNMessage bindingRequest, IPEndPoint remote
stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port);
byte[] stunRespBytes = stunResponse.ToByteBufferStringKey(LocalIcePassword, true);
+ logger.LogTrace("Sending STUN binding response for {lcl}->{rmt}.",
+ matchingChecklistEntry.LocalCandidate.DestinationEndPoint,
+ matchingChecklistEntry.RemoteCandidate.DestinationEndPoint);
+
if (wasRelayed)
{
- var protocol = matchingChecklistEntry.LocalCandidate.IceServer.Protocol;
- SendRelay(protocol, remoteEndPoint, stunRespBytes, matchingChecklistEntry.LocalCandidate.IceServer.ServerEndPoint, matchingChecklistEntry.LocalCandidate.IceServer);
+ if (matchingChecklistEntry.LocalCandidate.protocol == RTCIceProtocol.tcp)
+ {
+ SendOverTCP(matchingChecklistEntry.LocalCandidate.IceServer._secondaryRelayUri, stunRespBytes, matchingChecklistEntry.LocalCandidate.IceServer.ServerEndPoint);
+ }
+ else
+ {
+ var protocol = matchingChecklistEntry.LocalCandidate.IceServer.Protocol;
+ SendRelay(protocol, remoteEndPoint, stunRespBytes, matchingChecklistEntry.LocalCandidate.IceServer.ServerEndPoint, matchingChecklistEntry.LocalCandidate.IceServer);
+ }
OnStunMessageSent?.Invoke(stunResponse, remoteEndPoint, true);
}
else
@@ -2339,7 +2692,11 @@ private SocketError SendTurnAllocateRequest(IceServer iceServer)
STUNMessage allocateRequest = new STUNMessage(STUNMessageTypesEnum.Allocate);
allocateRequest.Header.TransactionId = Encoding.ASCII.GetBytes(iceServer.TransactionID);
- allocateRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.RequestedTransport, STUNAttributeConstants.UdpTransportType));
+
+ allocateRequest.Attributes.Add(new STUNAttribute(STUNAttributeTypesEnum.RequestedTransport,
+ iceServer._reqIceProtocol == ProtocolType.Tcp ?
+ STUNAttributeConstants.TcpTransportType : STUNAttributeConstants.UdpTransportType));
+
allocateRequest.Attributes.Add(
new STUNAttribute(STUNAttributeTypesEnum.RequestedAddressFamily,
iceServer.ServerEndPoint.AddressFamily == AddressFamily.InterNetwork ?
@@ -2356,13 +2713,31 @@ private SocketError SendTurnAllocateRequest(IceServer iceServer)
allocateReqBytes = allocateRequest.ToByteBuffer(null, false);
}
- var sendResult = iceServer.Protocol == ProtocolType.Tcp ?
- SendOverTCP(iceServer, allocateReqBytes) :
- base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes);
+ SocketError sendResult;
+ if (iceServer.Protocol == ProtocolType.Tcp)
+ {
+ sendResult = SendOverTCP(iceServer, allocateReqBytes);
+ }
+ else
+ {
+ // technically we could use TCP relay even if we use UDP when using standard-deviating TURN server,
+ // but RFC 6062 section 4-5 enforces this for standard TURN server.
+ // https://datatracker.ietf.org/doc/html/rfc6062#section-4.1
+ if (iceServer._reqIceProtocol == ProtocolType.Tcp)
+ {
+ sendResult = SocketError.ProtocolType;
+ logger.LogWarning("Cannot allocate TCP relay with TURN when using UDP transport with TURN server. " +
+ "Use [?{STUNTransportScheme}] on the iceServers' urls. ", STUNUri.SCHEME_TRANSPORT_TCP);
+ }
+ else
+ {
+ sendResult = base.Send(RTPChannelSocketsEnum.RTP, iceServer.ServerEndPoint, allocateReqBytes);
+ }
+ }
if (sendResult != SocketError.Success)
{
- logger.LogWarning("Error sending TURN Allocate request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. {SendResult}.",
+ logger.LogWarning("Error sending TURN Allocate request {OutstandingRequestsSent} for {Uri} to {ServerEndPoint}. Error = [{SendResult}].",
iceServer.OutstandingRequestsSent, iceServer._uri, iceServer.ServerEndPoint, sendResult);
}
else
@@ -2487,47 +2862,7 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer)
{
try
{
- //Connect to destination
- RtpTcpSocketByUri.TryGetValue(iceServer?._uri, out Socket sendSocket);
- //LastRtpDestination = dstEndPoint;
-
- if (sendSocket == null)
- {
- return SocketError.Fault;
- }
-
- //Prevent Send to IPV4 while socket is IPV6 (Mono Error)
- if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && sendSocket.AddressFamily != dstEndPoint.AddressFamily)
- {
- dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port);
- }
-
- Func equals = (IPEndPoint e1, IPEndPoint e2) =>
- {
- return e1.Port == e2.Port && e1.Address.Equals(e2.Address);
- };
-
- 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);
-
- 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;
+ return SendOverTCP(iceServer?._uri, buffer, dstEndPoint);
}
catch (ObjectDisposedException) // Thrown when socket is closed. Can be safely ignored.
{
@@ -2545,6 +2880,87 @@ protected virtual SocketError SendOverTCP(IceServer iceServer, byte[] buffer)
}
}
+ private static bool IPEqual(IPEndPoint e1, IPEndPoint e2)
+ {
+ return e1 is not null && e2 is not null && e1.Port == e2.Port && e1.Address.Equals(e2.Address);
+ }
+
+ private SocketError SendOverTCP(STUNUri uri, byte[] buffer, IPEndPoint dstEndPoint)
+ {
+ if (!RtpTcpSocketByUri.TryGetValue(uri, out Socket sendSocket))
+ {
+ return SocketError.Fault;
+ }
+
+ //Prevent Send to IPV4 while socket is IPV6 (Mono Error)
+ if (dstEndPoint.AddressFamily == AddressFamily.InterNetwork && sendSocket.AddressFamily != dstEndPoint.AddressFamily)
+ {
+ dstEndPoint = new IPEndPoint(dstEndPoint.Address.MapToIPv6(), dstEndPoint.Port);
+ }
+
+ if (!sendSocket.Connected || !IPEqual(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint))
+ {
+ var canReuse = false;
+ if (sendSocket.Connected)
+ {
+ logger.LogDebug("SendOverTCP request disconnect.");
+ sendSocket.Disconnect(true);
+ canReuse = true;
+ }
+
+ // There were problems and are not handled in IceTcpReceiver, we will fault here.
+ if (!canReuse && IPEqual(sendSocket.RemoteEndPoint as IPEndPoint, dstEndPoint))
+ {
+ logger.LogCritical("Something bad happened to TCP connection of {lcl}->{ep} and is no longer connected, Closing.",
+ sendSocket.LocalEndPoint, dstEndPoint);
+
+ if (m_rtpTcpReceiverByUri.TryGetValue(uri, out var oldIceTcpRecv))
+ {
+ oldIceTcpRecv.Close("Reconnection.");
+ }
+
+ return SocketError.SocketError;
+
+ var ep = (sendSocket.LocalEndPoint as IPEndPoint);
+ (var ip, var port) = (ep.Address, ep.Port);
+
+ //recreate sockets?
+ // TODO: may modify this path if base TCP is supported
+
+ NetServices.CreateRtpSocket(false, ProtocolType.Tcp, ip, port, null, true, true, out var reConnectSocket, out _);
+
+ m_rtpTcpReceiverByUri[uri] = GetIceTcpReceiver(reConnectSocket);
+ sendSocket = RtpTcpSocketByUri[uri] = reConnectSocket;
+ }
+
+ sendSocket.Connect(dstEndPoint);
+
+ logger.LogDebug("SendOverTCP status: {Status} endpoint: {EndPoint}", sendSocket.Connected, dstEndPoint);
+ }
+
+ //Fix ReceiveFrom logic if any previous exception happens
+ m_rtpTcpReceiverByUri.TryGetValue(uri, out IceTcpReceiver rtpTcpReceiver);
+ if (rtpTcpReceiver != null && !rtpTcpReceiver.IsRunningReceive && !rtpTcpReceiver.IsClosed)
+ {
+ rtpTcpReceiver.BeginReceiveFrom();
+ }
+
+ if (logger.IsEnabled(LogLevel.Trace))
+ {
+ logger.LogTrace("Sent {bits} bytes {src}", buffer.Length, sendSocket.LocalEndPoint.ToString());
+ }
+
+ // there's no directional endpoint in TCP, need to call connect beforehand
+ //sendSocket.BeginSendTo(buffer, 0, buffer.Length, SocketFlags.None, dstEndPoint, EndSendToTCP, sendSocket);
+ //sendSocket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, EndSendToTCP, sendSocket);
+
+ // synchronous is easier to maintain than async at this point
+ sendSocket.Send(buffer, 0, buffer.Length, SocketFlags.None, out var sendResult);
+
+ return sendResult;
+ //return SocketError.Success;
+ }
+
protected virtual void EndSendToTCP(IAsyncResult ar)
{
try
@@ -2591,6 +3007,25 @@ private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, string usern
return stunRequest.ToByteBuffer(hash, true);
}
+ void OnRTPPacketTCPRelayReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet)
+ {
+ if (packet?.Length > 0)
+ {
+ base.LastRtpDestination = remoteEndPoint;
+
+ if (packet[0] == 0x00 || packet[0] == 0x01)
+ {
+ // STUN packet.
+ var stunMessage = STUNMessage.ParseSTUNMessage(packet, packet.Length);
+ _ = ProcessStunMessage(stunMessage, remoteEndPoint, true);
+ }
+ else
+ {
+ OnRTPDataReceived?.Invoke(localPort, remoteEndPoint, packet);
+ }
+ }
+ }
+
///
/// Event handler for packets received on the RTP UDP socket. This channel will detect STUN messages
/// and extract STUN messages to deal with ICE connectivity checks and TURN relays.
@@ -2600,11 +3035,12 @@ private byte[] GetAuthenticatedStunRequest(STUNMessage stunRequest, string usern
/// The remote end point of the sender.
/// The raw packet received (note this may not be RTP if other protocols are being multiplexed).
protected override void OnRTPPacketReceived(UdpReceiver receiver, int localPort, IPEndPoint remoteEndPoint, byte[] packet)
+ => OnPacketReceived(localPort, remoteEndPoint, packet, false);
+
+ private void OnPacketReceived(int localPort, IPEndPoint remoteEndPoint, byte[] packet, bool wasRelayed)
{
if (packet?.Length > 0)
{
- bool wasRelayed = false;
-
if (packet[0] == 0x00 && packet[1] == 0x17)
{
wasRelayed = true;
@@ -2719,14 +3155,20 @@ private async Task ResolveMdnsName(RTCIceCandidate candidate)
public override SocketError Send(RTPChannelSocketsEnum sendOn, IPEndPoint dstEndPoint, byte[] buffer)
{
if (NominatedEntry != null && NominatedEntry.LocalCandidate.type == RTCIceCandidateType.relay &&
- NominatedEntry.LocalCandidate.IceServer != null &&
NominatedEntry.RemoteCandidate.DestinationEndPoint.Address.Equals(dstEndPoint.Address) &&
NominatedEntry.RemoteCandidate.DestinationEndPoint.Port == dstEndPoint.Port)
{
- // A TURN relay channel is being used to communicate with the remote peer.
- var protocol = NominatedEntry.LocalCandidate.IceServer.Protocol;
- var serverEndPoint = NominatedEntry.LocalCandidate.IceServer.ServerEndPoint;
- return SendRelay(protocol, dstEndPoint, buffer, serverEndPoint, NominatedEntry.LocalCandidate.IceServer);
+ if (NominatedEntry.LocalCandidate.protocol == RTCIceProtocol.tcp)
+ {
+ return SendOverTCP(NominatedEntry.LocalCandidate.IceServer._secondaryRelayUri, buffer, NominatedEntry.LocalCandidate.IceServer.ServerEndPoint);
+ }
+ else
+ {
+ // A TURN relay channel is being used to communicate with the remote peer.
+ var protocol = NominatedEntry.LocalCandidate.IceServer.Protocol;
+ var serverEndPoint = NominatedEntry.LocalCandidate.IceServer.ServerEndPoint;
+ return SendRelay(protocol, dstEndPoint, buffer, serverEndPoint, NominatedEntry.LocalCandidate.IceServer);
+ }
}
else
{
diff --git a/src/net/RTP/RTPChannel.cs b/src/net/RTP/RTPChannel.cs
index 3d8dd5894..c3ba48e71 100755
--- a/src/net/RTP/RTPChannel.cs
+++ b/src/net/RTP/RTPChannel.cs
@@ -352,18 +352,23 @@ public bool IsClosed
public event Action OnClosed;
///
- /// Creates a new RTP channel. The RTP and optionally RTCP sockets will be bound in the constructor.
- /// They do not start receiving until the Start method is called.
+ /// Creates a new RTP channel.
///
+ ///
+ ///
The RTP and optionally RTCP sockets will be bound in the constructor.
+ ///
They do not start receiving until the Start method is called.
+ ///
/// Set to true if a separate RTCP control socket should be created. If RTP and
/// RTCP are being multiplexed (as they are for WebRTC) there's no need to a separate control socket.
/// Optional. An IP address belonging to a local interface that will be used to bind
/// the RTP and control sockets to. If left empty then the IPv6 any address will be used if IPv6 is supported
/// and fallback to the IPv4 any address.
+ /// Wheter to use TCP as transport.
/// Optional. The specific port to attempt to bind the RTP port on.
- public RTPChannel(bool createControlSocket, IPAddress bindAddress, int bindPort = 0, PortRange rtpPortRange = null)
+ public RTPChannel(bool createControlSocket, IPAddress bindAddress, int bindPort = 0, PortRange rtpPortRange = null, bool useTcp = false)
{
- NetServices.CreateRtpSocket(createControlSocket, bindAddress, bindPort, rtpPortRange, out var rtpSocket, out m_controlSocket);
+ NetServices.CreateRtpSocket(createControlSocket, useTcp ? ProtocolType.Tcp : ProtocolType.Udp,
+ bindAddress, bindPort, rtpPortRange, true, true, out var rtpSocket, out m_controlSocket);
if (rtpSocket == null)
{
@@ -445,8 +450,8 @@ public void Close(string reason)
}
m_isClosed = true;
- m_rtpReceiver?.Close(null);
- m_controlReceiver?.Close(null);
+ m_rtpReceiver?.Close(closeReason);
+ m_controlReceiver?.Close(closeReason);
OnClosed?.Invoke(closeReason);
}
diff --git a/src/net/RTP/RTPSession.cs b/src/net/RTP/RTPSession.cs
index baaeabfe9..f85f3a96c 100755
--- a/src/net/RTP/RTPSession.cs
+++ b/src/net/RTP/RTPSession.cs
@@ -2217,7 +2217,7 @@ protected virtual RTPChannel CreateRtpChannel()
// If RTCP is multiplexed we don't need a control socket.
int bindPort = (rtpSessionConfig.BindPort == 0) ? 0 : rtpSessionConfig.BindPort + m_rtpChannelsCount;
- var rtpChannel = new RTPChannel(!rtpSessionConfig.IsRtcpMultiplexed, rtpSessionConfig.BindAddress, bindPort, rtpSessionConfig.RtpPortRange);
+ var rtpChannel = new RTPChannel(!rtpSessionConfig.IsRtcpMultiplexed, rtpSessionConfig.BindAddress/*, rtpSessionConfig.UseTCP*/, bindPort, rtpSessionConfig.RtpPortRange);
if (rtpSessionConfig.IsMediaMultiplexed)
diff --git a/src/net/RTP/RTPSessionConfig.cs b/src/net/RTP/RTPSessionConfig.cs
index e6c8f232d..865b76343 100644
--- a/src/net/RTP/RTPSessionConfig.cs
+++ b/src/net/RTP/RTPSessionConfig.cs
@@ -81,5 +81,10 @@ public sealed class RtpSessionConfig
public bool IsSecure { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.DtlsSrtp; }
public bool UseSdpCryptoNegotiation { get => RtpSecureMediaOption == RtpSecureMediaOptionEnum.SdpCryptoNegotiation; }
+
+ ///
+ /// Optional. If specified, will use TCP as transport.
+ ///
+ public bool UseTCP { get; set; } = false;
}
}
diff --git a/src/net/STUN/STUNHeader.cs b/src/net/STUN/STUNHeader.cs
index 3633b532a..c4880d3b4 100644
--- a/src/net/STUN/STUNHeader.cs
+++ b/src/net/STUN/STUNHeader.cs
@@ -1,4 +1,4 @@
-//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
// Filename: STUNHeader.cs
//
// Description: Implements STUN header as defined in RFC5389
@@ -105,10 +105,16 @@ public enum STUNMessageTypesEnum : ushort
// New methods defined in TURN (RFC6062).
Connect = 0x000a,
+ ConnectSuccess = 0x0100 | Connect,
+
ConnectionBind = 0x000b,
+ ConnectionBindSuccess = 0x0100 | ConnectionBind,
+
ConnectionAttempt = 0x000c,
+ ConnectionAttemptIndication = 0x0010 | ConnectionAttempt,
}
+
///
/// The class is interpreted from the message type. It does not get explicitly
/// set in the STUN header.
diff --git a/src/net/WebRTC/IRTCPeerConnection.cs b/src/net/WebRTC/IRTCPeerConnection.cs
index a26575656..cbe49d14e 100755
--- a/src/net/WebRTC/IRTCPeerConnection.cs
+++ b/src/net/WebRTC/IRTCPeerConnection.cs
@@ -97,6 +97,14 @@ public class RTCIceServer
public string username;
public RTCIceCredentialType credentialType;
public string credential;
+
+ ///
+ /// Controls what ICE candidate to be allocated if using TURN server.
+ ///
+ ///
+ /// Only affect TURN usage.
+ ///
+ public RTCIceProtocol X_ICERelayProtocol { get; set; }
}
///
@@ -356,6 +364,18 @@ public class RTCConfiguration
/// Timeout for gathering local IP addresses
///
public int X_GatherTimeoutMs = 30000;
+
+ ///
+ /// Forces the ICE candidates to be TCP candidate. Including TURN allocation.
+ ///
+ ///
+ /// WARNING, Experimental.
+ ///
Currently only works when used with = .
+ ///
This disables any support for browser-based peer-to-peer (non-relay) WebRTC connections.
+ ///
+ /// Also see .
+ ///
+ public bool X_ICEForceTCP = false;
}
///
diff --git a/src/net/WebRTC/RTCPeerConnection.cs b/src/net/WebRTC/RTCPeerConnection.cs
index 5e80ff488..3648c0933 100755
--- a/src/net/WebRTC/RTCPeerConnection.cs
+++ b/src/net/WebRTC/RTCPeerConnection.cs
@@ -385,7 +385,16 @@ public RTCPeerConnection() :
///
/// Optional.
public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0, PortRange portRange = null, Boolean videoAsPrimary = false) :
- base(true, true, true, configuration?.X_BindAddress, bindPort, portRange)
+ base(new RtpSessionConfig
+ {
+ IsMediaMultiplexed = true,
+ IsRtcpMultiplexed = true,
+ RtpSecureMediaOption = RtpSecureMediaOptionEnum.DtlsSrtp,
+ BindAddress = configuration?.X_BindAddress,
+ BindPort = bindPort,
+ RtpPortRange = portRange,
+ UseTCP = configuration.X_ICEForceTCP
+ })
{
dataChannels = new RTCDataChannelCollection(useEvenIds: () => _dtlsHandle.IsClient);
@@ -409,6 +418,23 @@ public RTCPeerConnection(RTCConfiguration configuration, int bindPort = 0, PortR
{
RTP_MEDIA_PROFILE = RTP_MEDIA_FEEDBACK_PROFILE;
}
+
+ if (_configuration.X_ICEForceTCP)
+ {
+ // TODO: Remove conditional block when TCP is implemented for direct RTP
+ if (_configuration.iceTransportPolicy == RTCIceTransportPolicy.relay)
+ {
+ // TODO: keep logging when TCP is implemented for direct RTP
+ logger.LogDebug("Forcing all ICE candidates to use TCP.");
+ }
+ else
+ {
+ logger.LogWarning("Experimental {X_ICEForceTCP} is set but ICE transport policy is not {relay}, ICE candidates will not use TCP.",
+ nameof(_configuration.X_ICEForceTCP), RTCIceTransportPolicy.relay);
+
+ _configuration.X_ICEForceTCP = false;
+ }
+ }
}
else
{
@@ -653,7 +679,8 @@ protected override RTPChannel CreateRtpChannel()
_configuration != null ? _configuration.iceTransportPolicy : RTCIceTransportPolicy.all,
_configuration != null ? _configuration.X_ICEIncludeAllInterfaceAddresses : false,
rtpSessionConfig.BindPort == 0 ? 0 : rtpSessionConfig.BindPort + m_rtpChannelsCount * 2,
- rtpSessionConfig.RtpPortRange);
+ rtpSessionConfig.RtpPortRange,
+ _configuration.X_ICEForceTCP);
if (rtpSessionConfig.IsMediaMultiplexed)
{
@@ -662,8 +689,11 @@ protected override RTPChannel CreateRtpChannel()
rtpIceChannel.OnRTPDataReceived += OnRTPDataReceived;
+ if (!_configuration.X_ICEForceTCP)
+ {
// Start the RTP, and if required the Control, socket receivers and the RTCP session.
rtpIceChannel.Start();
+ }
m_rtpChannelsCount++;