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++;