/// <summary> /// Performs a connectivity check for a single candidate pair entry. /// </summary> /// <param name="candidatePair">The candidate pair to perform a connectivity check for.</param> /// <param name="setUseCandidate">If true indicates we are acting as the "controlling" ICE agent /// and are nominating this candidate as the chosen one.</param> /// <remarks>As specified in https://tools.ietf.org/html/rfc8445#section-7.2.4.</remarks> private void SendConnectivityCheck(ChecklistEntry candidatePair, bool setUseCandidate) { candidatePair.State = ChecklistEntryState.InProgress; candidatePair.LastCheckSentAt = DateTime.Now; candidatePair.ChecksSent++; candidatePair.RequestTransactionID = Crypto.GetRandomString(STUNv2Header.TRANSACTION_ID_LENGTH); IPEndPoint remoteEndPoint = candidatePair.RemoteCandidate.GetEndPoint(); logger.LogDebug($"Sending ICE connectivity check from {_rtpChannel.RTPLocalEndPoint} to {remoteEndPoint} (use candidate {setUseCandidate})."); STUNv2Message stunRequest = new STUNv2Message(STUNv2MessageTypesEnum.BindingRequest); stunRequest.Header.TransactionId = Encoding.ASCII.GetBytes(candidatePair.RequestTransactionID); stunRequest.AddUsernameAttribute(RemoteIceUser + ":" + LocalIceUser); stunRequest.Attributes.Add(new STUNv2Attribute(STUNv2AttributeTypesEnum.Priority, BitConverter.GetBytes(candidatePair.Priority))); if (setUseCandidate) { stunRequest.Attributes.Add(new STUNv2Attribute(STUNv2AttributeTypesEnum.UseCandidate, null)); } byte[] stunReqBytes = stunRequest.ToByteBufferStringKey(RemoteIcePassword, true); _rtpChannel.SendAsync(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunReqBytes); }
/// <summary> /// Sets the nominated checklist entry. This action completes the checklist processing and /// indicates the connection checks were successful. /// </summary> /// <param name="entry">The checklist entry that was nominated.</param> private void SetNominatedEntry(ChecklistEntry entry) { entry.Nominated = true; _checklistState = ChecklistState.Completed; NominatedCandidate = entry.RemoteCandidate; ConnectionState = RTCIceConnectionState.connected; OnIceConnectionStateChange?.Invoke(RTCIceConnectionState.connected); }
/// <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> /// Performs a connectivity check for a single candidate pair entry. /// </summary> /// <param name="candidatePair">The candidate pair to perform a connectivity check for.</param> /// <remarks>As specified in https://tools.ietf.org/html/rfc8445#section-7.2.4.</remarks> private void DoConnectivityCheck(ChecklistEntry candidatePair) { candidatePair.State = ChecklistEntryState.InProgress; IPAddress remoteAddress = IPAddress.Parse(candidatePair.RemoteCandidate.address); IPEndPoint remoteEndPoint = new IPEndPoint(remoteAddress, candidatePair.RemoteCandidate.port); logger.LogDebug($"Sending ICE connectivity check from {_rtpChannel.RTPLocalEndPoint} to {remoteEndPoint}."); string localUser = LocalIceUser; STUNv2Message stunRequest = new STUNv2Message(STUNv2MessageTypesEnum.BindingRequest); stunRequest.Header.TransactionId = Guid.NewGuid().ToByteArray().Take(12).ToArray(); stunRequest.AddUsernameAttribute(RemoteIceUser + ":" + localUser); stunRequest.Attributes.Add(new STUNv2Attribute(STUNv2AttributeTypesEnum.Priority, new byte[] { 0x6e, 0x7f, 0x1e, 0xff })); stunRequest.Attributes.Add(new STUNv2Attribute(STUNv2AttributeTypesEnum.UseCandidate, null)); byte[] stunReqBytes = stunRequest.ToByteBufferStringKey(RemoteIcePassword, true); _rtpChannel.SendAsync(RTPChannelSocketsEnum.RTP, remoteEndPoint, stunReqBytes); //localIceCandidate.LastSTUNSendAt = DateTime.Now; }
/// <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> /// Attempts to add a checklist entry. If there is already an equivalent entry in the checklist /// the entry may not be added or may replace an existing entry. /// </summary> /// <param name="entry">The new entry to attempt to add to the checklist.</param> private void AddChecklistEntry(ChecklistEntry entry) { // Check if there is already an entry that matches the remote candidate. // Note: The implementation in this class relies binding the socket used for all // local candidates on a SINGLE address (typically 0.0.0.0 or [::]). Consequently // there is no need to check the local candidate when determining duplicates. As long // as there is one checklist entry with each remote candidate the connectivity check will // work. To put it another way the local candidate information is not used on the // "Nominated" pair. var entryRemoteEP = entry.RemoteCandidate.GetEndPoint(); var existingEntry = _checklist.Where(x => x.RemoteCandidate.GetEndPoint().Address.Equals(entryRemoteEP.Address) && x.RemoteCandidate.GetEndPoint().Port == entryRemoteEP.Port && x.RemoteCandidate.protocol == entry.RemoteCandidate.protocol).SingleOrDefault(); if (existingEntry != null) { if (entry.Priority > existingEntry.Priority) { logger.LogDebug($"Removing lower priority entry and adding candidate pair to checklist for: {entry.RemoteCandidate}"); _checklist.Remove(existingEntry); _checklist.Add(entry); } else { logger.LogDebug($"Existing checklist entry has higher priority, NOT adding entry for: {entry.RemoteCandidate}"); } } else { // No existing entry. logger.LogDebug($"Adding new candidate pair to checklist for: {entry.RemoteCandidate}"); _checklist.Add(entry); } }