Exemple #1
0
        /// <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);
            }
        }
Exemple #2
0
        /// <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);
        }
Exemple #3
0
        /// <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}.");
            }
        }
Exemple #7
0
        /// <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);
                }
            }
        }
Exemple #8
0
 /// <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.");
     }
 }
Exemple #9
0
        /// <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.");
            }
        }
Exemple #10
0
        /// <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()}");
            }
        }
Exemple #11
0
        /// <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);
        }
Exemple #12
0
        /// <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);
                }
            }
        }
Exemple #13
0
        /// <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);
                }
            }
        }
Exemple #14
0
        /// <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);
        }
Exemple #15
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}.");
            }
        }
Exemple #16
0
        /// <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}.");
            }
        }