forked from sipsorcery-org/sipsorcery
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathIceServerResolver.cs
More file actions
182 lines (156 loc) · 6.76 KB
/
IceServerResolver.cs
File metadata and controls
182 lines (156 loc) · 6.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//-----------------------------------------------------------------------------
// Filename: IceServerResolver.cs
//
// Description: Resolves A list of STUN/TURN servers into a convenient form.
//
// Author(s):
// Aaron Clauson (aaron@sipsorcery.com)
//
// History:
// 24 May 2025 Aaron Clauson Refactored from RtpIceChannel.
//
// License:
// BSD 3-Clause "New" or "Revised" License and the additional
// BDS BY-NC-SA restriction, see included LICENSE.md file.
//-----------------------------------------------------------------------------
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using SIPSorcery.Sys;
namespace SIPSorcery.Net;
public class IceServerResolver
{
private static readonly ILogger logger = LogFactory.CreateLogger<IceServerResolver>();
private ConcurrentDictionary<STUNUri, IceServer> _iceServers = new();
public IReadOnlyDictionary<STUNUri, IceServer> IceServers => new ReadOnlyDictionary<STUNUri, IceServer>(_iceServers);
public IceServerResolver()
{ }
/// <summary>
/// Initialises the ICE servers if any were provided in the initial configuration.
/// ICE servers are STUN and TURN servers and are used to gather "server reflexive"
/// and "relay" candidates. If the transport policy is "relay only" then only TURN
/// servers will be added to the list of ICE servers being checked.
/// </summary>
/// <remarks>See https://tools.ietf.org/html/rfc8445#section-5.1.1.2</remarks>
public void InitialiseIceServers(
List<RTCIceServer> iceServers,
RTCIceTransportPolicy policy)
{
if(iceServers == null || iceServers.Count == 0)
{
logger.LogDebug("{caller} no ICE servers provided.", nameof(IceServerResolver));
return;
}
int iceServerID = IceServer.MINIMUM_ICE_SERVER_ID;
_iceServers.Clear();
foreach (var cfg in iceServers)
{
foreach (var rawUrl in cfg.urls.Split([ ',' ], IceServer.MAXIMUM_ICE_SERVER_ID + 1, StringSplitOptions.RemoveEmptyEntries))
{
if (!STUNUri.TryParse(rawUrl.Trim(), out var stunUri))
{
logger.LogWarning("{caller} could not parse ICE server URL {url}", nameof(IceServerResolver), rawUrl);
continue;
}
// Filter out policy excluded entries (TURNS/STUNS are now supported via TLS)
if (policy == RTCIceTransportPolicy.relay && stunUri.Scheme == STUNSchemesEnum.stun)
{
logger.LogWarning("{caller} ignoring ICE server {stunUri} (scheme {scheme})", nameof(IceServerResolver), stunUri, stunUri.Scheme);
continue;
}
// Avoid deplicates.
if (_iceServers.ContainsKey(stunUri))
{
continue;
}
var server = new IceServer(stunUri, iceServerID++, cfg.username, cfg.credential);
// immediate bind if it’s already an IP
if (IPAddress.TryParse(stunUri.Host, out var ip))
{
server.ServerEndPoint = new IPEndPoint(ip, stunUri.Port);
logger.LogDebug("{caller} bound {Uri} -> {EndPoint}", nameof(IceServerResolver), stunUri, server.ServerEndPoint);
}
_iceServers[stunUri] = server;
if (server.ServerEndPoint == null)
{
// Kick off DNS in background, passing the key so we can update the map.
ScheduleDnsLookup(stunUri, server);
}
if (iceServerID > IceServer.MAXIMUM_ICE_SERVER_ID)
{
logger.LogWarning("{caller} reached max ICE server count", nameof(IceServerResolver));
break;
}
}
}
}
private void ScheduleDnsLookup(STUNUri key, IceServer server)
{
if (server.DnsLookupSentAt != DateTime.MinValue)
{
return;
}
if (_iceServers.ContainsKey(key))
{
// NOTE: must use DateTime.Now (not UtcNow) because RtpIceChannel.CheckIceServers
// measures the elapsed time with DateTime.Now.Subtract(DnsLookupSentAt). Using
// UtcNow here makes the timeout fire ~immediately in any non-UTC timezone (the
// sign of the local offset determines whether the channel gives up before DNS
// resolves or never times out at all).
_iceServers[key].DnsLookupSentAt = DateTime.Now;
}
logger.LogDebug("{caller} starting DNS lookup for ICE server {Uri}", nameof(IceServerResolver), key);
server.DnsResolutionTask = Task.Run(async () =>
{
try
{
var resolveTask = STUNDns.Resolve(key);
var timeout = Task.Delay(TimeSpan.FromSeconds(IceServer.DNS_LOOKUP_TIMEOUT_SECONDS));
var winner = await Task.WhenAny(resolveTask, timeout).ConfigureAwait(false);
if (winner == resolveTask)
{
var ep = await resolveTask.ConfigureAwait(false);
server.ServerEndPoint = ep;
logger.LogDebug("{caller} resolved {Uri} -> {EndPoint}", nameof(IceServerResolver), key, ep);
}
else
{
server.Error = SocketError.TimedOut;
logger.LogWarning("{caller} DNS lookup timed out for {Uri}", nameof(IceServerResolver), key);
}
}
catch (Exception ex)
{
server.Error = SocketError.HostNotFound;
logger.LogWarning(ex, "{caller} DNS resolution failed for {Uri}", nameof(IceServerResolver), key);
}
_iceServers[key] = server;
});
}
/// <summary>
/// Wait until all ICE servers have resolved or timed out. Optional timeout.
/// </summary>
public async Task WaitForAllIceServersAsync(TimeSpan? timeout = null)
{
var tasks = _iceServers.Values
.Select(s => s.DnsResolutionTask ?? Task.CompletedTask)
.ToArray();
var all = Task.WhenAll(tasks);
if (timeout.HasValue)
{
if (await Task.WhenAny(all, Task.Delay(timeout.Value)).ConfigureAwait(false) != all)
{
throw new TimeoutException(
$"Timed out waiting {timeout.Value} for ICE server DNS resolutions");
}
}
// propagate any resolution exception
await all.ConfigureAwait(false);
}
}