using DnsClient.Protocol.Options; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace DnsClient { /// /// The is the main query class of this library and should be used for any kind of DNS lookup query. /// /// It implements and which contains a number of extension methods, too. /// The extension methods internally all invoke the standard queries though. /// /// /// /// /// /// A basic example wihtout specifying any DNS server, which will use the DNS server configured by your local network. /// /// /// /// public class LookupClient : ILookupClient, IDnsQuery { private static readonly int s_serverHealthCheckInterval = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; private static int _uniqueId = 0; private readonly ResponseCache _cache; private readonly DnsMessageHandler _messageHandler; private readonly DnsMessageHandler _tcpFallbackHandler; private readonly Random _random = new Random(); private bool _healthCheckRunning = false; private int _lastHealthCheck = 0; // for backward compat /// /// Gets the list of configured name servers. /// public IReadOnlyCollection NameServers => Settings.NameServers; // TODO: make readonly when obsolete stuff is removed /// /// Gets the settings. /// public LookupClientSettings Settings { get; private set; } #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public TimeSpan? MinimumCacheTimeout { get => Settings.MinimumCacheTimeout; set { if (Settings.MinimumCacheTimeout != value) { Settings = Settings.WithMinimumCacheTimeout(value); } } } public bool UseTcpFallback { get => Settings.UseTcpFallback; set { if (Settings.UseTcpFallback != value) { Settings = Settings.WithUseTcpFallback(value); } } } public bool UseTcpOnly { get => Settings.UseTcpOnly; set { if (Settings.UseTcpOnly != value) { Settings = Settings.WithUseTcpOnly(value); } } } public bool EnableAuditTrail { get => Settings.EnableAuditTrail; set { if (Settings.EnableAuditTrail != value) { Settings = Settings.WithEnableAuditTrail(value); } } } public bool Recursion { get => Settings.Recursion; set { if (Settings.Recursion != value) { Settings = Settings.WithRecursion(value); } } } public int Retries { get => Settings.Retries; set { if (Settings.Retries != value) { Settings = Settings.WithRetries(value); } } } public bool ThrowDnsErrors { get => Settings.ThrowDnsErrors; set { if (Settings.ThrowDnsErrors != value) { Settings = Settings.WithThrowDnsErrors(value); } } } public TimeSpan Timeout { get => Settings.Timeout; set { if (Settings.Timeout != value) { Settings = Settings.WithTimeout(value); } } } public bool UseCache { //TODO: change logic with options/settings - UseCache is just a setting, cache can still be enabled get => Settings.UseCache; set { if (Settings.UseCache != value) { Settings = Settings.WithUseCache(value); } } } public bool UseRandomNameServer { get; set; } = true; public bool ContinueOnDnsError { get => Settings.ContinueOnDnsError; set { if (Settings.ContinueOnDnsError != value) { Settings = Settings.WithContinueOnDnsError(value); } } } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member internal ResponseCache ResponseCache => _cache; /// /// Creates a new instance of without specifying any name server. /// This will implicitly use the name server(s) configured by the local network adapter(s). /// /// /// This uses . /// The resulting list of name servers is highly dependent on the local network configuration and OS. /// /// /// In the following example, we will create a new without explicitly defining any DNS server. /// This will use the DNS server configured by your local network. /// /// /// /// public LookupClient() : this(new LookupClientOptions(resolveNameServers: true)) { } /// /// Creates a new instance of with default settings and one or more DNS servers identified by their . /// The default port 53 will be used for all s provided. /// /// The (s) to be used by this instance. /// /// Connecting to one or more DNS server using the default port: /// /// /// /// public LookupClient(params IPAddress[] nameServers) : this(new LookupClientOptions(nameServers)) { } /// /// Create a new instance of with default settings and one DNS server defined by and . /// /// The of the DNS server. /// The port of the DNS server. /// /// Connecting to one specific DNS server which does not run on the default port 53: /// /// /// /// public LookupClient(IPAddress address, int port) : this(new LookupClientOptions(new[] { new NameServer(address, port) })) { } /// /// Creates a new instance of with default settings and the given name servers. /// /// The (s) to be used by this instance. /// /// Connecting to one specific DNS server which does not run on the default port 53: /// /// /// /// /// The class also contains pre defined s for the public google DNS servers, which can be used as follows: /// /// /// /// /// public LookupClient(params IPEndPoint[] nameServers) : this(new LookupClientOptions(nameServers)) { } /// /// Creates a new instance of with default settings and the given name servers. /// /// The (s) to be used by this instance. public LookupClient(params NameServer[] nameServers) : this(new LookupClientOptions(nameServers)) { } /// /// Creates a new instance of with custom settings. /// /// The options to use with this instance. public LookupClient(LookupClientOptions options) : this(options, null, null) { } internal LookupClient(LookupClientOptions options, DnsMessageHandler udpHandler = null, DnsMessageHandler tcpHandler = null) { Settings = options ?? throw new ArgumentNullException(nameof(options)); // TODO: revisit, do we need this check? Maybe throw on query instead, in case no default name servers nor the per query settings have any defined. ////if (Settings.NameServers == null || Settings.NameServers.Count == 0) ////{ //// throw new ArgumentException("At least one name server must be configured.", nameof(options)); ////} _messageHandler = udpHandler ?? new DnsUdpMessageHandler(true); _tcpFallbackHandler = tcpHandler ?? new DnsTcpMessageHandler(); _cache = new ResponseCache(true, Settings.MinimumCacheTimeout); } /// /// Does a reverse lookup for the . /// /// The . /// /// The which might contain the for the . /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => Query(GetReverseQuestion(ipAddress)); /// /// Does a reverse lookup for the . /// /// The . /// Query options to be used instead of 's settings. /// /// The which might contain the for the . /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryOptions queryOptions) => Query(GetReverseQuestion(ipAddress), queryOptions); /// /// Does a reverse lookup for the . /// /// The . /// The cancellation token. /// /// The which might contain the for the . /// /// If is null. /// If cancellation has been requested for the passed in . /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => QueryAsync(GetReverseQuestion(ipAddress), cancellationToken); /// /// Does a reverse lookup for the . /// /// The . /// Query options to be used instead of 's settings. /// The cancellation token. /// /// The which might contain the for the . /// /// If is null. /// If cancellation has been requested for the passed in . /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => QueryAsync(GetReverseQuestion(ipAddress), queryOptions, cancellationToken); /// /// Performs a DNS lookup for the given , and . /// /// The domain name query. /// The . /// The . /// Query options to be used instead of 's settings. /// /// The which contains the response headers and lists of resource records. /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. /// /// The behavior of the query can be controlled by default settings of this instance or via . /// for example can be disabled and would instruct the DNS server to return no additional records. /// public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, DnsQueryOptions queryOptions = null) { var question = new DnsQuestion(query, queryType, queryClass); if (queryOptions == null) { return QueryInternal(question, Settings); } return Query(question, queryOptions); } /// /// Performs a DNS lookup for the given . /// /// The domain name query. /// /// The which contains the response headers and lists of resource records. /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse Query(DnsQuestion question) => QueryInternal(question, Settings); /// /// Performs a DNS lookup for the given . /// /// The domain name query. /// Query options to be used instead of 's settings. /// /// The which contains the response headers and lists of resource records. /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse Query(DnsQuestion question, DnsQueryOptions queryOptions) { if (queryOptions == null) { throw new ArgumentNullException(nameof(queryOptions)); } DnsQuerySettings settings; if (queryOptions.NameServers.Count == 0 && queryOptions.AutoResolvedNameServers == false) { // fallback to already configured nameservers in case none are specified. settings = new DnsQuerySettings(queryOptions, Settings.NameServers); } else { settings = new DnsQuerySettings(queryOptions); } return QueryInternal(question, settings); } /// /// Performs a DNS lookup for the given , and . /// /// The domain name query. /// The . /// The . /// The cancellation token. /// Query options to be used instead of 's settings. /// /// The which contains the response headers and lists of resource records. /// /// If is null. /// If cancellation has been requested for the passed in . /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. /// /// The behavior of the query can be controlled by default settings of this instance or via . /// for example can be disabled and would instruct the DNS server to return no additional records. /// public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, DnsQueryOptions queryOptions = null, CancellationToken cancellationToken = default) { var question = new DnsQuestion(query, queryType, queryClass); if (queryOptions == null) { return QueryInternalAsync(question, Settings, cancellationToken: cancellationToken); } return QueryAsync(question, queryOptions, cancellationToken); } /// /// Performs a DNS lookup for the given . /// /// The domain name query. /// The cancellation token. /// /// The which contains the response headers and lists of resource records. /// /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => QueryInternalAsync(question, Settings, cancellationToken: cancellationToken); /// /// Performs a DNS lookup for the given . /// /// The domain name query. /// Query options to be used instead of 's settings. /// The cancellation token. /// /// The which contains the response headers and lists of resource records. /// /// If or is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryAsync(DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) { if (queryOptions == null) { throw new ArgumentNullException(nameof(queryOptions)); } DnsQuerySettings settings; if (queryOptions.NameServers.Count == 0 && queryOptions.AutoResolvedNameServers == false) { // fallback to already configured nameservers in case none are specified. settings = new DnsQuerySettings(queryOptions, Settings.NameServers); } else { settings = new DnsQuerySettings(queryOptions); } return QueryInternalAsync(question, settings, cancellationToken: cancellationToken); } /// /// Performs a DNS lookup for the given , and /// using only the passed in . /// /// /// To query specific servers can be useful in cases where you have to use a different DNS server than initially configured /// (without creating a new instance of for example). /// /// The list of one or more server(s) which should be used for the lookup. /// The domain name query. /// The . /// The . /// /// The which contains the response headers and lists of resource records. /// /// If the collection doesn't contain any elements. /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) { if (servers == null) { throw new ArgumentNullException(nameof(servers)); } var question = new DnsQuestion(query, queryType, queryClass); return QueryInternal(question, Settings, servers); } /// /// Performs a DNS lookup for the given , and /// using only the passed in . /// /// /// To query specific servers can be useful in cases where you have to use a different DNS server than initially configured /// (without creating a new instance of for example). /// /// The list of one or more server(s) which should be used for the lookup. /// The domain name query. /// The . /// The . /// The cancellation token. /// /// The which contains the response headers and lists of resource records. /// /// If the collection doesn't contain any elements. /// If is null. /// If cancellation has been requested for the passed in . /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) { if (servers == null) { throw new ArgumentNullException(nameof(servers)); } var question = new DnsQuestion(query, queryType, queryClass); return QueryInternalAsync(question, Settings, servers, cancellationToken); } /// /// Does a reverse lookup for the /// using only the passed in . /// /// The list of one or more server(s) which should be used for the lookup. /// The . /// /// The which might contain the for the . /// /// If the collection doesn't contain any elements. /// If is null. /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) { if (servers == null) { throw new ArgumentNullException(nameof(servers)); } return QueryInternal(GetReverseQuestion(ipAddress), Settings, servers); } /// /// Does a reverse lookup for the /// using only the passed in . /// /// The list of one or more server(s) which should be used for the lookup. /// The . /// The cancellation token. /// /// The which might contain the for the . /// /// If the collection doesn't contain any elements. /// If is null. /// If cancellation has been requested for the passed in . /// After retries and fallbacks, if none of the servers were accessible, timed out or (if is enabled) returned error results. public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) { if (servers == null) { throw new ArgumentNullException(nameof(servers)); } return QueryInternalAsync(GetReverseQuestion(ipAddress), Settings, servers, cancellationToken); } private IDnsQueryResponse QueryInternal(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection useServers = null) { if (question == null) { throw new ArgumentNullException(nameof(question)); } var head = new DnsRequestHeader(GetNextUniqueId(), settings.Recursion, DnsOpCode.Query); var request = new DnsRequestMessage(head, question); var handler = settings.UseTcpOnly ? _tcpFallbackHandler : _messageHandler; var servers = useServers ?? settings.ShuffleNameServers(); if (servers.Count == 0) { throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); } return ResolveQuery(servers, settings, handler, request); } private Task QueryInternalAsync(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection useServers = null, CancellationToken cancellationToken = default) { if (question == null) { throw new ArgumentNullException(nameof(question)); } if (settings == null) { throw new ArgumentNullException(nameof(settings)); } var head = new DnsRequestHeader(GetNextUniqueId(), settings.Recursion, DnsOpCode.Query); var request = new DnsRequestMessage(head, question); var handler = settings.UseTcpOnly ? _tcpFallbackHandler : _messageHandler; var servers = useServers ?? settings.ShuffleNameServers(); if (servers.Count == 0) { throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); } return ResolveQueryAsync(servers, settings, handler, request, cancellationToken: cancellationToken); } // making it internal for unit testing internal IDnsQueryResponse ResolveQuery(IReadOnlyCollection servers, DnsQuerySettings settings, DnsMessageHandler handler, DnsRequestMessage request, LookupClientAudit continueAudit = null) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } LookupClientAudit audit = null; if (settings.EnableAuditTrail) { audit = continueAudit ?? new LookupClientAudit(settings); } DnsResponseException lastDnsResponseException = null; Exception lastException = null; DnsQueryResponse lastQueryResponse = null; foreach (var serverInfo in servers) { var cacheKey = string.Empty; if (settings.UseCache) { cacheKey = ResponseCache.GetCacheKey(request.Question, serverInfo); var item = _cache.Get(cacheKey); if (item != null) { return item; } } var tries = 0; do { tries++; lastDnsResponseException = null; lastException = null; try { audit?.StartTimer(); DnsResponseMessage response = handler.Query(serverInfo.IPEndPoint, request, settings.Timeout); response.Audit = audit; if (response.Header.ResultTruncated && settings.UseTcpFallback && !handler.GetType().Equals(typeof(DnsTcpMessageHandler))) { audit?.AuditTruncatedRetryTcp(); return ResolveQuery(new[] { serverInfo }, settings, _tcpFallbackHandler, request, audit); } audit?.AuditResolveServers(servers.Count); audit?.AuditResponseHeader(response.Header); if (settings.EnableAuditTrail && response.Header.ResponseCode != DnsResponseCode.NoError) { audit.AuditResponseError(response.Header.ResponseCode); } HandleOptRecords(settings, audit, serverInfo, response); DnsQueryResponse queryResponse = response.AsQueryResponse(serverInfo.Clone(), settings); audit?.AuditResponse(); audit?.AuditEnd(queryResponse); serverInfo.Enabled = true; serverInfo.LastSuccessfulRequest = request; lastQueryResponse = queryResponse; if (response.Header.ResponseCode != DnsResponseCode.NoError && (settings.ThrowDnsErrors || settings.ContinueOnDnsError)) { throw new DnsResponseException(response.Header.ResponseCode); } if (settings.UseCache) { _cache.Add(cacheKey, queryResponse); } // TODO: trigger here? RunHealthCheck(); return queryResponse; } catch (DnsResponseException ex) { ////audit.AuditException(ex); ex.AuditTrail = audit?.Build(null); lastDnsResponseException = ex; if (settings.ContinueOnDnsError) { break; // don't retry this server, response was kinda valid } throw ex; } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressFamilyNotSupported) { // this socket error might indicate the server endpoint is actually bad and should be ignored in future queries. DisableServer(serverInfo); break; } catch (Exception ex) when ( ex is TimeoutException || handler.IsTransientException(ex) || ex is OperationCanceledException) { DisableServer(serverInfo); continue; // retrying the same server... } catch (Exception ex) { DisableServer(serverInfo); audit?.AuditException(ex); lastException = ex; // not retrying the same server, use next or return break; } } while (tries <= settings.Retries && serverInfo.Enabled); if (settings.EnableAuditTrail && servers.Count > 1 && serverInfo != servers.Last()) { audit?.AuditRetryNextServer(serverInfo); } } if (lastDnsResponseException != null && settings.ThrowDnsErrors) { throw lastDnsResponseException; } if (lastQueryResponse != null) { return lastQueryResponse; } if (lastException != null) { throw new DnsResponseException(DnsResponseCode.Unassigned, "Unhandled exception", lastException) { AuditTrail = audit?.Build(null) }; } throw new DnsResponseException(DnsResponseCode.ConnectionTimeout, $"No connection could be established to any of the following name servers: {string.Join(", ", servers)}.") { AuditTrail = audit?.Build(null) }; } internal async Task ResolveQueryAsync(IReadOnlyCollection servers, DnsQuerySettings settings, DnsMessageHandler handler, DnsRequestMessage request, LookupClientAudit continueAudit = null, CancellationToken cancellationToken = default) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } LookupClientAudit audit = null; if (settings.EnableAuditTrail) { audit = continueAudit ?? new LookupClientAudit(settings); } DnsResponseException lastDnsResponseException = null; Exception lastException = null; DnsQueryResponse lastQueryResponse = null; foreach (var serverInfo in servers) { var cacheKey = string.Empty; if (settings.UseCache) { cacheKey = ResponseCache.GetCacheKey(request.Question, serverInfo); var item = _cache.Get(cacheKey); if (item != null) { return item; } } var tries = 0; do { tries++; lastDnsResponseException = null; lastException = null; try { cancellationToken.ThrowIfCancellationRequested(); audit?.StartTimer(); DnsResponseMessage response; Action onCancel = () => { }; Task resultTask = handler.QueryAsync(serverInfo.IPEndPoint, request, cancellationToken, (cancel) => { onCancel = cancel; }); if (settings.Timeout != System.Threading.Timeout.InfiniteTimeSpan || (cancellationToken != CancellationToken.None && cancellationToken.CanBeCanceled)) { var cts = new CancellationTokenSource(settings.Timeout); CancellationTokenSource linkedCts = null; if (cancellationToken != CancellationToken.None) { linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); } using (cts) using (linkedCts) { response = await resultTask.WithCancellation((linkedCts ?? cts).Token, onCancel).ConfigureAwait(false); } } else { response = await resultTask.ConfigureAwait(false); } response.Audit = audit; // TODO: better way to prevent infinity looping TCP calls (remove GetType.Equals...) if (response.Header.ResultTruncated && settings.UseTcpFallback && !handler.GetType().Equals(typeof(DnsTcpMessageHandler))) { audit?.AuditTruncatedRetryTcp(); return await ResolveQueryAsync(new[] { serverInfo }, settings, _tcpFallbackHandler, request, audit, cancellationToken).ConfigureAwait(false); } audit?.AuditResolveServers(servers.Count); audit?.AuditResponseHeader(response.Header); if (settings.EnableAuditTrail && response.Header.ResponseCode != DnsResponseCode.NoError) { audit?.AuditResponseError(response.Header.ResponseCode); } HandleOptRecords(settings, audit, serverInfo, response); DnsQueryResponse queryResponse = response.AsQueryResponse(serverInfo.Clone(), settings); audit?.AuditResponse(); audit?.AuditEnd(queryResponse); // got a valid result, lets enabled the server again if it was disabled serverInfo.Enabled = true; lastQueryResponse = queryResponse; serverInfo.LastSuccessfulRequest = request; if (response.Header.ResponseCode != DnsResponseCode.NoError && (settings.ThrowDnsErrors || settings.ContinueOnDnsError)) { throw new DnsResponseException(response.Header.ResponseCode); } if (settings.UseCache) { _cache.Add(cacheKey, queryResponse); } // TODO: trigger here? RunHealthCheck(); return queryResponse; } catch (DnsResponseException ex) { ex.AuditTrail = audit?.Build(null); lastDnsResponseException = ex; if (settings.ContinueOnDnsError) { break; // don't retry this server, response was kinda valid } throw; } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressFamilyNotSupported) { // this socket error might indicate the server endpoint is actually bad and should be ignored in future queries. DisableServer(serverInfo); break; } catch (Exception ex) when ( ex is TimeoutException timeoutEx || handler.IsTransientException(ex) || ex is OperationCanceledException) { // user's token got canceled, throw right away... if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException(cancellationToken); } DisableServer(serverInfo); } catch (Exception ex) { DisableServer(serverInfo); if (ex is AggregateException agg) { agg.Handle((e) => { if (e is TimeoutException || handler.IsTransientException(e) || e is OperationCanceledException) { if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException(cancellationToken); } return true; } return false; }); } audit?.AuditException(ex); lastException = ex; // try next server (this is actually a change and is not configurable, but should be a good thing I guess) break; } } while (tries <= settings.Retries && !cancellationToken.IsCancellationRequested && serverInfo.Enabled); if (settings.EnableAuditTrail && servers.Count > 1 && serverInfo != servers.Last()) { audit?.AuditRetryNextServer(serverInfo); } } if (lastDnsResponseException != null && settings.ThrowDnsErrors) { throw lastDnsResponseException; } if (lastQueryResponse != null) { return lastQueryResponse; } if (lastException != null) { throw new DnsResponseException(DnsResponseCode.Unassigned, "Unhandled exception", lastException) { AuditTrail = audit?.Build(null) }; } throw new DnsResponseException(DnsResponseCode.ConnectionTimeout, $"No connection could be established to any of the following name servers: {string.Join(", ", servers)}.") { AuditTrail = audit?.Build(null) }; } private static DnsQuestion GetReverseQuestion(IPAddress ipAddress) { if (ipAddress == null) { throw new ArgumentNullException(nameof(ipAddress)); } var arpa = ipAddress.GetArpaName(); return new DnsQuestion(arpa, QueryType.PTR, QueryClass.IN); } private void HandleOptRecords(DnsQuerySettings settings, LookupClientAudit audit, NameServer serverInfo, DnsResponseMessage response) { OptRecord record = null; foreach (var add in response.Additionals) { if (add is OptRecord optRecord) { record = optRecord; } } if (record != null) { if (settings.EnableAuditTrail) { audit.AuditOptPseudo(); } serverInfo.SupportedUdpPayloadSize = record.UdpSize; // TODO: handle opt records and remove them later response.Additionals.Remove(record); if (settings.EnableAuditTrail) { audit.AuditEdnsOpt(record.UdpSize, record.Version, record.ResponseCodeEx); } } } private void RunHealthCheck() { // TickCount jump every 25days to int.MinValue, adjusting... var currentTicks = Environment.TickCount & int.MaxValue; if (_lastHealthCheck + s_serverHealthCheckInterval < 0 || currentTicks + s_serverHealthCheckInterval < 0) _lastHealthCheck = 0; if (!_healthCheckRunning && _lastHealthCheck + s_serverHealthCheckInterval < currentTicks) { _lastHealthCheck = currentTicks; var source = new CancellationTokenSource(TimeSpan.FromMinutes(1)); Task.Factory.StartNew( state => DoHealthCheck((CancellationToken)state), source.Token, source.Token, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); } } private async Task DoHealthCheck(CancellationToken cancellationToken) { _healthCheckRunning = true; foreach (var server in Settings.NameServers) { if (!server.Enabled && server.LastSuccessfulRequest != null) { try { var result = await QueryAsync( server.LastSuccessfulRequest.Question, new DnsQueryOptions(server) { Retries = 0, Timeout = TimeSpan.FromSeconds(10), UseCache = false }, cancellationToken); } catch { } } } _healthCheckRunning = false; } private void DisableServer(NameServer server) { if (NameServers.Count > 1) { server.Enabled = false; } } private ushort GetNextUniqueId() { if (_uniqueId == ushort.MaxValue || _uniqueId == 0) { Interlocked.Exchange(ref _uniqueId, _random.Next(ushort.MaxValue / 2)); return (ushort)_uniqueId; } return unchecked((ushort)Interlocked.Increment(ref _uniqueId)); } } internal class LookupClientAudit { private const string c_placeHolder = "$$REPLACEME$$"; private static readonly int s_printOffset = -32; private StringBuilder _auditWriter = new StringBuilder(); private Stopwatch _swatch; public DnsQuerySettings Settings { get; } public LookupClientAudit(DnsQuerySettings settings) { Settings = settings ?? throw new ArgumentNullException(nameof(settings)); } public void StartTimer() { if (!Settings.EnableAuditTrail) { return; } _swatch = Stopwatch.StartNew(); _swatch.Restart(); } public void AuditResolveServers(int count) { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine($"; ({count} server found)"); } public string Build(IDnsQueryResponse queryResponse) { if (!Settings.EnableAuditTrail) { return string.Empty; } var writer = new StringBuilder(); if (queryResponse != null) { if (queryResponse.Questions.Count > 0) { writer.AppendLine(";; QUESTION SECTION:"); foreach (var question in queryResponse.Questions) { writer.AppendLine(question.ToString(s_printOffset)); } writer.AppendLine(); } if (queryResponse.Answers.Count > 0) { writer.AppendLine(";; ANSWER SECTION:"); foreach (var answer in queryResponse.Answers) { writer.AppendLine(answer.ToString(s_printOffset)); } writer.AppendLine(); } if (queryResponse.Authorities.Count > 0) { writer.AppendLine(";; AUTHORITIES SECTION:"); foreach (var auth in queryResponse.Authorities) { writer.AppendLine(auth.ToString(s_printOffset)); } writer.AppendLine(); } if (queryResponse.Additionals.Count > 0) { writer.AppendLine(";; ADDITIONALS SECTION:"); foreach (var additional in queryResponse.Additionals) { writer.AppendLine(additional.ToString(s_printOffset)); } writer.AppendLine(); } } var all = _auditWriter.ToString(); var dynamic = writer.ToString(); return all.Replace(c_placeHolder, dynamic); } public void AuditTruncatedRetryTcp() { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine(";; Truncated, retrying in TCP mode."); _auditWriter.AppendLine(); } public void AuditResponseError(DnsResponseCode responseCode) { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine($";; ERROR: {DnsResponseCodeText.GetErrorText(responseCode)}"); } public void AuditOptPseudo() { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine(";; OPT PSEUDOSECTION:"); } public void AuditResponseHeader(DnsResponseHeader header) { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine(";; Got answer:"); _auditWriter.AppendLine(header.ToString()); if (header.RecursionDesired && !header.RecursionAvailable) { _auditWriter.AppendLine(";; WARNING: recursion requested but not available"); } _auditWriter.AppendLine(); } public void AuditEdnsOpt(short udpSize, byte version, DnsResponseCode responseCodeEx) { if (!Settings.EnableAuditTrail) { return; } // TODO: flags _auditWriter.AppendLine($"; EDNS: version: {version}, flags:; udp: {udpSize}"); } public void AuditResponse() { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine(c_placeHolder); } public void AuditEnd(DnsQueryResponse queryResponse) { if (!Settings.EnableAuditTrail) { return; } var elapsed = _swatch.ElapsedMilliseconds; _auditWriter.AppendLine($";; Query time: {elapsed} msec"); _auditWriter.AppendLine($";; SERVER: {queryResponse.NameServer.Address}#{queryResponse.NameServer.Port}"); _auditWriter.AppendLine($";; WHEN: {DateTime.UtcNow.ToString("ddd MMM dd HH:mm:ss K yyyy", CultureInfo.InvariantCulture)}"); _auditWriter.AppendLine($";; MSG SIZE rcvd: {queryResponse.MessageSize}"); } public void AuditException(Exception ex) { if (!Settings.EnableAuditTrail) { return; } var aggEx = ex as AggregateException; if (ex is DnsResponseException dnsEx) { _auditWriter.AppendLine($";; Error: {DnsResponseCodeText.GetErrorText(dnsEx.Code)} {dnsEx.InnerException?.Message ?? dnsEx.Message}"); } else if (aggEx != null) { _auditWriter.AppendLine($";; Error: {aggEx.InnerException?.Message ?? aggEx.Message}"); } else { _auditWriter.AppendLine($";; Error: {ex.Message}"); } if (Debugger.IsAttached) { _auditWriter.AppendLine(ex.ToString()); } } public void AuditRetryNextServer(NameServer current) { if (!Settings.EnableAuditTrail) { return; } _auditWriter.AppendLine(); _auditWriter.AppendLine($"; SERVER: {current.Address}#{current.Port} failed; Retrying with the next server."); } } }