/// <summary> /// Gets an ICE candidate for this ICE server once the required server responses have been received. /// Note the related address and port are deliberately not set to avoid leaking information about /// internal network configuration. /// </summary> /// <param name="init">The initialisation parameters for the ICE candidate (mainly local username).</param> /// <param name="type">The type of ICE candidate to get, must be srflx or relay.</param> /// <returns>An ICE candidate that can be sent to the remote peer.</returns> internal RTCIceCandidate GetCandidate(RTCIceCandidateInit init, RTCIceCandidateType type) { RTCIceCandidate candidate = new RTCIceCandidate(init); if (type == RTCIceCandidateType.srflx && ServerReflexiveEndPoint != null) { candidate.SetAddressProperties(RTCIceProtocol.udp, ServerReflexiveEndPoint.Address, (ushort)ServerReflexiveEndPoint.Port, type, null, 0); candidate.IceServer = this; return(candidate); } else if (type == RTCIceCandidateType.relay && RelayEndPoint != null) { candidate.SetAddressProperties(RTCIceProtocol.udp, RelayEndPoint.Address, (ushort)RelayEndPoint.Port, type, null, 0); candidate.IceServer = this; return(candidate); } else { logger.LogWarning($"Could not get ICE server candidate for {_uri} and type {type}."); return(null); } }
/// <summary> /// Acquires an ICE candidate for each IP address that this host has except for: /// - Loopback addresses must not be included. /// - Deprecated IPv4-compatible IPv6 addresses and IPv6 site-local unicast addresses /// must not be included, /// - IPv4-mapped IPv6 address should not be included. /// - If a non-location tracking IPv6 address is available use it and do not included /// location tracking enabled IPv6 addresses (i.e. prefer temporary IPv6 addresses over /// permanent addresses), see RFC6724. /// </summary> /// <remarks>See https://tools.ietf.org/html/rfc8445#section-5.1.1.1</remarks> /// <returns>A list of "host" ICE candidates for the local machine.</returns> private List <RTCIceCandidate> GetHostCandidates() { List <RTCIceCandidate> hostCandidates = new List <RTCIceCandidate>(); RTCIceCandidateInit init = new RTCIceCandidateInit { usernameFragment = LocalIceUser }; var rtpBindAddress = _rtpChannel.RTPLocalEndPoint.Address; // We get a list of local addresses that can be used with the address the RTP socket is bound on. List <IPAddress> localAddresses = null; if (IPAddress.IPv6Any.Equals(rtpBindAddress)) { if (_rtpChannel.RtpSocket.DualMode) { // IPv6 dual mode listening on [::] means we can use all valid local addresses. localAddresses = NetServices.LocalIPAddresses.Where(x => !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal).ToList(); } else { // IPv6 but not dual mode on [::] means can use all valid local IPv6 addresses. localAddresses = NetServices.LocalIPAddresses.Where(x => x.AddressFamily == AddressFamily.InterNetworkV6 && !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal).ToList(); } } else if (IPAddress.Any.Equals(rtpBindAddress)) { // IPv4 on 0.0.0.0 means can use all valid local IPv4 addresses. localAddresses = NetServices.LocalIPAddresses.Where(x => x.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(x)).ToList(); } else { // If not bound on a [::] or 0.0.0.0 means we're only listening on a specific IP address // and that's the only one that can be used for the host candidate. localAddresses = new List <IPAddress> { rtpBindAddress }; } foreach (var localAddress in localAddresses) { var hostCandidate = new RTCIceCandidate(init); hostCandidate.SetAddressProperties(RTCIceProtocol.udp, localAddress, (ushort)_rtpChannel.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 == 0) { hostCandidates.Add(hostCandidate); OnIceCandidate?.Invoke(hostCandidate); } } return(hostCandidates); }
/// <summary> /// Creates a new entry for the ICE session checklist. /// </summary> /// <param name="localCandidate">The local candidate for the checklist pair.</param> /// <param name="remoteCandidate">The remote candidate for the checklist pair.</param> /// <param name="isLocalController">True if we are acting as the controlling agent in the ICE session.</param> public ChecklistEntry(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate, bool isLocalController) { LocalCandidate = localCandidate; RemoteCandidate = remoteCandidate; IsLocalController = isLocalController; LocalPriority = localCandidate.priority; RemotePriority = remoteCandidate.priority; }
/// <summary> /// Creates a new entry for the ICE session checklist. /// </summary> /// <param name="localCandidate">The local candidate for the checklist pair.</param> /// <param name="remoteCandidate">The remote candidate for the checklist pair.</param> /// <param name="isLocalController">True if we are acting as the controlling agent in the ICE session.</param> public ChecklistEntry(RTCIceCandidate localCandidate, RTCIceCandidate remoteCandidate, bool isLocalController) { LocalCandidate = localCandidate; RemoteCandidate = remoteCandidate; var controllingCandidate = (isLocalController) ? localCandidate : remoteCandidate; var controlledCandidate = (isLocalController) ? remoteCandidate : localCandidate; Priority = (2 << 32) * Math.Min(controllingCandidate.priority, controlledCandidate.priority) + (ulong)2 * Math.Max(controllingCandidate.priority, controlledCandidate.priority) + (ulong)((controllingCandidate.priority > controlledCandidate.priority) ? 1 : 0); }
public static RTCIceCandidate Parse(string candidateLine) { if (string.IsNullOrEmpty(candidateLine)) { throw new ArgumentNullException("Cant parse ICE candidate from empty string.", candidateLine); } else { candidateLine = candidateLine.Replace("candidate:", ""); RTCIceCandidate candidate = new RTCIceCandidate(); string[] candidateFields = candidateLine.Trim().Split(' '); candidate.foundation = candidateFields[0]; if (Enum.TryParse <RTCIceComponent>(candidateFields[1], out var candidateComponent)) { candidate.component = candidateComponent; } if (Enum.TryParse <RTCIceProtocol>(candidateFields[2], out var candidateProtocol)) { candidate.protocol = candidateProtocol; } if (ulong.TryParse(candidateFields[3], out var candidatePriority)) { candidate.priority = candidatePriority; } candidate.address = candidateFields[4]; candidate.port = Convert.ToUInt16(candidateFields[5]); if (Enum.TryParse <RTCIceCandidateType>(candidateFields[7], out var candidateType)) { candidate.type = candidateType; } if (candidateFields.Length > 8 && candidateFields[8] == REMOTE_ADDRESS_KEY) { candidate.relatedAddress = candidateFields[9]; } if (candidateFields.Length > 10 && candidateFields[10] == REMOTE_PORT_KEY) { candidate.relatedPort = Convert.ToUInt16(candidateFields[11]); } return(candidate); } }
/// <summary> /// Adds a remote ICE candidate to the list this peer is attempting to connect against. /// </summary> /// <param name="candidateInit">The remote candidate to add.</param> public void addIceCandidate(RTCIceCandidateInit candidateInit) { RTCIceCandidate candidate = new RTCIceCandidate(candidateInit); if (_rtpIceChannel.Component == candidate.component) { _rtpIceChannel.AddRemoteCandidate(candidate); } else { logger.LogWarning($"Remote ICE candidate not added as no available ICE session for component {candidate.component}."); } }
/// <summary> /// Updates the checklist with new candidate pairs. /// </summary> /// <remarks> /// From https://tools.ietf.org/html/rfc8445#section-6.1.2.2: /// IPv6 link-local addresses MUST NOT be paired with other than link-local addresses. /// </remarks> private void UpdateChecklist(RTCIceCandidate remoteCandidate) { lock (_checklist) { // Local server reflexive candidates don't get added to the checklist since they are just local // "host" candidates with an extra NAT address mapping. The NAT address mapping is needed for the // remote ICE peer but locally a server reflexive candidate is always going to be represented by // a "host" candidate. foreach (var localCandidate in Candidates.Where(x => x.type != RTCIceCandidateType.srflx)) { if (localCandidate.CandidateAddress != null && remoteCandidate.CandidateAddress != null && localCandidate.CandidateAddress.AddressFamily == remoteCandidate.CandidateAddress.AddressFamily) { if (remoteCandidate.CandidateAddress.IsIPv6LinkLocal) { if (localCandidate.CandidateAddress.IsIPv6LinkLocal) { // Only pair IPv6 link local candidates if both are link local. ChecklistEntry entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); // Because only ONE checklist is currently supported each candidate pair can be set to // a "waiting" state. If an additional checklist is ever added then only one candidate // pair with the same foundation should be set to waiting across all checklists. // See https://tools.ietf.org/html/rfc8445#section-6.1.2.6 for a somewhat convoluted // explanation and example. entry.State = ChecklistEntryState.Waiting; _checklist.Add(entry); } } else { ChecklistEntry entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); // See comment above about why the candidate state is adjusted. entry.State = ChecklistEntryState.Waiting; _checklist.Add(entry); } } } // Finally sort the checklist to put it in priority order and if necessary remove lower // priority pairs. _checklist.Sort(); while (_checklist.Count > MAX_CHECKLIST_ENTRIES) { _checklist.RemoveAt(_checklist.Count - 1); } } }
/// <summary> /// Adds a remote ICE candidate to the ICE session. /// </summary> /// <param name="candidate">An ICE candidate from the remote party.</param> public void AddRemoteCandidate(RTCIceCandidate candidate) { if (!_remoteCandidates.Any(x => x.foundation == candidate.foundation)) { _remoteCandidates.Add(candidate); UpdateChecklist(candidate); } else { // This occurs if the remote party made an offer and assumed we couldn't multiplex the audio and video streams. // It will offer the same ICE candidates separately for the audio and video announcements. logger.LogWarning($"ICE session not adding remote ICE candidate as candidate with foundation {candidate.foundation} already present."); } }
/// <summary> /// Adds a remote ICE candidate to the ICE session. /// </summary> /// <param name="candidate">An ICE candidate from the remote party.</param> public void AddRemoteCandidate(RTCIceCandidate candidate) { if (!_remoteCandidates.Any(x => x.foundation == candidate.foundation)) { // Have a remote candidate. Connectivity checks can start. Note because we support ICE trickle // we may also still be gathering candidates. Connectivity checks and gathering can be done in parallel. _remoteCandidates.Add(candidate); UpdateChecklist(candidate); } else { // This occurs if the remote party made an offer and assumed we couldn't multiplex the audio and video streams. // It will offer the same ICE candidates separately for the audio and video announcements. logger.LogWarning($"ICE session not adding remote ICE candidate as candidate with foundation {candidate.foundation} already present."); } }
/// <summary> /// Adds a remote ICE candidate to the ICE session. /// </summary> /// <param name="candidate">An ICE candidate from the remote party.</param> public void AddRemoteCandidate(RTCIceCandidate candidate) { if (candidate.component == Component) { // Have a remote candidate. Connectivity checks can start. Note because we support ICE trickle // we may also still be gathering candidates. Connectivity checks and gathering can be done in parallel. logger.LogDebug($"ICE session adding remote candidate: {candidate.ToString()}"); _remoteCandidates.Add(candidate); UpdateChecklist(candidate); } else { // This occurs if the remote party made an offer and assumed we couldn't multiplex the audio and video streams. // It will offer the same ICE candidates separately for the audio and video announcements. logger.LogWarning($"ICE session omitting remote candidate with unsupported component: {candidate.ToString()}"); } }
/// <summary> /// Acquires an ICE candidate for each IP address that this host has except for: /// - Loopback addresses must not be included. /// - Deprecated IPv4-compatible IPv6 addresses and IPv6 site-local unicast addresses /// must not be included, /// - IPv4-mapped IPv6 address should not be included. /// - If a non-location tracking IPv6 address is available use it and do not included /// location tracking enabled IPv6 addresses (i.e. prefer temporary IPv6 addresses over /// permanent addresses), see RFC6724. /// </summary> /// <remarks>See https://tools.ietf.org/html/rfc8445#section-5.1.1.1</remarks> /// <returns>A list of "host" ICE candidates for the local machine.</returns> private List <RTCIceCandidate> GetHostCandidates() { List <RTCIceCandidate> hostCandidates = new List <RTCIceCandidate>(); RTCIceCandidateInit init = new RTCIceCandidateInit { usernameFragment = LocalIceUser }; foreach (var localAddress in NetServices.LocalIPAddresses.Where(x => !IPAddress.IsLoopback(x) && !x.IsIPv4MappedToIPv6 && !x.IsIPv6SiteLocal)) { var hostCandidate = new RTCIceCandidate(init); hostCandidate.SetAddressProperties(RTCIceProtocol.udp, localAddress, (ushort)_rtpChannel.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 == 0) { hostCandidates.Add(hostCandidate); } } return(hostCandidates); }
/// <summary> /// Updates the checklist with new candidate pairs. /// </summary> /// <remarks> /// From https://tools.ietf.org/html/rfc8445#section-6.1.2.2: /// IPv6 link-local addresses MUST NOT be paired with other than link-local addresses. /// </remarks> private void UpdateChecklist(RTCIceCandidate remoteCandidate) { lock (_checklist) { // TODO: Check for duplicate entries and adjust reflexive local candidates to use the base address // as per https://tools.ietf.org/html/rfc8445#section-6.1.2.4. foreach (var localCandidate in Candidates) { if (localCandidate.CandidateAddress != null && remoteCandidate.CandidateAddress != null && localCandidate.CandidateAddress.AddressFamily == remoteCandidate.CandidateAddress.AddressFamily) { if (remoteCandidate.CandidateAddress.IsIPv6LinkLocal) { if (localCandidate.CandidateAddress.IsIPv6LinkLocal) { // Only pair IPv6 link local candidates if both are link local. ChecklistEntry entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); _checklist.Add(entry); _checklist.Sort(); } } else { ChecklistEntry entry = new ChecklistEntry(localCandidate, remoteCandidate, IsController); _checklist.Add(entry); _checklist.Sort(); } } } while (_checklist.Count > MAX_CHECKLIST_ENTRIES) { _checklist.RemoveAt(_checklist.Count - 1); } } }
/// <summary> /// Updates the checklist with new candidate pairs. /// </summary> /// <remarks> /// From https://tools.ietf.org/html/rfc8445#section-6.1.2.2: /// IPv6 link-local addresses MUST NOT be paired with other than link-local addresses. /// </remarks> private void UpdateChecklist(RTCIceCandidate remoteCandidate) { lock (_checklist) { // Local server reflexive candidates don't get added to the checklist since they are just local // "host" candidates with an extra NAT address mapping. The NAT address mapping is needed for the // remote ICE peer but locally a server reflexive candidate is always going to be represented by // a "host" candidate. bool supportsIPv4 = _rtpChannel.RtpSocket.AddressFamily == AddressFamily.InterNetwork || _rtpChannel.IsDualMode; bool supportsIPv6 = _rtpChannel.RtpSocket.AddressFamily == AddressFamily.InterNetworkV6 || _rtpChannel.IsDualMode; if (remoteCandidate.addressFamily == AddressFamily.InterNetwork && supportsIPv4 || remoteCandidate.addressFamily == AddressFamily.InterNetworkV6 && supportsIPv6) { ChecklistEntry entry = new ChecklistEntry(_localChecklistCandidate, remoteCandidate, IsController); // Because only ONE checklist is currently supported each candidate pair can be set to // a "waiting" state. If an additional checklist is ever added then only one candidate // pair with the same foundation should be set to waiting across all checklists. // See https://tools.ietf.org/html/rfc8445#section-6.1.2.6 for a somewhat convoluted // explanation and example. entry.State = ChecklistEntryState.Waiting; AddChecklistEntry(entry); } // Finally sort the checklist to put it in priority order and if necessary remove lower // priority pairs. _checklist.Sort(); while (_checklist.Count > MAX_CHECKLIST_ENTRIES) { _checklist.RemoveAt(_checklist.Count - 1); } } }
/// <summary> /// Creates a new instance of an ICE session. /// </summary> /// <param name="rtpChannel">The RTP channel is the object managing the socket /// doing the media sending and receiving. Its the same socket the ICE session /// will need to initiate all the connectivity checks on.</param> /// <param name="component">The component (RTP or RTCP) the channel is being used for. Note /// for cases where RTP and RTCP are multiplexed the component is set to RTP.</param> public IceSession(RTPChannel rtpChannel, RTCIceComponent component) { if (rtpChannel == null) { throw new ArgumentNullException("rtpChannel"); } _rtpChannel = rtpChannel; Component = component; LocalIceUser = Crypto.GetRandomString(ICE_UFRAG_LENGTH); LocalIcePassword = Crypto.GetRandomString(ICE_PASSWORD_LENGTH); _localChecklistCandidate = new RTCIceCandidate(new RTCIceCandidateInit { sdpMid = "0", sdpMLineIndex = 0, usernameFragment = LocalIceUser }); _localChecklistCandidate.SetAddressProperties( RTCIceProtocol.udp, _rtpChannel.RTPLocalEndPoint.Address, (ushort)_rtpChannel.RTPLocalEndPoint.Port, RTCIceCandidateType.host, null, 0); }
/// <summary> /// Attempts to get a list of local ICE candidates. /// </summary> //private async Task GetIceCandidatesAsync() //{ // // The media is being multiplexed so the audio and video RTP channel is the same. // var rtpChannel = GetRtpChannel(SDPMediaTypesEnum.audio); // if (rtpChannel == null) // { // throw new ApplicationException("Cannot start gathering ICE candidates without an RTP channel."); // } // else // { // var localIPAddresses = _offerAddresses ?? NetServices.GetAllLocalIPAddresses(); // IceNegotiationStartedAt = DateTime.Now; // LocalIceCandidates = new List<IceCandidate>(); // foreach (var address in localIPAddresses.Where(x => x.AddressFamily == rtpChannel.RTPLocalEndPoint.AddressFamily)) // { // var iceCandidate = new IceCandidate(address, (ushort)rtpChannel.RTPPort); // if (_turnServerEndPoint != null) // { // iceCandidate.TurnServer = new TurnServer() { ServerEndPoint = _turnServerEndPoint }; // iceCandidate.InitialStunBindingCheck = SendTurnServerBindingRequest(iceCandidate); // } // LocalIceCandidates.Add(iceCandidate); // } // await Task.WhenAll(LocalIceCandidates.Where(x => x.InitialStunBindingCheck != null).Select(x => x.InitialStunBindingCheck)).ConfigureAwait(false); // } //} public void ProcessStunMessage(STUNv2Message stunMessage, IPEndPoint receivedOn) { IPEndPoint remoteEndPoint = (!receivedOn.Address.IsIPv4MappedToIPv6) ? receivedOn : new IPEndPoint(receivedOn.Address.MapToIPv4(), receivedOn.Port); //logger.LogDebug($"STUN message received from remote {remoteEndPoint} {stunMessage.Header.MessageType}."); if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingRequest) { STUNv2Message stunResponse = new STUNv2Message(STUNv2MessageTypesEnum.BindingSuccessResponse); stunResponse.Header.TransactionId = stunMessage.Header.TransactionId; stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); // ToDo: Check authentication. string localIcePassword = LocalIcePassword; byte[] stunRespBytes = stunResponse.ToByteBufferStringKey(localIcePassword, true); //iceCandidate.LocalRtpSocket.SendTo(stunRespBytes, remoteEndPoint); _rtpChannel.SendAsync(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunRespBytes); //iceCandidate.LastStunRequestReceivedAt = DateTime.Now; //iceCandidate.IsStunRemoteExchangeComplete = true; //if (remoteEndPoint == null) //{ //RemoteEndPoint = remoteEndPoint; //SetDestination(SDPMediaTypesEnum.audio, RemoteEndPoint, RemoteEndPoint); //OnIceConnected?.Invoke(iceCandidate, remoteEndPoint); //IceConnectionState = RTCIceConnectionState.connected; //} if (_remoteCandidates != null && !_remoteCandidates.Any(x => (x.address == remoteEndPoint.Address.ToString() || x.relatedAddress == remoteEndPoint.Address.ToString()) && (x.port == remoteEndPoint.Port || x.relatedPort == remoteEndPoint.Port))) { // This STUN request has come from a socket not in the remote ICE candidates list. Add it so we can send our STUN binding request to it. // RTCIceCandidate remoteIceCandidate = new IceCandidate("udp", remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.host); RTCIceCandidate peerRflxCandidate = new RTCIceCandidate(new RTCIceCandidateInit()); peerRflxCandidate.SetAddressProperties(RTCIceProtocol.udp, remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.prflx, null, 0); logger.LogDebug($"Adding peer reflex ICE candidate for {remoteEndPoint}."); _remoteCandidates.Add(peerRflxCandidate); // Some browsers require a STUN binding request from our end before the DTLS handshake will be initiated. // The STUN connectivity checks are already scheduled but we can speed things up by sending a binding // request immediately. SendStunConnectivityChecks(null); } } else if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingSuccessResponse) { if (ConnectionState != RTCIceConnectionState.connected) { logger.LogDebug($"ICE session setting connected remote end point to {remoteEndPoint}."); _connectedRemoteEndPoint = remoteEndPoint; ConnectionState = RTCIceConnectionState.connected; OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); } // TODO: What needs to be done here? //if (_turnServerEndPoint != null && remoteEndPoint.ToString() == _turnServerEndPoint.ToString()) //{ // if (iceCandidate.IsGatheringComplete == false) // { // var reflexAddressAttribute = stunMessage.Attributes.FirstOrDefault(y => y.AttributeType == STUNv2AttributeTypesEnum.XORMappedAddress) as STUNv2XORAddressAttribute; // if (reflexAddressAttribute != null) // { // iceCandidate.StunRflxIPEndPoint = new IPEndPoint(reflexAddressAttribute.Address, reflexAddressAttribute.Port); // iceCandidate.IsGatheringComplete = true; // logger.LogDebug("ICE gathering complete for local socket " + iceCandidate.RtpChannel.RTPLocalEndPoint + ", rflx address " + iceCandidate.StunRflxIPEndPoint + "."); // } // else // { // iceCandidate.IsGatheringComplete = true; // logger.LogDebug("The STUN binding response received on " + iceCandidate.RtpChannel.RTPLocalEndPoint + " from " + remoteEndPoint + " did not have an XORMappedAddress attribute, rlfx address can not be determined."); // } // } //} //else //{ // iceCandidate.LastStunResponseReceivedAt = DateTime.Now; // if (iceCandidate.IsStunLocalExchangeComplete == false) // { // iceCandidate.IsStunLocalExchangeComplete = true; // logger.LogDebug("WebRTC client STUN exchange complete for call " + CallID + ", candidate local socket " + iceCandidate.RtpChannel.RTPLocalEndPoint + ", remote socket " + remoteEndPoint + "."); // SetIceConnectionState(IceConnectionStatesEnum.Connected); // } //} } else if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingErrorResponse) { logger.LogWarning($"A STUN binding error response was received from {remoteEndPoint}."); } else { logger.LogWarning($"An unrecognised STUN request was received from {remoteEndPoint}."); } }
/// <summary> /// Processes a received STUN request or response. /// </summary> /// <param name="stunMessage">The STUN message received.</param> /// <param name="remoteEndPoint">The remote end point the STUN packet was received from.</param> public void ProcessStunMessage(STUNv2Message stunMessage, IPEndPoint remoteEndPoint) { remoteEndPoint = (!remoteEndPoint.Address.IsIPv4MappedToIPv6) ? remoteEndPoint : new IPEndPoint(remoteEndPoint.Address.MapToIPv4(), remoteEndPoint.Port); //logger.LogDebug($"STUN message received from remote {remoteEndPoint} {stunMessage.Header.MessageType}."); if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingRequest) { // TODO: The integrity check method needs to be implemented (currently just returns true). bool result = stunMessage.CheckIntegrity(System.Text.Encoding.UTF8.GetBytes(LocalIcePassword), LocalIceUser, RemoteIceUser); if (!result) { // Send STUN error response. STUNv2Message stunErrResponse = new STUNv2Message(STUNv2MessageTypesEnum.BindingErrorResponse); stunErrResponse.Header.TransactionId = stunMessage.Header.TransactionId; _rtpChannel.SendAsync(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunErrResponse.ToByteBuffer(null, false)); } else { var matchingCandidate = (_remoteCandidates != null) ? _remoteCandidates.Where(x => x.IsEquivalentEndPoint(RTCIceProtocol.udp, remoteEndPoint)).FirstOrDefault() : null; if (matchingCandidate == null) { // This STUN request has come from a socket not in the remote ICE candidates list. // Add a new remote peer reflexive candidate. RTCIceCandidate peerRflxCandidate = new RTCIceCandidate(new RTCIceCandidateInit()); peerRflxCandidate.SetAddressProperties(RTCIceProtocol.udp, remoteEndPoint.Address, (ushort)remoteEndPoint.Port, RTCIceCandidateType.prflx, null, 0); logger.LogDebug($"Adding peer reflex ICE candidate for {remoteEndPoint}."); _remoteCandidates.Add(peerRflxCandidate); UpdateChecklist(peerRflxCandidate); matchingCandidate = peerRflxCandidate; } // Find the checklist entry for this remote candidate and update its status. var matchingChecklistEntry = _checklist.Where(x => x.RemoteCandidate.foundation == matchingCandidate.foundation).FirstOrDefault(); if (matchingChecklistEntry == null) { logger.LogWarning("ICE session STUN request matched a remote candidate but NOT a checklist entry."); } //else //{ // if (!IsController) // { // matchingChecklistEntry.State = ChecklistEntryState.Succeeded; // } //} // 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. if (stunMessage.Attributes.Any(x => x.AttributeType == STUNv2AttributeTypesEnum.UseCandidate)) { if (ConnectionState != RTCIceConnectionState.connected) { // If we are the "controlled" agent and get a "use candidate" attribute that sets the matching candidate as nominated // as per https://tools.ietf.org/html/rfc8445#section-7.3.1.5. if (matchingChecklistEntry == null) { logger.LogWarning("ICE session STUN request had UseCandidate set but no matching checklist entry was found."); } else { logger.LogDebug($"ICE session remote peer nominated entry from binding request: {matchingChecklistEntry.RemoteCandidate}"); SetNominatedEntry(matchingChecklistEntry); } } } STUNv2Message stunResponse = new STUNv2Message(STUNv2MessageTypesEnum.BindingSuccessResponse); stunResponse.Header.TransactionId = stunMessage.Header.TransactionId; stunResponse.AddXORMappedAddressAttribute(remoteEndPoint.Address, remoteEndPoint.Port); string localIcePassword = LocalIcePassword; byte[] stunRespBytes = stunResponse.ToByteBufferStringKey(localIcePassword, true); _rtpChannel.SendAsync(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunRespBytes); } } else if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingSuccessResponse) { // Correlate with request using transaction ID as per https://tools.ietf.org/html/rfc8445#section-7.2.5. // Actions to take on a successful STUN response https://tools.ietf.org/html/rfc8445#section-7.2.5.3 // - Discover peer reflexive remote candidates // (TODO: According to https://tools.ietf.org/html/rfc8445#section-7.2.5.3.1 peer reflexive get added to the local candidates list?) // - Construct a valid pair which means match a candidate pair in the check list and mark it as valid (since a successful STUN exchange // has now taken place on it). A new entry may need to be created for this pair since peer reflexive candidates are not added to the connectivity // check checklist. // - Update state of candidate pair that generated the check to Succeeded. // - If the controlling candidate set the USE_CANDIDATE attribute then the ICE agent that receives the successful response sets the nominated // flag of the pair to true. Once the nominated flag is set it concludes the ICE processing for that component. if (_checklistState == ChecklistState.Running) { string txID = Encoding.ASCII.GetString(stunMessage.Header.TransactionId); // Attempt to find the checklist entry for this transaction ID. var matchingChecklistEntry = _checklist.Where(x => x.RequestTransactionID == txID).FirstOrDefault(); if (matchingChecklistEntry == null) { logger.LogWarning("ICE session STUN response transaction ID did not match a checklist entry."); } else { matchingChecklistEntry.State = ChecklistEntryState.Succeeded; if (matchingChecklistEntry.Nominated) { logger.LogDebug($"ICE session remote peer nominated entry from binding response: {matchingChecklistEntry.RemoteCandidate}"); // This is the response to a connectivity check that had the "UseCandidate" attribute set. SetNominatedEntry(matchingChecklistEntry); } else if (this.IsController && !_checklist.Any(x => x.Nominated)) { // If we are the controlling ICE agent it's up to us to decide when to nominate a candidate pair to use for the connection. // To start with we'll just use whichever pair gets the first successful STUN exchange. If needs be the selection algorithm can // improve over time. matchingChecklistEntry.ChecksSent = 0; matchingChecklistEntry.LastCheckSentAt = DateTime.MinValue; matchingChecklistEntry.Nominated = true; SendConnectivityCheck(matchingChecklistEntry, true); } } } } else if (stunMessage.Header.MessageType == STUNv2MessageTypesEnum.BindingErrorResponse) { logger.LogWarning($"A STUN binding error response was received from {remoteEndPoint}."); // Attempt to find the checklist entry for this transaction ID. string txID = Encoding.ASCII.GetString(stunMessage.Header.TransactionId); var matchingChecklistEntry = _checklist.Where(x => x.RequestTransactionID == txID).FirstOrDefault(); if (matchingChecklistEntry == null) { logger.LogWarning("ICE session STUN error response transaction ID did not match a checklist entry."); } else { logger.LogWarning($"ICE session check list entry set to failed: {matchingChecklistEntry.RemoteCandidate}"); matchingChecklistEntry.State = ChecklistEntryState.Failed; } } else { logger.LogWarning($"An unrecognised STUN request was received from {remoteEndPoint}."); } }