using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using DnsClient.Protocol; namespace DnsClient { public class LookupClient : IDisposable { private static readonly TimeSpan s_defaultTimeout = TimeSpan.FromSeconds(5); private static readonly TimeSpan s_infiniteTimeout = System.Threading.Timeout.InfiniteTimeSpan; private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); private static ushort _uniqueId = 0; private readonly ResponseCache _cache = new ResponseCache(true); private readonly object _endpointLock = new object(); private readonly DnsMessageHandler _messageHandler; private Queue _endpoints; private TimeSpan _timeout = s_defaultTimeout; private bool _disposedValue = false; /// /// Gets the list of configured name servers. /// public IReadOnlyCollection NameServers { get; } /// /// Gets or set a flag indicating if recursion should be enabled for DNS queries. /// public bool Recursion { get; set; } = true; /// /// Gets or sets number of tries to connect to one name server before trying the next one or throwing an exception. /// public int Retries { get; set; } = 5; /// /// Gets or sets a flag indicating if the should throw an /// if the returned result contains an error flag other than . /// (The default behavior is False). /// public bool ThrowDnsErrors { get; set; } = false; /// /// Gets or sets timeout in milliseconds. /// Timeout must be greater than zero and less than . /// public TimeSpan Timeout { get { return _timeout; } set { if ((value <= TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) { throw new ArgumentOutOfRangeException(nameof(value)); } _timeout = value; } } /// /// Gets or sets a flag indicating if the should use caching or not. /// The TTL of cached results is defined by each resource record individually. /// public bool UseCache { get { return _cache.Enabled; } set { _cache.Enabled = value; } } /// /// Gets or sets a which can override the TTL of a resource record in case the /// TTL of the record is lower than this minimum value. /// This is useful in cases where the server retruns a zero TTL and the record should be cached for a /// very short duration anyways. /// /// This setting gets igonred in case is set to False. /// public TimeSpan? MimimumCacheTimeout { get { return _cache.MinimumTimout; } set { _cache.MinimumTimout = value; } } public LookupClient() : this(NameServer.ResolveNameServers().ToArray()) { } public LookupClient(params IPEndPoint[] nameServers) : this(new DnsUdpMessageHandler(), nameServers) { } public LookupClient(params IPAddress[] nameServers) : this( new DnsUdpMessageHandler(), nameServers.Select(p => new IPEndPoint(p, NameServer.DefaultPort)).ToArray()) { } public LookupClient(DnsMessageHandler messageHandler, ICollection nameServers) { if (messageHandler == null) { throw new ArgumentNullException(nameof(messageHandler)); } if (nameServers == null || nameServers.Count == 0) { throw new ArgumentException("At least one name server must be configured.", nameof(nameServers)); } NameServers = nameServers.ToArray(); _endpoints = new Queue(); foreach (var server in NameServers) { _endpoints.Enqueue(new EndPointInfo(server)); } _messageHandler = messageHandler; } /// /// Translates the IPV4 or IPV6 address into an arpa address. /// /// IP address to get the arpa address form /// The mirrored IPV4 or IPV6 arpa address public static string GetArpaName(IPAddress ip) { var bytes = ip.GetAddressBytes(); Array.Reverse(bytes); // check IP6 if (ip.AddressFamily == AddressFamily.InterNetworkV6) { // reveresed bytes need to be split into 4 bit parts and separated by '.' var newBytes = bytes .SelectMany(b => new[] { (b >> 0) & 0xf, (b >> 4) & 0xf }) .Aggregate(new StringBuilder(), (s, b) => s.Append(b.ToString("x")).Append(".")) + "ip6.arpa."; return newBytes; } else if (ip.AddressFamily == AddressFamily.InterNetwork) { // else IP4 return string.Join(".", bytes) + ".in-addr.arpa."; } throw new InvalidOperationException("Not a valid IP4 or IP6 address."); } public Task QueryAsync(string query, QueryType queryType) => QueryAsync(query, queryType, CancellationToken.None); public Task QueryAsync(string query, QueryType queryType, CancellationToken cancellationToken) => QueryAsync(query, queryType, QueryClass.IN, cancellationToken); public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass) => QueryAsync(query, queryType, queryClass, CancellationToken.None); public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass, CancellationToken cancellationToken) => QueryAsync(new DnsQuestion(query, queryType, queryClass), cancellationToken); ////public Task QueryAsync(params DnsQuestion[] questions) //// => QueryAsync(CancellationToken.None, questions); private async Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken) { if (question == null) { throw new ArgumentNullException(nameof(question)); } var head = new DnsRequestHeader(GetNextUniqueId(), 1, Recursion, DnsOpCode.Query); var request = new DnsRequestMessage(head, question); var cacheKey = ResponseCache.GetCacheKey(question); var result = await _cache.GetOrAdd(cacheKey, async () => await ResolveQueryAsync(request, cancellationToken)); return result; } public Task QueryReverseAsync(IPAddress ipAddress) => QueryReverseAsync(ipAddress, CancellationToken.None); public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken) { if (ipAddress == null) { throw new ArgumentNullException(nameof(ipAddress)); } var arpa = GetArpaName(ipAddress); return QueryAsync(arpa, QueryType.PTR, QueryClass.IN, cancellationToken); } private static ushort GetNextUniqueId() { if (_uniqueId == ushort.MaxValue || _uniqueId == 0) { _uniqueId = (ushort)(new Random()).Next(ushort.MaxValue / 2); } return _uniqueId++; } // TODO: TCP fallback on truncates // TODO: most popular DNS servers do not support mulitple queries in one packet, therefore, split it into multiple requests? //private async Task QueryAsync(DnsRequestMessage request, CancellationToken cancellationToken) //{ //} private async Task ResolveQueryAsync(DnsRequestMessage request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } for (int index = 0; index < NameServers.Count; index++) { EndPointInfo serverInfo = null; lock (_endpointLock) { while (_endpoints.Count > 0 && serverInfo == null) { serverInfo = _endpoints.Dequeue(); if (serverInfo.IsDisabled) { serverInfo = null; } else { // put it back and then use it.. _endpoints.Enqueue(serverInfo); } } if (serverInfo == null) { // let's be optimistic and eable them again, maybe they wher offline one for a while _endpoints.ToList().ForEach(p => p.IsDisabled = false); continue; } } var tries = 0; do { tries++; try { DnsResponseMessage response; var resultTask = _messageHandler.QueryAsync(serverInfo.Endpoint, request, cancellationToken); if (Timeout != s_infiniteTimeout) { response = await resultTask.TimeoutAfter(Timeout); } response = await resultTask; var result = response.AsReadonly; if (ThrowDnsErrors && result.Header.ResponseCode != DnsResponseCode.NoError) { throw new DnsResponseException(result.Header.ResponseCode); } return result; } catch (DnsResponseException) { // occurs only if the option to throw dns exceptions is enabled on the lookup client. (see above). // lets not mess with the stack throw; } catch (TimeoutException) { // do nothing... transient if timeoutAfter timed out } 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. serverInfo.IsDisabled = true; Debug.WriteLine($"Disabling name server {serverInfo.Endpoint}."); break; } catch (Exception ex) when (_messageHandler.IsTransientException(ex)) { } catch (Exception ex) { var agg = ex as AggregateException; if (agg != null) { agg.Handle(e => { if (e is TimeoutException) return true; if (_messageHandler.IsTransientException(e)) return true; return false; }); throw new DnsResponseException("Unhandled exception", agg.InnerException); } throw new DnsResponseException("Unhandled exception", ex); } finally { // do cleanup stuff or logging? } } while (tries <= Retries && !cancellationToken.IsCancellationRequested); } throw new DnsResponseException($"No connection could be established to any of the following name servers: {string.Join(", ", NameServers)}."); } private class EndPointInfo { public IPEndPoint Endpoint { get; } public bool IsDisabled { get; set; } public EndPointInfo(IPEndPoint endpoint) { Endpoint = endpoint; IsDisabled = false; } } protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { _messageHandler.Dispose(); } _disposedValue = true; } } public void Dispose() { Dispose(true); } } }