Repository navigation
Expand file tree
/
Copy pathDNSClient.cs
More file actions
280 lines (244 loc) · 12.1 KB
/
Copy pathDNSClient.cs
File metadata and controls
280 lines (244 loc) · 12.1 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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
using DnsClient;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using log4net;
namespace NETworkManager.Utilities;
public class DNSClient : SingletonBase<DNSClient>
{
private static readonly ILog Log = LogManager.GetLogger(typeof(DNSClient));
/// <summary>
/// Error message which is returned when the DNS client is not configured.
/// </summary>
private const string NotConfiguredMessage = "DNS client is not configured. Call Configure() first.";
/// <summary>
/// Hold the current instance of the LookupClient.
/// </summary>
private LookupClient _client;
/// <summary>
/// Indicates if the DNS client is configured.
/// </summary>
private bool _isConfigured;
/// <summary>
/// Store the current DNS settings.
/// </summary>
private DNSClientSettings _settings;
/// <summary>
/// Method to configure the DNS client.
/// </summary>
/// <param name="settings"></param>
public void Configure(DNSClientSettings settings)
{
_settings = settings;
Log.Debug("Configure - Configuring DNS client...");
if (_settings.UseCustomDNSServers)
{
Log.Debug("Configure - Using custom DNS servers...");
// Setup custom DNS servers
List<NameServer> servers = [];
foreach (var (server, port) in _settings.DNSServers)
{
Log.Debug($"Configure - Adding custom DNS server: {server}:{port}");
servers.Add(new IPEndPoint(IPAddress.Parse(server), port));
}
Log.Debug("Configure - Creating LookupClient with custom DNS servers...");
_client = new LookupClient(new LookupClientOptions(servers.ToArray()));
}
else
{
Log.Debug("Configure - Creating LookupClient with Windows default DNS servers...");
_client = new LookupClient();
}
Log.Debug("Configure - DNS client configured.");
_isConfigured = true;
}
/// <summary>
/// Method to update the (Windows) name servers of the DNS client
/// when they may have changed due to a network update.
/// </summary>
public void UpdateWindowsDNSSever()
{
Log.Debug("UpdateWindowsDNSSever - Recreating LookupClient with with Windows default DNS servers...");
_client = new LookupClient();
}
/// <summary>
/// Resolve an IPv4 address from a hostname or FQDN.
/// </summary>
/// <param name="query">Hostname or FQDN as string like "example.com".</param>
/// <returns><see cref="IPAddress" /> of the host.</returns>
public async Task<DNSClientResultIPAddress> ResolveAAsync(string query)
{
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);
try
{
var result = await _client.QueryAsync(query, QueryType.A);
// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
// treat it as "no record". SERVFAIL/REFUSED are not included here: for a forward lookup
// they mean the resolver actually failed or refused the query, not "record does not exist".
if (result.HasError)
return new DNSClientResultIPAddress(result.HasError, result.ErrorMessage, $"{result.NameServer}")
{ IsNotFound = IsNotFoundResponseCode(result.Header.ResponseCode) };
// Validate result because of https://github.com/BornToBeRoot/NETworkManager/issues/1934
var record = result.Answers.ARecords().FirstOrDefault();
return record != null
? new DNSClientResultIPAddress(record.Address, $"{result.NameServer}")
: new DNSClientResultIPAddress(true,
$"IP address for \"{query}\" could not be resolved and the DNS server did not return an error. Try to check your DNS server with: dig @{result.NameServer.Address} {query}",
$"{result.NameServer}")
{ IsNotFound = true };
}
catch (DnsResponseException ex)
{
return new DNSClientResultIPAddress(true, ex.Message);
}
catch (Exception ex)
{
Log.Error($"Error while resolving A record (Query string is \"{query}\".", ex);
return new DNSClientResultIPAddress(true, ex.Message);
}
}
/// <summary>
/// Resolve an IPv6 address from a hostname or FQDN.
/// </summary>
/// <param name="query">Hostname or FQDN as string like "example.com".</param>
/// <returns><see cref="IPAddress" /> of the host.</returns>
public async Task<DNSClientResultIPAddress> ResolveAaaaAsync(string query)
{
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);
try
{
var result = await _client.QueryAsync(query, QueryType.AAAA);
// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
// treat it as "no record". SERVFAIL/REFUSED are not included here: for a forward lookup
// they mean the resolver actually failed or refused the query, not "record does not exist".
if (result.HasError)
return new DNSClientResultIPAddress(result.HasError, result.ErrorMessage, $"{result.NameServer}")
{ IsNotFound = IsNotFoundResponseCode(result.Header.ResponseCode) };
// Validate result because of https://github.com/BornToBeRoot/NETworkManager/issues/1934
var record = result.Answers.AaaaRecords().FirstOrDefault();
return record != null
? new DNSClientResultIPAddress(record.Address, $"{result.NameServer}")
: new DNSClientResultIPAddress(true,
$"IP address for \"{query}\" could not be resolved and the DNS server did not return an error. Try to check your DNS server with: dig @{result.NameServer.Address} {query}",
$"{result.NameServer}")
{ IsNotFound = true };
}
catch (DnsResponseException ex)
{
return new DNSClientResultIPAddress(true, ex.Message);
}
catch (Exception ex)
{
Log.Error($"Error while resolving AAAA record (Query string is \"{query}\".", ex);
return new DNSClientResultIPAddress(true, ex.Message);
}
}
/// <summary>
/// Resolve a CNAME from a hostname or FQDN.
/// </summary>
/// <param name="query">Hostname or FQDN as string like "example.com".</param>
/// <returns>CNAME of the host.</returns>
public async Task<DNSClientResultString> ResolveCnameAsync(string query)
{
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);
try
{
var result = await _client.QueryAsync(query, QueryType.CNAME);
// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is not a real failure like a timeout - flag it via IsNotFound so callers can
// treat it as "no record". SERVFAIL/REFUSED are not included here: for a forward lookup
// they mean the resolver actually failed or refused the query, not "record does not exist".
if (result.HasError)
return new DNSClientResultString(result.HasError, result.ErrorMessage, $"{result.NameServer}")
{ IsNotFound = IsNotFoundResponseCode(result.Header.ResponseCode) };
// Validate result because of https://github.com/BornToBeRoot/NETworkManager/issues/1934
var record = result.Answers.CnameRecords().FirstOrDefault();
return record != null
? new DNSClientResultString(record.CanonicalName, $"{result.NameServer}")
: new DNSClientResultString(true,
$"CNAME for \"{query}\" could not be resolved and the DNS server did not return an error. Try to check your DNS server with: dig @{result.NameServer.Address} {query}",
$"{result.NameServer}")
{ IsNotFound = true };
}
catch (DnsResponseException ex)
{
return new DNSClientResultString(true, ex.Message);
}
catch (Exception ex)
{
Log.Error($"Error while resolving CNAME record (Query string is \"{query}\".", ex);
return new DNSClientResultString(true, ex.Message);
}
}
/// <summary>
/// Resolve a PTR for an IP address.
/// </summary>
/// <param name="ipAddress">IP address of the host.</param>
/// <returns>PTR domain name.</returns>
public async Task<DNSClientResultString> ResolvePtrAsync(IPAddress ipAddress)
{
if (!_isConfigured)
throw new DNSClientNotConfiguredException(NotConfiguredMessage);
try
{
var result = await _client.QueryReverseAsync(ipAddress);
// Pass the error we got from the lookup client (dns server).
// NXDOMAIN is always a clean "no record". For private/ULA IP ranges (the common case for
// router/computer PTR lookups) many resolvers also return SERVFAIL/REFUSED instead of a
// clean NXDOMAIN when there is no reverse zone, so those are treated as "not found" too -
// but only for private ranges, since for a public IP a SERVFAIL/REFUSED usually means the
// resolver actually failed or refused the query.
if (result.HasError)
return new DNSClientResultString(result.HasError, result.ErrorMessage, $"{result.NameServer}")
{
IsNotFound = IsNotFoundResponseCode(result.Header.ResponseCode,
IPAddressHelper.IsPrivateIPAddress(ipAddress))
};
// Validate result because of https://github.com/BornToBeRoot/NETworkManager/issues/1934
var record = result.Answers.PtrRecords().FirstOrDefault();
return record != null
? new DNSClientResultString(record.PtrDomainName, $"{result.NameServer}")
: new DNSClientResultString(true,
$"PTR for \"{ipAddress}\" could not be resolved and the DNS server did not return an error. Try to check your DNS server with: dig @{result.NameServer.Address} -x {ipAddress}",
$"{result.NameServer}")
{ IsNotFound = true };
}
catch (DnsResponseException ex)
{
return new DNSClientResultString(true, ex.Message);
}
catch (Exception ex)
{
Log.Error($"Error while resolving PTR record (IP address is \"{ipAddress}\".", ex);
return new DNSClientResultString(true, ex.Message);
}
}
/// <summary>
/// Determines whether a DNS response code means "no record" rather than a real failure.
/// NXDOMAIN is always a clean "does not exist". SERVFAIL and REFUSED are only treated as
/// "no record" when <paramref name="treatServerErrorsAsNotFound" /> is set, since outside of
/// that case they usually indicate the resolver actually failed or refused the query rather
/// than a confirmed absence of the record.
/// </summary>
/// <param name="responseCode">The DNS response code to check.</param>
/// <param name="treatServerErrorsAsNotFound">
/// Whether SERVFAIL/REFUSED should also count as "no record" - true for reverse (PTR) lookups
/// on private/RFC1918/ULA IP ranges, where many resolvers return them instead of a clean
/// NXDOMAIN when there is no reverse zone.
/// </param>
private static bool IsNotFoundResponseCode(DnsHeaderResponseCode responseCode,
bool treatServerErrorsAsNotFound = false)
{
if (responseCode is DnsHeaderResponseCode.NotExistentDomain)
return true;
return treatServerErrorsAsNotFound
&& responseCode is DnsHeaderResponseCode.ServerFailure or DnsHeaderResponseCode.Refused;
}
}