From 93c562926984261a1a16cc15db42390c116c0e9b Mon Sep 17 00:00:00 2001 From: MichaC Date: Sat, 14 Mar 2020 02:43:50 +0100 Subject: [PATCH] Improved Error and EDNS handling (#62), ContinueOnEmptyResponse (#64) * changed how opt records are created and used. Added configuration to disable EDNS and to set the requested buffer size and DnsSec * Changes the behavior in case of bad responses which were truncated by some middleman proxy or router - fixes #52 * Changing default unknown record handling to preserve the original data so that users can work with those records. * Reworking error handling see #60 * Adding new setting ContinueOnEmptyResponse #64 --- Documentation/build.cmd | 2 +- Documentation/docfx.json | 8 +- .../ManagedReference.html.primary.tmpl | 32 +- README.md | 53 +- azure-pipelines-ci.yml | 6 + samples/MiniDig/DnsCommand.cs | 44 +- samples/MiniDig/MiniDig.csproj | 2 +- samples/MiniDig/PerfCommand.cs | 2 +- samples/MiniDig/Program.cs | 2 +- samples/MiniDig/RandomCommand.cs | 44 +- src/DnsClient/DnsClient.csproj | 1 + src/DnsClient/DnsDatagramWriter.cs | 2 - src/DnsClient/DnsMessageHandler.cs | 71 +- src/DnsClient/DnsQueryExtensions.cs | 24 +- src/DnsClient/DnsQueryOptions.cs | 955 ++++++++++ src/DnsClient/DnsQueryResponse.cs | 46 +- src/DnsClient/DnsRecordFactory.cs | 6 +- src/DnsClient/DnsRequestHeader.cs | 22 +- src/DnsClient/DnsRequestMessage.cs | 11 +- src/DnsClient/DnsResponseCode.cs | 133 +- src/DnsClient/DnsResponseHeader.cs | 6 +- src/DnsClient/DnsResponseMessage.cs | 4 +- src/DnsClient/DnsResponseParseException.cs | 31 +- src/DnsClient/DnsTcpMessageHandler.cs | 6 +- src/DnsClient/DnsUdpMessageHandler.cs | 8 +- src/DnsClient/ILookupClient.cs | 867 --------- .../Internal/StringBuilderObjectPool.cs | 218 ++- src/DnsClient/LookupClient.cs | 1234 ++++++++----- src/DnsClient/NameServer.cs | 30 +- src/DnsClient/Protocol/DnsResourceRecord.cs | 6 +- src/DnsClient/Protocol/Options/OptRecord.cs | 15 +- src/DnsClient/Protocol/UnknownRecord.cs | 50 + src/DnsClient/ResponseCache.cs | 1 + src/DnsClient/Tracing.cs | 12 - .../ApiDesign.OldReference.csproj | 8 +- test/ApiDesign/ApiDesign.csproj | 3 +- test/ApiDesign/Program.cs | 31 +- test/Benchmarks/Benchmarks.csproj | 2 +- .../DnsClientBenchmarks.DatagramReader.cs | 7 +- ...ClientBenchmarks.RequestResponseParsing.cs | 134 +- test/DnsClient.Tests/ApiCompatibilityTest.cs | 15 +- test/DnsClient.Tests/DatagramReaderTest.cs | 80 +- test/DnsClient.Tests/DnsClient.Tests.csproj | 18 +- test/DnsClient.Tests/DnsMessageHandlerTest.cs | 22 +- test/DnsClient.Tests/DnsRecordFactoryTest.cs | 14 +- test/DnsClient.Tests/DnsRequestHeaderTest.cs | 10 +- .../DnsClient.Tests/DnsResponseParsingTest.cs | 65 +- test/DnsClient.Tests/DnsStringTest.cs | 1 + test/DnsClient.Tests/LookupClientRetryTest.cs | 1565 ++++++++++++++--- .../LookupConfigurationTest.cs | 498 +++++- test/DnsClient.Tests/LookupTest.cs | 524 +++--- test/DnsClient.Tests/MockExampleTest.cs | 7 +- test/DnsClient.Tests/PooledByteTest.cs | 1 + test/DnsClient.Tests/ResponseCacheTest.cs | 33 +- tools/etc/named.conf | 3 + 55 files changed, 4732 insertions(+), 2263 deletions(-) create mode 100644 src/DnsClient/DnsQueryOptions.cs create mode 100644 src/DnsClient/Protocol/UnknownRecord.cs diff --git a/Documentation/build.cmd b/Documentation/build.cmd index e5e2b283..fc95e9e7 100644 --- a/Documentation/build.cmd +++ b/Documentation/build.cmd @@ -4,7 +4,7 @@ cd %~dp0 call GetMsdn.cmd SETLOCAL -SET DOCFX_VERSION=2.16.7 +SET DOCFX_VERSION=2.50 SET CACHED_ZIP=%LocalAppData%\DocFx\docfx.%DOCFX_VERSION%.zip IF EXIST %CACHED_ZIP% goto extract diff --git a/Documentation/docfx.json b/Documentation/docfx.json index c7369c70..22f981ce 100644 --- a/Documentation/docfx.json +++ b/Documentation/docfx.json @@ -7,10 +7,7 @@ "files": "**/*.csproj" } ], - "dest": "obj/api", - "properties": { - "TargetFramework": "netstandard1.3" - } + "dest": "obj/api" } ], "build": { @@ -56,13 +53,12 @@ ] } ], - "globalMetadata": { "_gitContribute": { "branch": "dev", "apiSpecFolder": "Documentation/apispec" } - }, + }, "postProcessors": [ "ExtractSearchIndex" ], "xref": "msdn.4.5.2/content/msdn.4.5.2.zip", "dest": "..\\..\\dnsclient.michaco.net\\website\\Docs", diff --git a/Documentation/template/ManagedReference.html.primary.tmpl b/Documentation/template/ManagedReference.html.primary.tmpl index 55945862..7ca200b6 100644 --- a/Documentation/template/ManagedReference.html.primary.tmpl +++ b/Documentation/template/ManagedReference.html.primary.tmpl @@ -1,8 +1,3 @@ -{{#item}} - - {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} - -
{{^_disableBreadcrumb}} @@ -21,24 +16,24 @@ {{/_disableToc}} {{#_disableToc}}
- {{/_disableToc}} + {{/_disableToc}} {{#_disableAffix}}
{{/_disableAffix}} {{^_disableAffix}}
{{/_disableAffix}} -
- {{#isNamespace}} - {{>partials/namespace}} - {{/isNamespace}} - {{#isClass}} - {{>partials/class}} - {{/isClass}} - {{#isEnum}} - {{>partials/enum}} - {{/isEnum}} - {{>partials/customMREFContent}} +
+ {{#isNamespace}} + {{>partials/namespace}} + {{/isNamespace}} + {{#isClass}} + {{>partials/class}} + {{/isClass}} + {{#isEnum}} + {{>partials/enum}} + {{/isEnum}} + {{>partials/customMREFContent}}
{{^_disableAffix}} @@ -46,5 +41,4 @@ {{/_disableAffix}}
-
-{{/item}} + \ No newline at end of file diff --git a/README.md b/README.md index e5be1ac1..578c0631 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # DnsClient.NET +[![Build Status](https://dev.azure.com/michaco/DnsClient/_apis/build/status/MichaCo.DnsClient.NET?branchName=dev)](https://dev.azure.com/michaco/DnsClient/_build/latest?definitionId=1&branchName=dev) [![NuGet](https://img.shields.io/nuget/v/DnsClient.svg?style=flat&label=Stable)](https://www.nuget.org/packages/DnsClient) [![MyGet](https://img.shields.io/myget/dnsclient/vpre/DnsClient.svg?style=flat&label=Pre-release)](https://www.myget.org/feed/dnsclient/package/nuget/DnsClient) @@ -9,56 +10,47 @@ DnsClient.NET is a simple yet very powerful and high performant open source libr See http://dnsclient.michaco.net for more details and documentation. -The following example instantiates a new `LookupClient` without specifying a DNS endpoint. -DnsClient.NET will query your system network adapters to determine available DNS servers. +The following example instantiates a new `LookupClient` to query some IP address. ``` csharp var lookup = new LookupClient(); -var result = await lookup.QueryAsync("google.com", QueryType.ANY); +var result = await lookup.QueryAsync("google.com", QueryType.A); var record = result.Answers.ARecords().FirstOrDefault(); -var address = record?.Address; +var ip = record?.Address; ``` -## Builds - -[![Build Status](https://dev.azure.com/michaco/DnsClient/_apis/build/status/MichaCo.DnsClient.NET?branchName=dev)](https://dev.azure.com/michaco/DnsClient/_build/latest?definitionId=1&branchName=dev) - -Get it via NuGet https://www.nuget.org/packages/DnsClient/ - -Get beta builds from [MyGet](https://www.myget.org/feed/dnsclient/package/nuget/DnsClient). - ## Features ### General -* Full Async API -* UDP and TCP lookup, configurable if TCP should be used as fallback in case UDP result is truncated (default=true). +* Sync & Async API +* UDP and TCP lookup, configurable if TCP should be used as fallback in case the UDP result is truncated (default=true). +* Configurable EDNS support to change the default UDP buffer size and request security relevant records * Caching * Query result cache based on provided TTL * Minimum TTL setting to overrule the result's TTL and always cache the responses for at least that time. (Even very low value, like a few milliseconds, do make a huge difference if used in high traffic low latency scenarios) - * Cache can be disabled altogether -* Supports multiple DNS endpoints to be configured -* Configurable retry over configured DNS servers if one or more returned a server error -* Configurable retry logic in case of timeouts and other exceptions + * Maximum TTL to limit cache duration + * Cache can be disabled +* Multiple DNS endpoints can be configured. DnsClient will use them in random or sequential order (configurable), with re-tries. +* Configurable retry of queries * Optional audit trail of each response and exception * Configurable error handling. Throwing DNS errors, like `NotExistentDomain` is turned off by default +* Optional Trace/Logging ### Supported resource records * A, AAAA, NS, CNAME, SOA, MB, MG, MR, WKS, HINFO, MINFO, MX, RP, TXT, AFSDB, URI, CAA, NULL, SSHFP * PTR for reverse lookups -* SRV For service discovery. `LookupClient` has some extensions to help with that. -* OPT (currently only for reading the supported UDP buffer size, EDNS version) +* SRV for service discovery. `LookupClient` has some extensions to help with that. * AXFR zone transfer (as per spec, LookupClient has to be set to TCP mode only for this type. Also, the result depends on if the DNS server trusts your current connection) ## Build from Source -The solution requires a .NET Core 2.x SDK and the [.NET 4.7.1 Dev Pack](https://www.microsoft.com/net/download/dotnet-framework/net471) being installed. +The solution requires a .NET Core 3.x SDK and the [.NET 4.7.1 Dev Pack](https://www.microsoft.com/net/download/dotnet-framework/net471) being installed. -Just clone the repository and open the solution in Visual Studio 2017. -Or use the dotnet client via command line. +Just clone the repository and open the solution in Visual Studio 2017/2019. The unit tests don't require any additional setup right now. @@ -70,18 +62,11 @@ Now, you can use **samples/MiniDig** to query the local DNS server. The following should return many different resource records: ``` cmd -dotnet run -s localhost micha.mcnet.com any -``` - -To test some random domain names, run MiniDig with the `random` sub command (works without setting up Bind, too). - -``` cmd -dotnet run random -s localhost +dotnet run -s localhost mcnet.com any ``` ## Examples -* The [Samples](https://github.com/MichaCo/DnsClient.NET.Samples) repository will have some solutions to showcase the usage and also to test some functionality. - -* [MiniDig](https://github.com/MichaCo/DnsClient.NET/tree/dev/samples/MiniDig) (See the readme over there) - +* More docuemntation and a simple query window on http://dnsclient.michaco.net +* The [Samples](https://github.com/MichaCo/DnsClient.NET.Samples) repository (there might be more in the future). +* [MiniDig](https://github.com/MichaCo/DnsClient.NET/tree/dev/samples/MiniDig) \ No newline at end of file diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index b80b246a..4de71d7e 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -4,6 +4,12 @@ trigger: - dev - master +pr: + autoCancel: false + branches: + include: + - '*' + # pool: # vmImage: 'windows-latest' diff --git a/samples/MiniDig/DnsCommand.cs b/samples/MiniDig/DnsCommand.cs index 6541e645..d98213eb 100644 --- a/samples/MiniDig/DnsCommand.cs +++ b/samples/MiniDig/DnsCommand.cs @@ -9,25 +9,6 @@ namespace DigApp { - public class LookupSettings - { - public IPEndPoint[] Endpoints { get; set; } - - public TimeSpan MinTTL { get; set; } - - public bool NoTcp { get; set; } - - public bool Recursion { get; set; } - - public int Retries { get; set; } - - public bool TcpOnly { get; set; } - - public TimeSpan Timeout { get; set; } - - public bool UseCache { get; set; } - } - public abstract class DnsCommand { public CommandOption ConnectTimeoutArg { get; set; } @@ -36,6 +17,10 @@ public abstract class DnsCommand public CommandOption MaximumTTLArg { get; set; } + public CommandOption MaximumBufferSizeArg { get; set; } + + public CommandOption RequestDnsSecRecordsArg { get; set; } + public CommandOption NoRecurseArg { get; set; } public string[] OriginalArgs { get; } @@ -112,7 +97,9 @@ public LookupClientOptions GetLookupSettings() UseCache = GetUseCache(), UseTcpOnly = GetUseTcp(), UseTcpFallback = !GetNoTcp(), - MaximumCacheTimeout = GetMaximumTTL() + MaximumCacheTimeout = GetMaximumTTL(), + ExtendedDnsBufferSize = GetMaximumBufferSize(), + RequestDnsSecRecords = GetRequestDnsSec() }; } @@ -136,6 +123,11 @@ public LookupClientOptions GetLookupSettings() return null; } + public int GetMaximumBufferSize() + => MaximumBufferSizeArg.HasValue() ? int.Parse(MaximumBufferSizeArg.Value()) : DnsQueryOptions.MaximumBufferSize; + + public bool GetRequestDnsSec() => RequestDnsSecRecordsArg.HasValue(); + public int GetTimeoutValue() => ConnectTimeoutArg.HasValue() ? int.Parse(ConnectTimeoutArg.Value()) : 1000; public int GetTriesValue() => TriesArg.HasValue() ? int.Parse(TriesArg.Value()) : 5; @@ -193,7 +185,17 @@ protected virtual void Configure() "Maximum cache ttl.", CommandOptionType.SingleValue); - App.HelpOption("-? | -h | --help"); + MaximumBufferSizeArg = App.Option( + "--bufsize", + "Maximum EDNS buffer size.", + CommandOptionType.SingleValue); + + RequestDnsSecRecordsArg = App.Option( + "--dnssec", + "Request DNS SEC records (do flag).", + CommandOptionType.NoValue); + + App.HelpOption("-? | -h | --help | --helpme"); } protected abstract Task Execute(); diff --git a/samples/MiniDig/MiniDig.csproj b/samples/MiniDig/MiniDig.csproj index 83ada7de..d4e185e8 100644 --- a/samples/MiniDig/MiniDig.csproj +++ b/samples/MiniDig/MiniDig.csproj @@ -1,7 +1,7 @@  - net471;netcoreapp3.1; + net45;net471;netcoreapp3.1; MiniDig Exe MiniDig diff --git a/samples/MiniDig/PerfCommand.cs b/samples/MiniDig/PerfCommand.cs index 62c526dd..4976a5e0 100644 --- a/samples/MiniDig/PerfCommand.cs +++ b/samples/MiniDig/PerfCommand.cs @@ -40,7 +40,7 @@ protected override void Configure() { QueryArg = App.Argument("query", "the domain query to run.", false); ClientsArg = App.Option("-c | --clients", "Number of clients to run", CommandOptionType.SingleValue); - RuntimeArg = App.Option("-r | --runtime", "Time in seconds to run", CommandOptionType.SingleValue); + RuntimeArg = App.Option("-r | --run", "Time in seconds to run", CommandOptionType.SingleValue); SyncArg = App.Option("--sync", "Run synchronous api", CommandOptionType.NoValue); base.Configure(); } diff --git a/samples/MiniDig/Program.cs b/samples/MiniDig/Program.cs index 8d005ecb..3a7b22d8 100644 --- a/samples/MiniDig/Program.cs +++ b/samples/MiniDig/Program.cs @@ -9,7 +9,7 @@ public class Program { public static async Task Main(string[] args) { - DnsClient.Tracing.Source.Switch.Level = SourceLevels.Warning; + DnsClient.Tracing.Source.Switch.Level = SourceLevels.Verbose; DnsClient.Tracing.Source.Listeners.Add(new ConsoleTraceListener()); var app = new CommandLineApplication(throwOnUnexpectedArg: true); diff --git a/samples/MiniDig/RandomCommand.cs b/samples/MiniDig/RandomCommand.cs index b7a0ae70..0f277a45 100644 --- a/samples/MiniDig/RandomCommand.cs +++ b/samples/MiniDig/RandomCommand.cs @@ -14,7 +14,6 @@ namespace DigApp public class RandomCommand : DnsCommand { private ConcurrentQueue _domainNames; - private static readonly object _nameLock = new object(); private static Random _randmom = new Random(); private int _clients; private int _runtime; @@ -25,6 +24,8 @@ public class RandomCommand : DnsCommand private LookupClient _lookup; private int _errors; private ConcurrentDictionary _errorsPerCode = new ConcurrentDictionary(); + private ConcurrentDictionary _successByServer = new ConcurrentDictionary(); + private ConcurrentDictionary _failByServer = new ConcurrentDictionary(); private int _success; private Spiner _spinner; private bool _runSync; @@ -55,7 +56,7 @@ public string NextDomainName() protected override void Configure() { ClientsArg = App.Option("-c | --clients", "Number of clients to run", CommandOptionType.SingleValue); - RuntimeArg = App.Option("-r | --runtime", "Time in seconds to run", CommandOptionType.SingleValue); + RuntimeArg = App.Option("-r | --run", "Time in seconds to run", CommandOptionType.SingleValue); SyncArg = App.Option("--sync", "Run synchronous api", CommandOptionType.NoValue); base.Configure(); } @@ -63,7 +64,7 @@ protected override void Configure() protected override async Task Execute() { var lines = File.ReadAllLines("names.txt"); - _domainNames = new ConcurrentQueue(lines.Select(p => p.Substring(p.IndexOf(',') + 1))); + _domainNames = new ConcurrentQueue(lines.Select(p => p.Substring(p.IndexOf(',') + 1)).OrderBy(x => _randmom.Next(0, lines.Length * 2))); _clients = ClientsArg.HasValue() ? int.Parse(ClientsArg.Value()) : 10; _runtime = RuntimeArg.HasValue() ? int.Parse(RuntimeArg.Value()) <= 1 ? 5 : int.Parse(RuntimeArg.Value()) : 5; @@ -71,6 +72,8 @@ protected override async Task Execute() _settings = GetLookupSettings(); _settings.EnableAuditTrail = true; + _settings.ThrowDnsErrors = false; + _settings.ContinueOnDnsError = false; _lookup = GetDnsLookup(_settings); _running = true; @@ -97,8 +100,14 @@ protected override async Task Execute() } tasks.Add(CollectPrint()); - - await Task.WhenAny(tasks.ToArray()); + try + { + await Task.WhenAny(tasks.ToArray()); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } double elapsedSeconds = sw.ElapsedMilliseconds / 1000d; @@ -132,7 +141,19 @@ private async Task CollectPrint() waitCount++; await Task.Delay(1000); - _spinner.Status = $"{_reportExcecutions:N2} req/sec"; + var serverUpdate = from good in _successByServer + join fail in _failByServer on good.Key equals fail.Key into all + from row in all.DefaultIfEmpty() + select new + { + good.Key, + Fails = row.Value, + Success = good.Value + }; + + var updateString = string.Join(" | ", serverUpdate.Select((p, i) => $"Server{i}: +{p.Success} -{p.Fails}")); + + _spinner.Status = $"{_reportExcecutions:N2} req/sec {_allExcecutions:N0} total - [{updateString}]"; Interlocked.Exchange(ref _reportExcecutions, 0); } _running = false; @@ -144,18 +165,19 @@ private async Task ExcecuteRun() while (_running) { var query = NextDomainName(); + try { + IDnsQueryResponse response = null; _spinner.Message = query; - IDnsQueryResponse response; + if (!_runSync) { - response = await _lookup.QueryAsync(query, QueryType.ANY); + response = await _lookup.QueryAsync(query, QueryType.A); } else { - response = await Task.Run(() => _lookup.Query(query, QueryType.ANY)); - await Task.Delay(0); + response = await Task.Run(() => _lookup.Query(query, QueryType.A)); } Interlocked.Increment(ref _allExcecutions); @@ -163,10 +185,12 @@ private async Task ExcecuteRun() if (response.HasError) { _errorsPerCode.AddOrUpdate(response.Header.ResponseCode.ToString(), 1, (c, v) => v + 1); + _failByServer.AddOrUpdate(response.NameServer, 1, (n, v) => v + 1); Interlocked.Increment(ref _errors); } else { + _successByServer.AddOrUpdate(response.NameServer, 1, (n, v) => v + 1); Interlocked.Increment(ref _success); } } diff --git a/src/DnsClient/DnsClient.csproj b/src/DnsClient/DnsClient.csproj index e3b3b1ea..e7d0b04f 100644 --- a/src/DnsClient/DnsClient.csproj +++ b/src/DnsClient/DnsClient.csproj @@ -51,5 +51,6 @@ + \ No newline at end of file diff --git a/src/DnsClient/DnsDatagramWriter.cs b/src/DnsClient/DnsDatagramWriter.cs index e584f16c..c611920b 100644 --- a/src/DnsClient/DnsDatagramWriter.cs +++ b/src/DnsClient/DnsDatagramWriter.cs @@ -36,8 +36,6 @@ public DnsDatagramWriter() public DnsDatagramWriter(ArraySegment useBuffer) { - Debug.Assert(useBuffer.Count >= BufferSize); - _buffer = useBuffer; } diff --git a/src/DnsClient/DnsMessageHandler.cs b/src/DnsClient/DnsMessageHandler.cs index e7c2c380..3dc0f612 100644 --- a/src/DnsClient/DnsMessageHandler.cs +++ b/src/DnsClient/DnsMessageHandler.cs @@ -1,19 +1,50 @@ using System; using System.Net; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using DnsClient.Protocol.Options; namespace DnsClient { + internal enum DnsMessageHandleType + { + None = 0, + UDP, + TCP + } + internal abstract class DnsMessageHandler { + public abstract DnsMessageHandleType Type { get; } + public abstract DnsResponseMessage Query(IPEndPoint endpoint, DnsRequestMessage request, TimeSpan timeout); public abstract Task QueryAsync(IPEndPoint endpoint, DnsRequestMessage request, CancellationToken cancellationToken, Action cancelationCallback); - public abstract bool IsTransientException(T exception) where T : Exception; + // Transient errors will be retried on the same NameServer before the resolver moves on + // to the next configured NameServer (if any). + public bool IsTransientException(T exception) where T : Exception + { + if (exception is SocketException socketException) + { + // I think those are reasonable socket errors which can be retried + // with the same NameServer. Hard to tell though and might change... + // Any other socket exception will cause the LookupClient to move on to the next server. + switch (socketException.SocketErrorCode) + { + case SocketError.TimedOut: + case SocketError.ConnectionAborted: + case SocketError.ConnectionReset: + case SocketError.OperationAborted: + case SocketError.TryAgain: + return true; + } + } + + return false; + } public virtual void GetRequestData(DnsRequestMessage request, DnsDatagramWriter writer) { @@ -43,32 +74,30 @@ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 writer.WriteInt16NetworkOrder(1); // we support single question only... (as most DNS servers anyways). writer.WriteInt16NetworkOrder(0); writer.WriteInt16NetworkOrder(0); - writer.WriteInt16NetworkOrder(1); // one additional for the Opt record. + + if (request.QuerySettings.UseExtendedDns) + { + writer.WriteInt16NetworkOrder(1); // one additional for the Opt record. + } + else + { + writer.WriteInt16NetworkOrder(0); + } writer.WriteHostName(question.QueryName); writer.WriteUInt16NetworkOrder((ushort)question.QuestionType); writer.WriteUInt16NetworkOrder((ushort)question.QuestionClass); - /* - +------------+--------------+------------------------------+ - | Field Name | Field Type | Description | - +------------+--------------+------------------------------+ - | NAME | domain name | MUST be 0 (root domain) | - | TYPE | u_int16_t | OPT (41) | - | CLASS | u_int16_t | requestor's UDP payload size | - | TTL | u_int32_t | extended RCODE and flags | - | RDLEN | u_int16_t | length of all RDATA | - | RDATA | octet stream | {attribute,value} pairs | - +------------+--------------+------------------------------+ - * */ - - var opt = new OptRecord(); + if (request.QuerySettings.UseExtendedDns) + { + var opt = new OptRecord(size: request.QuerySettings.ExtendedDnsBufferSize, doFlag: request.QuerySettings.RequestDnsSecRecords); - writer.WriteHostName(""); - writer.WriteUInt16NetworkOrder((ushort)opt.RecordType); - writer.WriteUInt16NetworkOrder((ushort)opt.RecordClass); - writer.WriteUInt32NetworkOrder((ushort)opt.InitialTimeToLive); - writer.WriteUInt16NetworkOrder(0); + writer.WriteHostName(""); + writer.WriteUInt16NetworkOrder((ushort)opt.RecordType); + writer.WriteUInt16NetworkOrder((ushort)opt.RecordClass); + writer.WriteUInt32NetworkOrder((ushort)opt.InitialTimeToLive); + writer.WriteUInt16NetworkOrder(0); + } } public virtual DnsResponseMessage GetResponseMessage(ArraySegment responseData) diff --git a/src/DnsClient/DnsQueryExtensions.cs b/src/DnsClient/DnsQueryExtensions.cs index 46e14fea..f1f2069f 100644 --- a/src/DnsClient/DnsQueryExtensions.cs +++ b/src/DnsClient/DnsQueryExtensions.cs @@ -69,10 +69,10 @@ public static class DnsQueryExtensions /// /// An instance that contains address information about the host specified in . /// In case the could not be resolved to a domain name, this method returns null, - /// unless is set to true, then it might throw a . + /// unless is set to true, then it might throw a . /// /// If is null. - /// In case is set to true and a DNS error occurs. + /// In case is set to true and a DNS error occurs. public static IPHostEntry GetHostEntry(this IDnsQuery query, string hostNameOrAddress) { if (query == null) @@ -145,10 +145,10 @@ public static IPHostEntry GetHostEntry(this IDnsQuery query, string hostNameOrAd /// /// An instance that contains address information about the host specified in . /// In case the could not be resolved to a domain name, this method returns null, - /// unless is set to true, then it might throw a . + /// unless is set to true, then it might throw a . /// /// If is null. - /// In case is set to true and a DNS error occurs. + /// In case is set to true and a DNS error occurs. public static Task GetHostEntryAsync(this IDnsQuery query, string hostNameOrAddress) { if (query == null) @@ -221,10 +221,10 @@ public static Task GetHostEntryAsync(this IDnsQuery query, string h /// /// An instance that contains address information about the host specified in . /// In case the could not be resolved to a domain name, this method returns null, - /// unless is set to true, then it might throw a . + /// unless is set to true, then it might throw a . /// /// If is null. - /// In case is set to true and a DNS error occurs. + /// In case is set to true and a DNS error occurs. public static IPHostEntry GetHostEntry(this IDnsQuery query, IPAddress address) { if (query == null) @@ -298,10 +298,10 @@ public static IPHostEntry GetHostEntry(this IDnsQuery query, IPAddress address) /// /// An instance that contains address information about the host specified in . /// In case the could not be resolved to a domain name, this method returns null, - /// unless is set to true, then it might throw a . + /// unless is set to true, then it might throw a . /// /// If is null. - /// In case is set to true and a DNS error occurs. + /// In case is set to true and a DNS error occurs. public static async Task GetHostEntryAsync(this IDnsQuery query, IPAddress address) { if (query == null) @@ -414,10 +414,10 @@ private static IPHostEntry GetHostEntryProcessResult(DnsString hostString, DnsRe /// The to resolve. /// /// The hostname if the reverse lookup was successful or null, in case the host was not found. - /// If is set to true, this method will throw an instead of returning null! + /// If is set to true, this method will throw an instead of returning null! /// /// If is null. - /// If no host has been found and is true. + /// If no host has been found and is true. public static string GetHostName(this IDnsQuery query, IPAddress address) { if (query == null) @@ -440,10 +440,10 @@ public static string GetHostName(this IDnsQuery query, IPAddress address) /// The to resolve. /// /// The hostname if the reverse lookup was successful or null, in case the host was not found. - /// If is set to true, this method will throw an instead of returning null! + /// If is set to true, this method will throw an instead of returning null! /// /// If is null. - /// If no host has been found and is true. + /// If no host has been found and is true. public static async Task GetHostNameAsync(this IDnsQuery query, IPAddress address) { if (query == null) diff --git a/src/DnsClient/DnsQueryOptions.cs b/src/DnsClient/DnsQueryOptions.cs new file mode 100644 index 00000000..504f599d --- /dev/null +++ b/src/DnsClient/DnsQueryOptions.cs @@ -0,0 +1,955 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using DnsClient.Protocol; + +namespace DnsClient +{ + /// + /// The options used to override the defaults of per query. + /// + public class DnsQueryOptions + { + /// + /// The minimum payload size. Anything equal or less than that will default back to this value and might disable EDNS. + /// + public const int MinimumBufferSize = 512; + + /// + /// The maximum reasonable payload size. + /// + public const int MaximumBufferSize = 4096; + + 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 TimeSpan _timeout = s_defaultTimeout; + private int _ednsBufferSize = MaximumBufferSize; + + /// + /// Gets or sets a flag indicating whether each will contain a full documentation of the response(s). + /// Default is False. + /// + /// + public bool EnableAuditTrail { get; set; } = false; + + /// + /// Gets or sets a flag indicating whether DNS queries should use response caching or not. + /// The cache duration is calculated by the resource record of the response. Usually, the lowest TTL is used. + /// Default is True. + /// + /// + /// In case the DNS Server returns records with a TTL of zero. The response cannot be cached. + /// + public bool UseCache { get; set; } = true; + + /// + /// Gets or sets a flag indicating whether DNS queries should instruct the DNS server to do recursive lookups, or not. + /// Default is True. + /// + /// The flag indicating if recursion should be used or not. + public bool Recursion { get; set; } = true; + + /// + /// Gets or sets the number of tries to get a response from one name server before trying the next one. + /// Only transient errors, like network or connection errors will be retried. + /// Default is 2 which will be three tries total. + /// + /// If all configured error out after retries, an exception will be thrown at the end. + /// + /// + /// The number of retries. + public int Retries { get; set; } = 2; + + /// + /// Gets or sets a flag indicating whether the should throw a + /// in case the query result has a other than . + /// Default is False. + /// + /// + /// + /// If set to False, the query will return a result with an + /// which contains more information. + /// + /// + /// If set to True, any query method of will throw an if + /// the response header indicates an error. + /// + /// + /// If both, and are set to True, + /// will continue to query all configured . + /// If none of the servers yield a valid response, a will be thrown + /// with the error of the last response. + /// + /// + /// + /// + public bool ThrowDnsErrors { get; set; } = false; + + /// + /// Gets or sets a flag indicating whether the can cycle through all + /// configured on each consecutive request, basically using a random server, or not. + /// Default is True. + /// If only one is configured, this setting is not used. + /// + /// + /// + /// If False, configured endpoint will be used in random order. + /// If True, the order will be preserved. + /// + /// + /// Even if is set to True, the endpoint might still get + /// disabled and might not being used for some time if it errors out, e.g. no connection can be established. + /// + /// + public bool UseRandomNameServer { get; set; } = true; + + /// + /// Gets or sets a flag indicating whether to query the next configured in case the response of the last query + /// returned a other than . + /// Default is True. + /// + /// + /// If True, lookup client will continue until a server returns a valid result, or, + /// if no yield a valid result, the last response with the error will be returned. + /// In case no server yields a valid result and is also enabled, an exception + /// will be thrown containing the error of the last response. + /// + /// If True and is enabled, the exception will be thrown on first encounter without trying any other servers. + /// + /// + /// + public bool ContinueOnDnsError { get; set; } = true; + + /// + /// Gets or sets a flag indicating whether to query the next configured + /// if the response does not have an error but the query was not answered by the response. + /// Default is True. + /// + /// + /// The query is answered if there is at least one in the answers section + /// matching the 's . + /// + /// If there are zero answeres in the response, the query is not answered, independent of the . + /// If there are answers in the response, the is used to find a matching record, + /// query types and will be ignored by this check. + /// + /// + public bool ContinueOnEmptyResponse { get; set; } = true; + + /// + /// Gets or sets the request timeout in milliseconds. is used for limiting the connection and request time for one operation. + /// Timeout must be greater than zero and less than . + /// If (or -1) is used, no timeout will be applied. + /// Default is 5 seconds. + /// + /// + /// If a very short timeout is configured, queries will more likely result in s. + /// + /// Important to note, s will be retried, if are not disabled (set to 0). + /// This should help in case one or more configured DNS servers are not reachable or under load for example. + /// + /// + 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 whether Tcp should be used in case a Udp response is truncated. + /// Default is True. + /// + /// If False, truncated results will potentially yield no or incomplete answers. + /// + /// + public bool UseTcpFallback { get; set; } = true; + + /// + /// Gets or sets a flag indicating whether Udp should not be used at all. + /// Default is False. + /// + /// Enable this only if Udp cannot be used because of your firewall rules for example. + /// Also, zone transfers (see ) must use TCP only. + /// + /// + public bool UseTcpOnly { get; set; } = false; + + /// + /// Gets or sets the maximum buffer used for UDP requests. + /// Defaults to 4096. + /// + /// If this value is less or equal to 512 bytes, EDNS might be disabled. + /// + /// + public int ExtendedDnsBufferSize + { + get + { + return _ednsBufferSize; + } + set + { + if (value < MinimumBufferSize) + { + _ednsBufferSize = MinimumBufferSize; + } + else if (value > MaximumBufferSize) + { + _ednsBufferSize = MaximumBufferSize; + } + else + { + _ednsBufferSize = value; + } + } + } + + /// + /// Gets or sets a flag indicating whether EDNS should be enabled and the DO flag should be set. + /// Defaults to False. + /// + public bool RequestDnsSecRecords { get; set; } = false; + + /// + /// Converts the query options into readonly settings. + /// + /// The options. + public static implicit operator DnsQuerySettings(DnsQueryOptions fromOptions) + { + if (fromOptions == null) + { + return null; + } + + return new DnsQuerySettings(fromOptions); + } + } + + /// + /// The options used to override the defaults of per query. + /// + public class DnsQueryAndServerOptions : DnsQueryOptions + { + // TODO: evalualte if defaulting resolveNameServers to false here and in the query plan, fall back to configured nameservers of the client + /// + /// Creates a new instance of without name servers. + /// + /// + /// If no nameservers are configured, a query will fallback to the nameservers already configured on the instance. + /// + /// If set to true, + /// will be used to get a list of nameservers. + public DnsQueryAndServerOptions(bool resolveNameServers = false) + : this(resolveNameServers ? NameServer.ResolveNameServers()?.ToArray() : null) + { + AutoResolvedNameServers = resolveNameServers; + } + + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public DnsQueryAndServerOptions(params NameServer[] nameServers) + : base() + { + if (nameServers != null && nameServers.Length > 0) + { + NameServers = nameServers.ToList(); + } + } + + // TODO: remove overloads in favor of implicit conversion to NameServer? + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public DnsQueryAndServerOptions(params IPEndPoint[] nameServers) + : this(nameServers?.Select(p => (NameServer)p).ToArray()) + { + } + + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public DnsQueryAndServerOptions(params IPAddress[] nameServers) + : this(nameServers?.Select(p => (NameServer)p).ToArray()) + { + } + + /// + /// Gets a flag indicating whether the name server collection was manually defined or automatically resolved + /// + public bool AutoResolvedNameServers { get; } + + /// + /// Gets or sets a list of name servers which should be used to query. + /// + public IList NameServers { get; set; } = new List(); + + /// + /// Converts the query options into readonly settings. + /// + /// The options. + public static implicit operator DnsQueryAndServerSettings(DnsQueryAndServerOptions fromOptions) + { + if (fromOptions == null) + { + return null; + } + + return new DnsQueryAndServerSettings(fromOptions); + } + } + + /// + /// The options used to configure defaults in and to optionally use specific settings per query. + /// + public class LookupClientOptions : DnsQueryAndServerOptions + { + private static readonly TimeSpan s_infiniteTimeout = System.Threading.Timeout.InfiniteTimeSpan; + + // max is 24 days + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + + private TimeSpan? _minimumCacheTimeout; + private TimeSpan? _maximumCacheTimeout; + + /// + /// Creates a new instance of without name servers. + /// + public LookupClientOptions(bool resolveNameServers = true) + : base(resolveNameServers) + { + } + + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public LookupClientOptions(params NameServer[] nameServers) + : base(nameServers) + { + } + + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public LookupClientOptions(params IPEndPoint[] nameServers) + : base(nameServers) + { + } + + /// + /// Creates a new instance of . + /// + /// A collection of name servers. + public LookupClientOptions(params IPAddress[] nameServers) + : base(nameServers) + { + } + + /// + /// 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. + /// Default is Null. + /// + /// This is useful in case the server retruns records with zero TTL. + /// + /// + /// + /// This setting gets igonred in case is set to False, + /// or the value is set to Null or . + /// The maximum value is 24 days or (choose a wise setting). + /// + public TimeSpan? MinimumCacheTimeout + { + get { return _minimumCacheTimeout; } + set + { + if (value.HasValue && + (value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (value == TimeSpan.Zero) + { + _minimumCacheTimeout = null; + } + else + { + _minimumCacheTimeout = value; + } + } + } + + /// + /// Gets a which can override the TTL of a resource record in case the + /// TTL of the record is higher than this maximum value. + /// Default is Null. + /// + /// + /// This setting gets igonred in case is set to False, + /// or the value is set to Null, or . + /// The maximum value is 24 days (which shouldn't be used). + /// + public TimeSpan? MaximumCacheTimeout + { + get { return _maximumCacheTimeout; } + set + { + if (value.HasValue && + (value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (value == TimeSpan.Zero) + { + _maximumCacheTimeout = null; + } + else + { + _maximumCacheTimeout = value; + } + } + } + + /// + /// Converts the options into readonly settings. + /// + /// The options. + public static implicit operator LookupClientSettings(LookupClientOptions fromOptions) + { + if (fromOptions == null) + { + return null; + } + + return new LookupClientSettings(fromOptions); + } + } + +#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() - intentionally, Equals is only used by unit tests + + /// + /// The options used to override the defaults of per query. + /// + public class DnsQuerySettings : IEquatable + { + /// + /// Gets a flag indicating whether each will contain a full documentation of the response(s). + /// Default is False. + /// + /// + public bool EnableAuditTrail { get; } + + /// + /// Gets a flag indicating whether DNS queries should use response caching or not. + /// The cache duration is calculated by the resource record of the response. Usually, the lowest TTL is used. + /// Default is True. + /// + /// + /// In case the DNS Server returns records with a TTL of zero. The response cannot be cached. + /// + public bool UseCache { get; } + + /// + /// Gets a flag indicating whether DNS queries should instruct the DNS server to do recursive lookups, or not. + /// Default is True. + /// + /// The flag indicating if recursion should be used or not. + public bool Recursion { get; } + + /// + /// Gets the number of tries to get a response from one name server before trying the next one. + /// Only transient errors, like network or connection errors will be retried. + /// Default is 5. + /// + /// If all configured error out after retries, an exception will be thrown at the end. + /// + /// + /// The number of retries. + public int Retries { get; } + + /// + /// Gets a flag indicating whether the should throw a + /// in case the query result has a other than . + /// Default is False. + /// + /// + /// + /// If set to False, the query will return a result with an + /// which contains more information. + /// + /// + /// If set to True, any query method of will throw an if + /// the response header indicates an error. + /// + /// + /// If both, and are set to True, + /// will continue to query all configured . + /// If none of the servers yield a valid response, a will be thrown + /// with the error of the last response. + /// + /// + /// + /// + public bool ThrowDnsErrors { get; } + + /// + /// Gets a flag indicating whether the can cycle through all + /// configured on each consecutive request, basically using a random server, or not. + /// Default is True. + /// If only one is configured, this setting is not used. + /// + /// + /// + /// If False, configured endpoint will be used in random order. + /// If True, the order will be preserved. + /// + /// + /// Even if is set to True, the endpoint might still get + /// disabled and might not being used for some time if it errors out, e.g. no connection can be established. + /// + /// + public bool UseRandomNameServer { get; } + + /// + /// Gets a flag indicating whether to query the next configured in case the response of the last query + /// returned a other than . + /// Default is True. + /// + /// + /// If True, lookup client will continue until a server returns a valid result, or, + /// if no yield a valid result, the last response with the error will be returned. + /// In case no server yields a valid result and is also enabled, an exception + /// will be thrown containing the error of the last response. + /// + /// If True and is enabled, the exception will be thrown on first encounter without trying any other servers. + /// + /// + /// + public bool ContinueOnDnsError { get; } + + /// + /// Gets or sets a flag indicating whether to query the next configured + /// if the response does not have an error but the query was not answered by the response. + /// Default is True. + /// + /// + /// The query is answered if there is at least one in the answers section + /// matching the 's . + /// + /// If there are zero answeres in the response, the query is not answered, independent of the . + /// If there are answers in the response, the is used to find a matching record, + /// query types and will be ignored by this check. + /// + /// + public bool ContinueOnEmptyResponse { get; } + + /// + /// Gets the request timeout in milliseconds. is used for limiting the connection and request time for one operation. + /// Timeout must be greater than zero and less than . + /// If (or -1) is used, no timeout will be applied. + /// Default is 5 seconds. + /// + /// + /// If a very short timeout is configured, queries will more likely result in s. + /// + /// Important to note, s will be retried, if are not disabled (set to 0). + /// This should help in case one or more configured DNS servers are not reachable or under load for example. + /// + /// + public TimeSpan Timeout { get; } + + /// + /// Gets a flag indicating whether Tcp should be used in case a Udp response is truncated. + /// Default is True. + /// + /// If False, truncated results will potentially yield no or incomplete answers. + /// + /// + public bool UseTcpFallback { get; } + + /// + /// Gets a flag indicating whether Udp should not be used at all. + /// Default is False. + /// + /// Enable this only if Udp cannot be used because of your firewall rules for example. + /// Also, zone transfers (see ) must use TCP only. + /// + /// + public bool UseTcpOnly { get; } + + /// + /// Gets a flag indicating whether EDNS is enabled based on the values + /// of and . + /// + public bool UseExtendedDns => ExtendedDnsBufferSize > DnsQueryOptions.MinimumBufferSize || RequestDnsSecRecords; + + /// + /// Gets the maximum buffer used for UDP requests. + /// Defaults to 4096. + /// + /// If this value is less or equal to 512 bytes, EDNS might be disabled. + /// + /// + public int ExtendedDnsBufferSize { get; } + + /// + /// Gets a flag indicating whether EDNS should be enabled and the DO flag should be set. + /// Defaults to False. + /// + public bool RequestDnsSecRecords { get; } + + /// + /// Creates a new instance of . + /// + public DnsQuerySettings(DnsQueryOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + ContinueOnDnsError = options.ContinueOnDnsError; + ContinueOnEmptyResponse = options.ContinueOnEmptyResponse; + EnableAuditTrail = options.EnableAuditTrail; + Recursion = options.Recursion; + Retries = options.Retries; + ThrowDnsErrors = options.ThrowDnsErrors; + Timeout = options.Timeout; + UseCache = options.UseCache; + UseRandomNameServer = options.UseRandomNameServer; + UseTcpFallback = options.UseTcpFallback; + UseTcpOnly = options.UseTcpOnly; + ExtendedDnsBufferSize = options.ExtendedDnsBufferSize; + RequestDnsSecRecords = options.RequestDnsSecRecords; + } + + /// + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return Equals(obj as DnsQuerySettings); + } + + /// + public bool Equals(DnsQuerySettings other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return EnableAuditTrail == other.EnableAuditTrail && + UseCache == other.UseCache && + Recursion == other.Recursion && + Retries == other.Retries && + ThrowDnsErrors == other.ThrowDnsErrors && + UseRandomNameServer == other.UseRandomNameServer && + ContinueOnDnsError == other.ContinueOnDnsError && + ContinueOnEmptyResponse == other.ContinueOnEmptyResponse && + Timeout.Equals(other.Timeout) && + UseTcpFallback == other.UseTcpFallback && + UseTcpOnly == other.UseTcpOnly && + ExtendedDnsBufferSize == other.ExtendedDnsBufferSize && + RequestDnsSecRecords == other.RequestDnsSecRecords; + } + } + + /// + /// The readonly version of used to customize settings per query. + /// + public class DnsQueryAndServerSettings : DnsQuerySettings, IEquatable + { + private readonly NameServer[] _endpoints; + private readonly Random _rnd = new Random(); + + /// + /// Gets a collection of name servers which should be used to query. + /// + public IReadOnlyList NameServers => _endpoints; + + /// + /// Gets a flag indicating whether the name server collection was manually defined or automatically resolved + /// + public bool AutoResolvedNameServers { get; } + + /// + /// Creates a new instance of . + /// + public DnsQueryAndServerSettings(DnsQueryAndServerOptions options) + : base(options) + { + _endpoints = options.NameServers?.ToArray() ?? new NameServer[0]; + + AutoResolvedNameServers = options.AutoResolvedNameServers; + } + + /// + /// Creates a new instance of . + /// + public DnsQueryAndServerSettings(DnsQueryAndServerOptions options, IReadOnlyCollection overrideServers) + : this(options) + { + _endpoints = overrideServers?.ToArray() ?? throw new ArgumentNullException(nameof(overrideServers)); + } + + /// + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return Equals(obj as DnsQueryAndServerSettings); + } + + /// + public bool Equals(DnsQueryAndServerSettings other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return NameServers.SequenceEqual(other.NameServers) + && AutoResolvedNameServers == other.AutoResolvedNameServers + && base.Equals(other); + } + + internal IReadOnlyList ShuffleNameServers() + { + if (_endpoints.Length > 1 && UseRandomNameServer) + { + var servers = _endpoints.ToArray(); + + for (var i = servers.Length; i > 0; i--) + { + var j = _rnd.Next(0, i); + var temp = servers[j]; + servers[j] = servers[i - 1]; + servers[i - 1] = temp; + } + + return servers; + } + + return NameServers; + } + } + + /// + /// The readonly version of used as default settings in . + /// + public class LookupClientSettings : DnsQueryAndServerSettings, IEquatable + { + /// + /// Creates a new instance of . + /// + public LookupClientSettings(LookupClientOptions options) + : base(options) + { + MinimumCacheTimeout = options.MinimumCacheTimeout; + MaximumCacheTimeout = options.MaximumCacheTimeout; + } + + /// + /// Gets a which can override the TTL of a resource record in case the + /// TTL of the record is lower than this minimum value. + /// Default is Null. + /// + /// This is useful in cases where the server retruns records with zero TTL. + /// + /// + /// + /// This setting gets igonred in case is set to False. + /// The maximum value is 24 days or . + /// + public TimeSpan? MinimumCacheTimeout { get; } + + /// + /// Gets a which can override the TTL of a resource record in case the + /// TTL of the record is higher than this maximum value. + /// Default is Null. + /// + /// + /// This setting gets igonred in case is set to False. + /// The maximum value is 24 days. + /// Setting it to would be equal to not providing a value. + /// + public TimeSpan? MaximumCacheTimeout { get; } + + /// + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return Equals(obj as LookupClientSettings); + } + + /// + public bool Equals(LookupClientSettings other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(MinimumCacheTimeout, other.MinimumCacheTimeout) + && Equals(MaximumCacheTimeout, other.MaximumCacheTimeout) + && base.Equals(other); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings Copy( + IReadOnlyCollection nameServers, + TimeSpan? minimumCacheTimeout, + bool? continueOnDnsError = null, + bool? enableAuditTrail = null, + bool? recursion = null, + int? retries = null, + bool? throwDnsErrors = null, + TimeSpan? timeout = null, + bool? useCache = null, + bool? useRandomNameServer = null, + bool? useTcpFallback = null, + bool? useTcpOnly = null) + { + // auto resolved flag might get lost here. But this stuff gets deleted anyways. + return new LookupClientOptions(nameServers?.ToArray()) + { + MinimumCacheTimeout = minimumCacheTimeout, + ContinueOnDnsError = continueOnDnsError ?? ContinueOnDnsError, + EnableAuditTrail = enableAuditTrail ?? EnableAuditTrail, + Recursion = recursion ?? Recursion, + Retries = retries ?? Retries, + ThrowDnsErrors = throwDnsErrors ?? ThrowDnsErrors, + Timeout = timeout ?? Timeout, + UseCache = useCache ?? UseCache, + UseRandomNameServer = useRandomNameServer ?? UseRandomNameServer, + UseTcpFallback = useTcpFallback ?? UseTcpFallback, + UseTcpOnly = useTcpOnly ?? UseTcpOnly, + MaximumCacheTimeout = MaximumCacheTimeout, + ExtendedDnsBufferSize = ExtendedDnsBufferSize, + RequestDnsSecRecords = RequestDnsSecRecords, + ContinueOnEmptyResponse = ContinueOnEmptyResponse + }; + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithContinueOnDnsError(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, continueOnDnsError: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithEnableAuditTrail(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, enableAuditTrail: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithMinimumCacheTimeout(TimeSpan? value) + { + return Copy(NameServers, minimumCacheTimeout: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithRecursion(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, recursion: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithRetries(int value) + { + return Copy(NameServers, MinimumCacheTimeout, retries: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithThrowDnsErrors(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, throwDnsErrors: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithTimeout(TimeSpan value) + { + return Copy(NameServers, MinimumCacheTimeout, timeout: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithUseCache(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, useCache: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithUseTcpFallback(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, useTcpFallback: value); + } + + // TODO: remove if LookupClient settings can be made readonly + internal LookupClientSettings WithUseTcpOnly(bool value) + { + return Copy(NameServers, MinimumCacheTimeout, useTcpOnly: value); + } + } + +#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() +} \ No newline at end of file diff --git a/src/DnsClient/DnsQueryResponse.cs b/src/DnsClient/DnsQueryResponse.cs index a9fbb4c8..4b35d39b 100644 --- a/src/DnsClient/DnsQueryResponse.cs +++ b/src/DnsClient/DnsQueryResponse.cs @@ -5,6 +5,33 @@ namespace DnsClient { + internal class TruncatedQueryResponse : IDnsQueryResponse + { + public IReadOnlyList Questions => throw new NotImplementedException(); + + public IReadOnlyList Additionals => throw new NotImplementedException(); + + public IEnumerable AllRecords => throw new NotImplementedException(); + + public IReadOnlyList Answers => throw new NotImplementedException(); + + public IReadOnlyList Authorities => throw new NotImplementedException(); + + public string AuditTrail => throw new NotImplementedException(); + + public string ErrorMessage => throw new NotImplementedException(); + + public bool HasError => throw new NotImplementedException(); + + public DnsResponseHeader Header => throw new NotImplementedException(); + + public int MessageSize => throw new NotImplementedException(); + + public NameServer NameServer => throw new NotImplementedException(); + + public DnsQuerySettings Settings => throw new NotImplementedException(); + } + /// /// The response returned by any query performed by with all answer sections, header and message information. /// @@ -44,7 +71,7 @@ public IEnumerable AllRecords /// /// The audit trail. /// - public string AuditTrail => Audit?.Build(this); + public string AuditTrail { get; private set; } /// /// Gets a list of answer records. @@ -60,12 +87,12 @@ public IEnumerable AllRecords /// Returns a string value representing the error response code in case an error occured, /// otherwise ''. /// - public string ErrorMessage => DnsResponseCodeText.GetErrorText(Header.ResponseCode); + public string ErrorMessage => DnsResponseCodeText.GetErrorText((DnsResponseCode)Header.ResponseCode); /// /// A flag indicating if the header contains a response codde other than . /// - public bool HasError => Header?.ResponseCode != DnsResponseCode.NoError; + public bool HasError => Header?.ResponseCode != DnsHeaderResponseCode.NoError; /// /// Gets the header of the response. @@ -90,9 +117,7 @@ public IEnumerable AllRecords /// public DnsQuerySettings Settings { get; } - internal LookupClientAudit Audit { get; } - - internal DnsQueryResponse(DnsResponseMessage dnsResponseMessage, NameServer nameServer, LookupClientAudit audit, DnsQuerySettings settings) + internal DnsQueryResponse(DnsResponseMessage dnsResponseMessage, NameServer nameServer, DnsQuerySettings settings) { if (dnsResponseMessage == null) throw new ArgumentNullException(nameof(dnsResponseMessage)); Header = dnsResponseMessage.Header; @@ -102,7 +127,6 @@ internal DnsQueryResponse(DnsResponseMessage dnsResponseMessage, NameServer name Additionals = dnsResponseMessage.Additionals.ToArray(); Authorities = dnsResponseMessage.Authorities.ToArray(); NameServer = nameServer ?? throw new ArgumentNullException(nameof(nameServer)); - Audit = audit; Settings = settings; } @@ -135,5 +159,13 @@ public override int GetHashCode() return _hashCode.Value; } + + internal static void SetAuditTrail(IDnsQueryResponse response, string value) + { + if (response is DnsQueryResponse queryResponse) + { + queryResponse.AuditTrail = value; + } + } } } \ No newline at end of file diff --git a/src/DnsClient/DnsRecordFactory.cs b/src/DnsClient/DnsRecordFactory.cs index d19c5eb3..556aa096 100644 --- a/src/DnsClient/DnsRecordFactory.cs +++ b/src/DnsClient/DnsRecordFactory.cs @@ -148,9 +148,7 @@ public DnsResourceRecord GetRecord(ResourceRecordInfo info) break; default: - // update reader index because we don't read full data for the empty record - _reader.Advance(info.RawDataLength); - result = new EmptyRecord(info); + result = new UnknownRecord(info, _reader.ReadBytes(info.RawDataLength).ToArray()); break; } @@ -172,7 +170,7 @@ private DnsResourceRecord ResolveOptRecord(ResourceRecordInfo info) { // Consume bytes in case the OPT record has any. var bytes = _reader.ReadBytes(info.RawDataLength).ToArray(); - return new OptRecord((int)info.RecordClass, info.InitialTimeToLive, info.RawDataLength, bytes); + return new OptRecord((int)info.RecordClass, ttlFlag: info.InitialTimeToLive, length: info.RawDataLength, data: bytes); } private DnsResourceRecord ResolveWksRecord(ResourceRecordInfo info) diff --git a/src/DnsClient/DnsRequestHeader.cs b/src/DnsClient/DnsRequestHeader.cs index ee860928..9217bfbb 100644 --- a/src/DnsClient/DnsRequestHeader.cs +++ b/src/DnsClient/DnsRequestHeader.cs @@ -5,8 +5,8 @@ namespace DnsClient { internal class DnsRequestHeader { + private static readonly Random s_random = new Random(); public const int HeaderLength = 12; - private ushort _flags = 0; public ushort RawFlags => _flags; @@ -31,7 +31,7 @@ internal DnsHeaderFlag HeaderFlags } } - public int Id { get; set; } + public int Id { get; private set; } public DnsOpCode OpCode { @@ -78,14 +78,14 @@ public bool UseRecursion } } - public DnsRequestHeader(int id, DnsOpCode queryKind) - : this(id, true, queryKind) + public DnsRequestHeader(DnsOpCode queryKind) + : this(true, queryKind) { } - public DnsRequestHeader(int id, bool useRecursion, DnsOpCode queryKind) + public DnsRequestHeader(bool useRecursion, DnsOpCode queryKind) { - Id = id; + Id = GetNextUniqueId(); OpCode = queryKind; UseRecursion = useRecursion; } @@ -94,5 +94,15 @@ public override string ToString() { return $"{Id} - Qs: {1} Recursion: {UseRecursion} OpCode: {OpCode}"; } + + public void RefreshId() + { + Id = GetNextUniqueId(); + } + + private ushort GetNextUniqueId() + { + return (ushort)s_random.Next(1, ushort.MaxValue); + } } } \ No newline at end of file diff --git a/src/DnsClient/DnsRequestMessage.cs b/src/DnsClient/DnsRequestMessage.cs index 19a2c1c5..62337a62 100644 --- a/src/DnsClient/DnsRequestMessage.cs +++ b/src/DnsClient/DnsRequestMessage.cs @@ -6,16 +6,25 @@ namespace DnsClient /// /// Represents a simple request message which can be send through . /// + [System.Diagnostics.DebuggerDisplay("Request:{Header} => {Question}")] internal class DnsRequestMessage { public DnsRequestHeader Header { get; } public DnsQuestion Question { get; } - public DnsRequestMessage(DnsRequestHeader header, DnsQuestion question) + public DnsQuerySettings QuerySettings { get; } + + public DnsRequestMessage(DnsRequestHeader header, DnsQuestion question, DnsQuerySettings dnsQuerySettings = null) { Header = header ?? throw new ArgumentNullException(nameof(header)); Question = question ?? throw new ArgumentNullException(nameof(question)); + QuerySettings = dnsQuerySettings ?? new DnsQuerySettings(new DnsQueryOptions()); + } + + public override string ToString() + { + return $"{Header} => {Question}"; } } } \ No newline at end of file diff --git a/src/DnsClient/DnsResponseCode.cs b/src/DnsClient/DnsResponseCode.cs index eab11041..9c9ff617 100644 --- a/src/DnsClient/DnsResponseCode.cs +++ b/src/DnsClient/DnsResponseCode.cs @@ -8,10 +8,10 @@ namespace DnsClient */ /// - /// Response codes of the . + /// 4 bit response codes of the 's header. /// /// RFC 6895 - public enum DnsResponseCode : short + public enum DnsHeaderResponseCode : short { /// /// No error condition @@ -83,6 +83,135 @@ public enum DnsResponseCode : short /// RFC 2136 NotZone = 10, + + /// + /// Unassigned value + /// + Unassinged11 = 11, + + /// + /// Unassigned value + /// + Unassinged12 = 12, + + /// + /// Unassigned value + /// + Unassinged13 = 13, + + /// + /// Unassigned value + /// + Unassinged14 = 14, + + /// + /// Unassigned value + /// + Unassinged15 = 15 + } + + /// + /// Extended response codes of the with OPT. + /// + /// RFC 6895 + public enum DnsResponseCode + { + /// + /// No error condition + /// + /// RFC 1035 + NoError = 0, + + /// + /// Format error. The name server was unable to interpret the query. + /// + /// RFC 1035 + FormatError = 1, + + /// + /// Server failure. The name server was unable to process this query due to a problem with the name server. + /// + /// RFC 1035 + ServerFailure = 2, + + /// + /// Name Error. Meaningful only for responses from an authoritative name server, + /// this code signifies that the domain name referenced in the query does not exist. + /// + /// RFC 1035 + NotExistentDomain = 3, + + /// + /// Not Implemented. The name server does not support the requested kind of query. + /// + /// RFC 1035 + NotImplemented = 4, + + /// + /// Refused. The name server refuses to perform the specified operation for policy reasons. + /// For example, a name server may not wish to provide the information to the particular requester, + /// or a name server may not wish to perform a particular operation (e.g., zone transfer) for particular data. + /// + /// RFC 1035 + Refused = 5, + + /// + /// Name Exists when it should not. + /// + /// RFC 2136 + ExistingDomain = 6, + + /// + /// Resource record set exists when it should not. + /// + /// RFC 2136 + ExistingResourceRecordSet = 7, + + /// + /// Resource record set that should exist but does not. + /// + /// RFC 2136 + MissingResourceRecordSet = 8, + + /// + /// Server Not Authoritative for zone / Not Authorized. + /// + /// RFC 2136 + /// RFC 2845 + NotAuthorized = 9, + + /// + /// Name not contained in zone. + /// + /// RFC 2136 + NotZone = 10, + + + /// + /// Unassigned value + /// + Unassinged11 = 11, + + /// + /// Unassigned value + /// + Unassinged12 = 12, + + /// + /// Unassigned value + /// + Unassinged13 = 13, + + /// + /// Unassigned value + /// + Unassinged14 = 14, + + /// + /// Unassigned value + /// + Unassinged15 = 15, + /// /// Bad OPT Version or TSIG Signature Failure. /// diff --git a/src/DnsClient/DnsResponseHeader.cs b/src/DnsClient/DnsResponseHeader.cs index a39ffc1a..09686f19 100644 --- a/src/DnsClient/DnsResponseHeader.cs +++ b/src/DnsClient/DnsResponseHeader.cs @@ -115,7 +115,7 @@ public class DnsResponseHeader /// /// The response code. /// - public DnsResponseCode ResponseCode => (DnsResponseCode)(_flags & DnsHeader.RCodeMask); + public DnsHeaderResponseCode ResponseCode => (DnsHeaderResponseCode)(_flags & DnsHeader.RCodeMask); /// /// Gets a value indicating whether the result was truncated. @@ -163,7 +163,7 @@ public DnsResponseHeader(int id, ushort flags, int questionCount, int answerCoun /// public override string ToString() { - var head = $";; ->>HEADER<<- opcode: {OPCode}, status: {DnsResponseCodeText.GetErrorText(ResponseCode)}, id: {Id}"; + var head = $";; ->>HEADER<<- opcode: {OPCode}, status: {DnsResponseCodeText.GetErrorText((DnsResponseCode)ResponseCode)}, id: {Id}"; var flags = new string[] { HasQuery ? "qr" : "", HasAuthorityAnswer ? "aa" : "", @@ -176,7 +176,7 @@ public override string ToString() var flagsString = string.Join(" ", flags.Where(p => p != "")); return $"{head}\r\n;; flags: {flagsString}; QUERY: {QuestionCount}, " + - $"ANSWER: {AnswerCount}, AUTORITY: {NameServerCount}, ADDITIONAL: {AdditionalCount}"; + $"ANSWER: {AnswerCount}, AUTHORITY: {NameServerCount}, ADDITIONAL: {AdditionalCount}"; } } } \ No newline at end of file diff --git a/src/DnsClient/DnsResponseMessage.cs b/src/DnsClient/DnsResponseMessage.cs index a2fe36f6..50d01b43 100644 --- a/src/DnsClient/DnsResponseMessage.cs +++ b/src/DnsClient/DnsResponseMessage.cs @@ -65,13 +65,11 @@ public void AddQuestion(DnsQuestion question) Questions.Add(question); } - internal LookupClientAudit Audit { get; set; } - /// /// Gets the readonly representation of this message which can be returned. /// public DnsQueryResponse AsQueryResponse(NameServer nameServer, DnsQuerySettings settings) - => new DnsQueryResponse(this, nameServer, Audit, settings); + => new DnsQueryResponse(this, nameServer, settings); public static DnsResponseMessage Combine(List messages) { diff --git a/src/DnsClient/DnsResponseParseException.cs b/src/DnsClient/DnsResponseParseException.cs index 25edd37c..a479c613 100644 --- a/src/DnsClient/DnsResponseParseException.cs +++ b/src/DnsClient/DnsResponseParseException.cs @@ -3,6 +3,7 @@ namespace DnsClient { using System; using System.Globalization; + using System.Linq; public class DnsResponseParseException : Exception @@ -11,7 +12,7 @@ public class DnsResponseParseException : Exception public int Index { get; } - public int Length { get; } + public int ReadLength { get; } public DnsResponseParseException() { @@ -28,15 +29,35 @@ public DnsResponseParseException(string message, Exception innerException) } public DnsResponseParseException(string message, byte[] data, int index = 0, int length = 0, Exception innerException = null) - : this(DefaultMessage(data.Length, index, length, message), innerException) + : this(DefaultMessage(data.Length, index, length, message, FormatData(data, index, length)), innerException) { ResponseData = data ?? throw new ArgumentNullException(nameof(data)); Index = index; - Length = length; + ReadLength = length; } - private static readonly Func DefaultMessage = (dataLength, index, length, message) - => string.Format(CultureInfo.InvariantCulture, "Response parser error, {0} bytes available, tried to read {1} bytes at index {2}. {3}", dataLength, length, index, message); + // Formats the data array to not spam too much data into the exception but + // at least have all bytes from the beginning to the position around where it failed. + private static string FormatData(byte[] data, int index, int length) + { + if (data == null || data.Length == 0) + { + return string.Empty; + } + + return Convert.ToBase64String(data.Take(index + length + 100).ToArray()); + } + + private static readonly Func DefaultMessage = (dataLength, index, length, message, dataDump) + => string.Format( + CultureInfo.InvariantCulture, + "Response parser error, {1} bytes available, tried to read {2} bytes at index {3}.{0}{4}{0}[{5}].", + Environment.NewLine, + dataLength, + length, + index, + message, + dataDump); } } #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/DnsClient/DnsTcpMessageHandler.cs b/src/DnsClient/DnsTcpMessageHandler.cs index b4be479d..50a18882 100644 --- a/src/DnsClient/DnsTcpMessageHandler.cs +++ b/src/DnsClient/DnsTcpMessageHandler.cs @@ -15,11 +15,7 @@ internal class DnsTcpMessageHandler : DnsMessageHandler { private readonly ConcurrentDictionary _pools = new ConcurrentDictionary(); - public override bool IsTransientException(T exception) - { - //if (exception is SocketException) return true; - return false; - } + public override DnsMessageHandleType Type { get; } = DnsMessageHandleType.TCP; public override DnsResponseMessage Query(IPEndPoint endpoint, DnsRequestMessage request, TimeSpan timeout) { diff --git a/src/DnsClient/DnsUdpMessageHandler.cs b/src/DnsClient/DnsUdpMessageHandler.cs index 6a1fd474..55bc23ac 100644 --- a/src/DnsClient/DnsUdpMessageHandler.cs +++ b/src/DnsClient/DnsUdpMessageHandler.cs @@ -16,17 +16,13 @@ internal class DnsUdpMessageHandler : DnsMessageHandler private static ConcurrentQueue _clientsIPv6 = new ConcurrentQueue(); private readonly bool _enableClientQueue; + public override DnsMessageHandleType Type { get; } = DnsMessageHandleType.UDP; + public DnsUdpMessageHandler(bool enableClientQueue) { _enableClientQueue = enableClientQueue; } - public override bool IsTransientException(T exception) - { - if (exception is SocketException) return true; - return false; - } - public override DnsResponseMessage Query( IPEndPoint server, DnsRequestMessage request, diff --git a/src/DnsClient/ILookupClient.cs b/src/DnsClient/ILookupClient.cs index 9643ed33..ef84237b 100644 --- a/src/DnsClient/ILookupClient.cs +++ b/src/DnsClient/ILookupClient.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; @@ -59,870 +58,4 @@ public interface ILookupClient : IDnsQuery #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } - - /// - /// The options used to override the defaults of per query. - /// - public class DnsQueryOptions - { - 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 TimeSpan _timeout = s_defaultTimeout; - - /// - /// Gets or sets a flag indicating whether each will contain a full documentation of the response(s). - /// Default is False. - /// - /// - public bool EnableAuditTrail { get; set; } = false; - - /// - /// Gets or sets a flag indicating whether DNS queries should use response caching or not. - /// The cache duration is calculated by the resource record of the response. Usually, the lowest TTL is used. - /// Default is True. - /// - /// - /// In case the DNS Server returns records with a TTL of zero. The response cannot be cached. - /// - public bool UseCache { get; set; } = true; - - /// - /// Gets or sets a flag indicating whether DNS queries should instruct the DNS server to do recursive lookups, or not. - /// Default is True. - /// - /// The flag indicating if recursion should be used or not. - public bool Recursion { get; set; } = true; - - /// - /// Gets or sets the number of tries to get a response from one name server before trying the next one. - /// Only transient errors, like network or connection errors will be retried. - /// Default is 5. - /// - /// If all configured error out after retries, an exception will be thrown at the end. - /// - /// - /// The number of retries. - public int Retries { get; set; } = 5; - - /// - /// Gets or sets a flag indicating whether the should throw a - /// in case the query result has a other than . - /// Default is False. - /// - /// - /// - /// If set to False, the query will return a result with an - /// which contains more information. - /// - /// - /// If set to True, any query method of will throw an if - /// the response header indicates an error. - /// - /// - /// If both, and are set to True, - /// will continue to query all configured . - /// If none of the servers yield a valid response, a will be thrown - /// with the error of the last response. - /// - /// - /// - /// - public bool ThrowDnsErrors { get; set; } = false; - - /// - /// Gets or sets a flag indicating whether the can cycle through all - /// configured on each consecutive request, basically using a random server, or not. - /// Default is True. - /// If only one is configured, this setting is not used. - /// - /// - /// - /// If False, configured endpoint will be used in random order. - /// If True, the order will be preserved. - /// - /// - /// Even if is set to True, the endpoint might still get - /// disabled and might not being used for some time if it errors out, e.g. no connection can be established. - /// - /// - public bool UseRandomNameServer { get; set; } = true; - - /// - /// Gets or sets a flag indicating whether to query the next configured in case the response of the last query - /// returned a other than . - /// Default is True. - /// - /// - /// If True, lookup client will continue until a server returns a valid result, or, - /// if no yield a valid result, the last response with the error will be returned. - /// In case no server yields a valid result and is also enabled, an exception - /// will be thrown containing the error of the last response. - /// - /// - public bool ContinueOnDnsError { get; set; } = true; - - /// - /// Gets or sets the request timeout in milliseconds. is used for limiting the connection and request time for one operation. - /// Timeout must be greater than zero and less than . - /// If (or -1) is used, no timeout will be applied. - /// Default is 5 seconds. - /// - /// - /// If a very short timeout is configured, queries will more likely result in s. - /// - /// Important to note, s will be retried, if are not disabled (set to 0). - /// This should help in case one or more configured DNS servers are not reachable or under load for example. - /// - /// - 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 whether Tcp should be used in case a Udp response is truncated. - /// Default is True. - /// - /// If False, truncated results will potentially yield no or incomplete answers. - /// - /// - public bool UseTcpFallback { get; set; } = true; - - /// - /// Gets or sets a flag indicating whether Udp should not be used at all. - /// Default is False. - /// - /// Enable this only if Udp cannot be used because of your firewall rules for example. - /// Also, zone transfers (see ) must use TCP only. - /// - /// - public bool UseTcpOnly { get; set; } = false; - - /// - /// Converts the query options into readonly settings. - /// - /// The options. - public static implicit operator DnsQuerySettings(DnsQueryOptions fromOptions) - { - if (fromOptions == null) - { - return null; - } - - return new DnsQuerySettings(fromOptions); - } - } - - /// - /// The options used to override the defaults of per query. - /// - public class DnsQueryAndServerOptions : DnsQueryOptions - { - // TODO: evalualte if defaulting resolveNameServers to false here and in the query plan, fall back to configured nameservers of the client - /// - /// Creates a new instance of without name servers. - /// - /// - /// If no nameservers are configured, a query will fallback to the nameservers already configured on the instance. - /// - /// If set to true, - /// will be used to get a list of nameservers. - public DnsQueryAndServerOptions(bool resolveNameServers = false) - : this(resolveNameServers ? NameServer.ResolveNameServers() : null) - { - AutoResolvedNameServers = resolveNameServers; - } - - /// - /// Creates a new instance of with one name server. - /// or can be used as well thanks to implicit conversion. - /// - /// The name servers. - public DnsQueryAndServerOptions(NameServer nameServer) - : this(new[] { nameServer }) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public DnsQueryAndServerOptions(IReadOnlyCollection nameServers) - : base() - { - if (nameServers != null && nameServers.Count > 0) - { - NameServers = nameServers.ToList(); - } - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public DnsQueryAndServerOptions(params NameServer[] nameServers) - : base() - { - if (nameServers != null && nameServers.Length > 0) - { - NameServers = nameServers.ToList(); - } - } - - // TODO: remove overloads in favor of implicit conversion to NameServer? - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public DnsQueryAndServerOptions(params IPEndPoint[] nameServers) - : this(nameServers.Select(p => (NameServer)p).ToArray()) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public DnsQueryAndServerOptions(params IPAddress[] nameServers) - : this(nameServers.Select(p => (NameServer)p).ToArray()) - { - } - - /// - /// Gets a flag indicating whether the name server collection was manually defined or automatically resolved - /// - public bool AutoResolvedNameServers { get; } - - /// - /// Gets or sets a list of name servers which should be used to query. - /// - public IList NameServers { get; set; } = new List(); - - /// - /// Converts the query options into readonly settings. - /// - /// The options. - public static implicit operator DnsQueryAndServerSettings(DnsQueryAndServerOptions fromOptions) - { - if (fromOptions == null) - { - return null; - } - - return new DnsQueryAndServerSettings(fromOptions); - } - } - - /// - /// The options used to configure defaults in and to optionally use specific settings per query. - /// - public class LookupClientOptions : DnsQueryAndServerOptions - { - private static readonly TimeSpan s_infiniteTimeout = System.Threading.Timeout.InfiniteTimeSpan; - - // max is 24 days - private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); - - private TimeSpan? _minimumCacheTimeout; - private TimeSpan? _maximumCacheTimeout; - - /// - /// Creates a new instance of without name servers. - /// - public LookupClientOptions(bool resolveNameServers = true) - : base(resolveNameServers) - { - } - - /// - /// Creates a new instance of with one name server. - /// or can be used as well thanks to implicit conversion. - /// - /// The name servers. - public LookupClientOptions(NameServer nameServer) - : base(nameServer) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public LookupClientOptions(params NameServer[] nameServers) - : base(nameServers) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public LookupClientOptions(params IPEndPoint[] nameServers) - : base(nameServers) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public LookupClientOptions(params IPAddress[] nameServers) - : base(nameServers) - { - } - - /// - /// Creates a new instance of . - /// - /// A collection of name servers. - public LookupClientOptions(IReadOnlyCollection nameServers) - : base(nameServers) - { - } - - /// - /// 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. - /// Default is Null. - /// - /// This is useful in cases where the server retruns records with zero TTL. - /// - /// - /// - /// This setting gets igonred in case is set to False. - /// The maximum value is 24 days or . - /// - public TimeSpan? MinimumCacheTimeout - { - get { return _minimumCacheTimeout; } - set - { - if (value.HasValue && - (value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _minimumCacheTimeout = value; - } - } - - /// - /// Gets a which can override the TTL of a resource record in case the - /// TTL of the record is higher than this maximum value. - /// Default is Null. - /// - /// - /// This setting gets igonred in case is set to False. - /// The maximum value is 24 days. - /// Setting it to would be equal to not providing a value. - /// - public TimeSpan? MaximumCacheTimeout - { - get { return _maximumCacheTimeout; } - set - { - if (value.HasValue && - (value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _maximumCacheTimeout = value; - } - } - - /// - /// Converts the options into readonly settings. - /// - /// The options. - public static implicit operator LookupClientSettings(LookupClientOptions fromOptions) - { - if (fromOptions == null) - { - return null; - } - - return new LookupClientSettings(fromOptions); - } - } - -#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() - intentionally, Equals is only used by unit tests - - /// - /// The options used to override the defaults of per query. - /// - public class DnsQuerySettings : IEquatable - { - /// - /// Gets a flag indicating whether each will contain a full documentation of the response(s). - /// Default is False. - /// - /// - public bool EnableAuditTrail { get; } - - /// - /// Gets a flag indicating whether DNS queries should use response caching or not. - /// The cache duration is calculated by the resource record of the response. Usually, the lowest TTL is used. - /// Default is True. - /// - /// - /// In case the DNS Server returns records with a TTL of zero. The response cannot be cached. - /// - public bool UseCache { get; } - - /// - /// Gets a flag indicating whether DNS queries should instruct the DNS server to do recursive lookups, or not. - /// Default is True. - /// - /// The flag indicating if recursion should be used or not. - public bool Recursion { get; } - - /// - /// Gets the number of tries to get a response from one name server before trying the next one. - /// Only transient errors, like network or connection errors will be retried. - /// Default is 5. - /// - /// If all configured error out after retries, an exception will be thrown at the end. - /// - /// - /// The number of retries. - public int Retries { get; } - - /// - /// Gets a flag indicating whether the should throw a - /// in case the query result has a other than . - /// Default is False. - /// - /// - /// - /// If set to False, the query will return a result with an - /// which contains more information. - /// - /// - /// If set to True, any query method of will throw an if - /// the response header indicates an error. - /// - /// - /// If both, and are set to True, - /// will continue to query all configured . - /// If none of the servers yield a valid response, a will be thrown - /// with the error of the last response. - /// - /// - /// - /// - public bool ThrowDnsErrors { get; } - - /// - /// Gets a flag indicating whether the can cycle through all - /// configured on each consecutive request, basically using a random server, or not. - /// Default is True. - /// If only one is configured, this setting is not used. - /// - /// - /// - /// If False, configured endpoint will be used in random order. - /// If True, the order will be preserved. - /// - /// - /// Even if is set to True, the endpoint might still get - /// disabled and might not being used for some time if it errors out, e.g. no connection can be established. - /// - /// - public bool UseRandomNameServer { get; } - - /// - /// Gets a flag indicating whether to query the next configured in case the response of the last query - /// returned a other than . - /// Default is True. - /// - /// - /// If True, lookup client will continue until a server returns a valid result, or, - /// if no yield a valid result, the last response with the error will be returned. - /// In case no server yields a valid result and is also enabled, an exception - /// will be thrown containing the error of the last response. - /// - /// - public bool ContinueOnDnsError { get; } - - /// - /// Gets the request timeout in milliseconds. is used for limiting the connection and request time for one operation. - /// Timeout must be greater than zero and less than . - /// If (or -1) is used, no timeout will be applied. - /// Default is 5 seconds. - /// - /// - /// If a very short timeout is configured, queries will more likely result in s. - /// - /// Important to note, s will be retried, if are not disabled (set to 0). - /// This should help in case one or more configured DNS servers are not reachable or under load for example. - /// - /// - public TimeSpan Timeout { get; } - - /// - /// Gets a flag indicating whether Tcp should be used in case a Udp response is truncated. - /// Default is True. - /// - /// If False, truncated results will potentially yield no or incomplete answers. - /// - /// - public bool UseTcpFallback { get; } - - /// - /// Gets a flag indicating whether Udp should not be used at all. - /// Default is False. - /// - /// Enable this only if Udp cannot be used because of your firewall rules for example. - /// Also, zone transfers (see ) must use TCP only. - /// - /// - public bool UseTcpOnly { get; } - - /// - /// Creates a new instance of . - /// - public DnsQuerySettings(DnsQueryOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - ContinueOnDnsError = options.ContinueOnDnsError; - EnableAuditTrail = options.EnableAuditTrail; - Recursion = options.Recursion; - Retries = options.Retries; - ThrowDnsErrors = options.ThrowDnsErrors; - Timeout = options.Timeout; - UseCache = options.UseCache; - UseRandomNameServer = options.UseRandomNameServer; - UseTcpFallback = options.UseTcpFallback; - UseTcpOnly = options.UseTcpOnly; - } - - /// - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return Equals(obj as DnsQuerySettings); - } - - /// - public bool Equals(DnsQuerySettings other) - { - if (other == null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return EnableAuditTrail == other.EnableAuditTrail && - UseCache == other.UseCache && - Recursion == other.Recursion && - Retries == other.Retries && - ThrowDnsErrors == other.ThrowDnsErrors && - UseRandomNameServer == other.UseRandomNameServer && - ContinueOnDnsError == other.ContinueOnDnsError && - Timeout.Equals(other.Timeout) && - UseTcpFallback == other.UseTcpFallback && - UseTcpOnly == other.UseTcpOnly; - } - } - - /// - /// The readonly version of used to customize settings per query. - /// - public class DnsQueryAndServerSettings : DnsQuerySettings, IEquatable - { - private readonly NameServer[] _endpoints; - private readonly Random _rnd = new Random(); - - /// - /// Gets a collection of name servers which should be used to query. - /// - public IReadOnlyCollection NameServers => _endpoints; - - /// - /// Gets a flag indicating whether the name server collection was manually defined or automatically resolved - /// - public bool AutoResolvedNameServers { get; } - - /// - /// Creates a new instance of . - /// - public DnsQueryAndServerSettings(DnsQueryAndServerOptions options) - : base(options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - _endpoints = options.NameServers?.ToArray() ?? new NameServer[0]; - - AutoResolvedNameServers = options.AutoResolvedNameServers; - } - - /// - /// Creates a new instance of . - /// - public DnsQueryAndServerSettings(DnsQueryAndServerOptions options, IReadOnlyCollection overrideServers) - : this(options) - { - _endpoints = overrideServers?.ToArray() ?? throw new ArgumentNullException(nameof(overrideServers)); - } - - /// - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return Equals(obj as DnsQueryAndServerSettings); - } - - /// - public bool Equals(DnsQueryAndServerSettings other) - { - if (other == null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return NameServers.SequenceEqual(other.NameServers) - && AutoResolvedNameServers == other.AutoResolvedNameServers - && base.Equals(other); - } - - internal IReadOnlyCollection ShuffleNameServers() - { - if (_endpoints.Length > 1 && UseRandomNameServer) - { - var servers = _endpoints.ToArray(); - - for (var i = servers.Length; i > 0; i--) - { - var j = _rnd.Next(0, i); - var temp = servers[j]; - servers[j] = servers[i - 1]; - servers[i - 1] = temp; - } - - return servers; - } - else - { - return NameServers; - } - } - } - - /// - /// The readonly version of used as default settings in . - /// - public class LookupClientSettings : DnsQueryAndServerSettings, IEquatable - { - /// - /// Creates a new instance of . - /// - public LookupClientSettings(LookupClientOptions options) - : base(options) - { - MinimumCacheTimeout = options.MinimumCacheTimeout; - MaximumCacheTimeout = options.MaximumCacheTimeout; - } - - /// - /// Gets a which can override the TTL of a resource record in case the - /// TTL of the record is lower than this minimum value. - /// Default is Null. - /// - /// This is useful in cases where the server retruns records with zero TTL. - /// - /// - /// - /// This setting gets igonred in case is set to False. - /// The maximum value is 24 days or . - /// - public TimeSpan? MinimumCacheTimeout { get; } - - /// - /// Gets a which can override the TTL of a resource record in case the - /// TTL of the record is higher than this maximum value. - /// Default is Null. - /// - /// - /// This setting gets igonred in case is set to False. - /// The maximum value is 24 days. - /// Setting it to would be equal to not providing a value. - /// - public TimeSpan? MaximumCacheTimeout { get; } - - /// - public override bool Equals(object obj) - { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return Equals(obj as LookupClientSettings); - } - - /// - public bool Equals(LookupClientSettings other) - { - if (other == null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return Equals(MinimumCacheTimeout, other.MinimumCacheTimeout) - && Equals(MaximumCacheTimeout, other.MaximumCacheTimeout) - && base.Equals(other); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings Copy( - IReadOnlyCollection nameServers, - TimeSpan? minimumCacheTimeout, - TimeSpan? maximumCacheTimeout = null, - bool? continueOnDnsError = null, - bool? enableAuditTrail = null, - bool? recursion = null, - int? retries = null, - bool? throwDnsErrors = null, - TimeSpan? timeout = null, - bool? useCache = null, - bool? useRandomNameServer = null, - bool? useTcpFallback = null, - bool? useTcpOnly = null) - { - // auto resolved flag might get lost here. But this stuff gets deleted anyways. - return new LookupClientOptions(nameServers) - { - MinimumCacheTimeout = minimumCacheTimeout, - MaximumCacheTimeout = maximumCacheTimeout, - ContinueOnDnsError = continueOnDnsError ?? ContinueOnDnsError, - EnableAuditTrail = enableAuditTrail ?? EnableAuditTrail, - Recursion = recursion ?? Recursion, - Retries = retries ?? Retries, - ThrowDnsErrors = throwDnsErrors ?? ThrowDnsErrors, - Timeout = timeout ?? Timeout, - UseCache = useCache ?? UseCache, - UseRandomNameServer = useRandomNameServer ?? UseRandomNameServer, - UseTcpFallback = useTcpFallback ?? UseTcpFallback, - UseTcpOnly = useTcpOnly ?? UseTcpOnly - }; - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithContinueOnDnsError(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, continueOnDnsError: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithEnableAuditTrail(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, enableAuditTrail: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithMinimumCacheTimeout(TimeSpan? value) - { - return Copy(NameServers, minimumCacheTimeout: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithRecursion(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, recursion: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithRetries(int value) - { - return Copy(NameServers, MinimumCacheTimeout, retries: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithThrowDnsErrors(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, throwDnsErrors: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithTimeout(TimeSpan value) - { - return Copy(NameServers, MinimumCacheTimeout, timeout: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithUseCache(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, useCache: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithUseTcpFallback(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, useTcpFallback: value); - } - - // TODO: remove if LookupClient settings can be made readonly - internal LookupClientSettings WithUseTcpOnly(bool value) - { - return Copy(NameServers, MinimumCacheTimeout, useTcpOnly: value); - } - } - -#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode() } \ No newline at end of file diff --git a/src/DnsClient/Internal/StringBuilderObjectPool.cs b/src/DnsClient/Internal/StringBuilderObjectPool.cs index 545142fd..7f947b61 100644 --- a/src/DnsClient/Internal/StringBuilderObjectPool.cs +++ b/src/DnsClient/Internal/StringBuilderObjectPool.cs @@ -1,6 +1,8 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -8,69 +10,221 @@ namespace DnsClient.Internal { public class StringBuilderObjectPool { - private readonly StringBuilder[] _items; - private readonly int _initialCapacity; - private readonly int _maxPooledCapacity; - private StringBuilder _fastAccess; + private readonly ObjectPool _pool; public static StringBuilderObjectPool Default { get; } = new StringBuilderObjectPool(); - public StringBuilderObjectPool(int pooledItems = 16, int initialCapacity = 200, int maxPooledCapacity = 1024 * 2) + public StringBuilderObjectPool(int initialCapacity = 200, int maxPooledCapacity = 1024 * 2) { - _items = new StringBuilder[pooledItems]; - _initialCapacity = initialCapacity; - _maxPooledCapacity = maxPooledCapacity; - _fastAccess = Create(); + _pool = new DefaultObjectPoolProvider().CreateStringBuilderPool(initialCapacity, maxPooledCapacity); } public StringBuilder Get() { - //return Create(); - var found = _fastAccess; + return _pool.Get(); + } + + public void Return(StringBuilder value) + { + _pool.Return(value); + } + } + + /* copy of MS extesions object pool implementation minus disposable object pool + * because it does not support the targets this library supports and fixes some issues I have with my StringBuilderObjectPool + * Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + */ + + internal interface IPooledObjectPolicy + { + T Create(); + + bool Return(T obj); + } - if (found == null || Interlocked.CompareExchange(ref _fastAccess, null, found) != found) + internal class DefaultObjectPool : ObjectPool where T : class + { + private protected readonly ObjectWrapper[] _items; + private protected readonly IPooledObjectPolicy _policy; + private protected readonly bool _isDefaultPolicy; + private protected T _firstItem; + + private protected readonly PooledObjectPolicy _fastPolicy; + + public DefaultObjectPool(IPooledObjectPolicy policy) + : this(policy, Environment.ProcessorCount * 2) + { + } + + public DefaultObjectPool(IPooledObjectPolicy policy, int maximumRetained) + { + _policy = policy ?? throw new ArgumentNullException(nameof(policy)); + _fastPolicy = policy as PooledObjectPolicy; + _isDefaultPolicy = IsDefaultPolicy(); + + // -1 due to _firstItem + _items = new ObjectWrapper[maximumRetained - 1]; + + bool IsDefaultPolicy() { - for (var i = 0; i < _items.Length; i++) - { - found = _items[i]; + var type = policy.GetType(); + +#if NETSTANDARD1_3 + return type.GenericTypeArguments?.Length > 0 && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>); +#else + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>); +#endif + } + } - if (found != null && Interlocked.CompareExchange(ref _items[i], null, found) == found) + public override T Get() + { + var item = _firstItem; + if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item) + { + var items = _items; + for (var i = 0; i < items.Length; i++) + { + item = items[i].Element; + if (item != null && Interlocked.CompareExchange(ref items[i].Element, null, item) == item) { - return found; + return item; } } + + item = Create(); } - return found ?? Create(); + return item; } - public void Return(StringBuilder value) + [MethodImpl(MethodImplOptions.NoInlining)] + private T Create() => _fastPolicy?.Create() ?? _policy.Create(); + + public override void Return(T obj) { - //return; - if (value == null || value.Capacity > _maxPooledCapacity) + if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj))) { - return; + if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null) + { + var items = _items; + for (var i = 0; i < items.Length && Interlocked.CompareExchange(ref items[i].Element, obj, null) != null; ++i) + { + } + } } + } - value.Clear(); + [DebuggerDisplay("{Element}")] + private protected struct ObjectWrapper + { + public T Element; + } + } - if (_fastAccess == null && Interlocked.CompareExchange(ref _fastAccess, value, null) == null) + internal class DefaultObjectPoolProvider : ObjectPoolProvider + { + public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2; + + public override ObjectPool Create(IPooledObjectPolicy policy) + { + if (policy == null) { - return; + throw new ArgumentNullException(nameof(policy)); } - for (var i = 0; i < _items.Length; i++) + return new DefaultObjectPool(policy, MaximumRetained); + } + } + + internal class DefaultPooledObjectPolicy : PooledObjectPolicy where T : class, new() + { + public override T Create() + { + return new T(); + } + + public override bool Return(T obj) + { + return true; + } + } + + internal abstract class ObjectPool where T : class + { + public abstract T Get(); + + public abstract void Return(T obj); + } + + internal static class ObjectPool + { + public static ObjectPool Create(IPooledObjectPolicy policy = null) where T : class, new() + { + var provider = new DefaultObjectPoolProvider(); + return provider.Create(policy ?? new DefaultPooledObjectPolicy()); + } + } + + internal abstract class ObjectPoolProvider + { + public ObjectPool Create() where T : class, new() + { + return Create(new DefaultPooledObjectPolicy()); + } + + public abstract ObjectPool Create(IPooledObjectPolicy policy) where T : class; + } + + internal abstract class PooledObjectPolicy : IPooledObjectPolicy + { + public abstract T Create(); + + public abstract bool Return(T obj); + } + + internal class StringBuilderPooledObjectPolicy : PooledObjectPolicy + { + public int InitialCapacity { get; set; } = 100; + + public int MaximumRetainedCapacity { get; set; } = 4 * 1024; + + public override StringBuilder Create() + { + return new StringBuilder(InitialCapacity); + } + + public override bool Return(StringBuilder obj) + { + if (obj.Capacity > MaximumRetainedCapacity) { - if (Interlocked.CompareExchange(ref _items[i], value, null) == null) - { - return; - } + return false; } + + obj.Clear(); + return true; } + } - private StringBuilder Create() + internal static class ObjectPoolProviderExtensions + { + public static ObjectPool CreateStringBuilderPool(this ObjectPoolProvider provider) { - return new StringBuilder(_initialCapacity); + return provider.Create(new StringBuilderPooledObjectPolicy()); + } + + public static ObjectPool CreateStringBuilderPool( + this ObjectPoolProvider provider, + int initialCapacity, + int maximumRetainedCapacity) + { + var policy = new StringBuilderPooledObjectPolicy() + { + InitialCapacity = initialCapacity, + MaximumRetainedCapacity = maximumRetainedCapacity, + }; + + return provider.Create(policy); } } } diff --git a/src/DnsClient/LookupClient.cs b/src/DnsClient/LookupClient.cs index 42899c51..43854ac4 100644 --- a/src/DnsClient/LookupClient.cs +++ b/src/DnsClient/LookupClient.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using DnsClient.Internal; +using DnsClient.Protocol; using DnsClient.Protocol.Options; namespace DnsClient @@ -40,16 +40,24 @@ public class LookupClient : ILookupClient, IDnsQuery { private const int c_eventStartQuery = 1; private const int c_eventQuery = 2; - private const int c_eventQueryTruncated = 3; + private const int c_eventQueryCachedResult = 3; + private const int c_eventQueryTruncated = 5; private const int c_eventQuerySuccess = 10; - private const int c_eventQueryCachedResult = 11; - private const int c_eventQueryResponseError = 40; - private const int c_eventQueryRetryError = 50; - private const int c_eventQueryFail = 60; + private const int c_eventQueryReturnResponseError = 11; + private const int c_eventQuerySuccessEmpty = 12; + + private const int c_eventQueryRetryErrorNextServer = 20; + private const int c_eventQueryRetryErrorSameServer = 21; + + private const int c_eventQueryFail = 90; + private const int c_eventQueryBadTruncation = 91; + + private const int c_eventResponseOpt = 31; + private const int c_eventResponseMissingOpt = 80; + private readonly DnsMessageHandler _messageHandler; private readonly DnsMessageHandler _tcpFallbackHandler; private readonly ILogger _logger; - private readonly Random _random = new Random(); // for backward compat /// @@ -318,16 +326,19 @@ public LookupClient(LookupClientOptions options) 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(); - _logger = Logging.LoggerFactory.CreateLogger(nameof(LookupClient)); + + if (_messageHandler.Type != DnsMessageHandleType.UDP) + { + throw new ArgumentException("UDP message handler's type must be UDP.", nameof(udpHandler)); + } + if (_tcpFallbackHandler.Type != DnsMessageHandleType.TCP) + { + throw new ArgumentException("TCP message handler's type must be TCP.", nameof(tcpHandler)); + } + + _logger = Logging.LoggerFactory.CreateLogger(GetType().FullName); Cache = new ResponseCache(true, Settings.MinimumCacheTimeout, Settings.MaximumCacheTimeout); } @@ -637,7 +648,8 @@ public Task QueryServerReverseAsync(IReadOnlyCollection queryOptions == null ? Settings : (DnsQuerySettings)queryOptions; - private IDnsQueryResponse QueryInternal(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection useServers) + private IDnsQueryResponse QueryInternal(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection servers) { + if (servers == null) + { + throw new ArgumentNullException(nameof(servers)); + } if (question == null) { throw new ArgumentNullException(nameof(question)); @@ -667,21 +683,54 @@ private IDnsQueryResponse QueryInternal(DnsQuestion question, DnsQuerySettings s { throw new ArgumentNullException(nameof(settings)); } + if (servers.Count == 0) + { + throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); + } - var head = new DnsRequestHeader(GetNextUniqueId(), settings.Recursion, DnsOpCode.Query); - var request = new DnsRequestMessage(head, question); + var head = new DnsRequestHeader(settings.Recursion, DnsOpCode.Query); + var request = new DnsRequestMessage(head, question, settings); var handler = settings.UseTcpOnly ? _tcpFallbackHandler : _messageHandler; + var audit = settings.EnableAuditTrail ? new LookupClientAudit(settings) : null; if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(c_eventStartQuery, "Begin query {0} => {1} on [{2}]", head, question, string.Join(", ", useServers)); + _logger.LogDebug(c_eventStartQuery, "Begin query [{0}] via {1} => {2} on [{3}].", head, handler.Type, question, string.Join(", ", servers)); + } + + var result = ResolveQuery(servers.ToList(), settings, handler, request, audit); + if (!(result is TruncatedQueryResponse)) + { + return result; + } + + if (!settings.UseTcpFallback) + { + throw new DnsResponseException(DnsResponseCode.Unassigned, "Response was truncated and UseTcpFallback is disabled, unable to resolve the question.") + { + AuditTrail = audit?.Build(result) + }; + } + + request.Header.RefreshId(); + var tcpResult = ResolveQuery(servers.ToList(), settings, _tcpFallbackHandler, request, audit); + if (tcpResult is TruncatedQueryResponse) + { + throw new DnsResponseException("Unexpected truncated result from TCP response.") + { + AuditTrail = audit?.Build(tcpResult) + }; } - return ResolveQuery(useServers, settings, handler, request); + return tcpResult; } - private Task QueryInternalAsync(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection useServers, CancellationToken cancellationToken = default) + private async Task QueryInternalAsync(DnsQuestion question, DnsQuerySettings settings, IReadOnlyCollection servers, CancellationToken cancellationToken = default) { + if (servers == null) + { + throw new ArgumentNullException(nameof(servers)); + } if (question == null) { throw new ArgumentNullException(nameof(question)); @@ -690,21 +739,54 @@ private Task QueryInternalAsync(DnsQuestion question, DnsQuer { throw new ArgumentNullException(nameof(settings)); } + if (servers.Count == 0) + { + throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); + } - var head = new DnsRequestHeader(GetNextUniqueId(), settings.Recursion, DnsOpCode.Query); - var request = new DnsRequestMessage(head, question); + var head = new DnsRequestHeader(settings.Recursion, DnsOpCode.Query); + var request = new DnsRequestMessage(head, question, settings); var handler = settings.UseTcpOnly ? _tcpFallbackHandler : _messageHandler; + var audit = settings.EnableAuditTrail ? new LookupClientAudit(settings) : null; if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(c_eventStartQuery, "Begin query {0} => {1} on [{2}].", head.Id, question, string.Join(", ", useServers)); + _logger.LogDebug(c_eventStartQuery, "Begin query [{0}] via {1} => {2} on [{3}].", head, handler.Type, question, string.Join(", ", servers)); + } + + var result = await ResolveQueryAsync(servers.ToList(), settings, handler, request, audit, cancellationToken: cancellationToken).ConfigureAwait(false); + if (!(result is TruncatedQueryResponse)) + { + return result; + } + + if (!settings.UseTcpFallback) + { + throw new DnsResponseException(DnsResponseCode.Unassigned, "Response was truncated and UseTcpFallback is disabled, unable to resolve the question.") + { + AuditTrail = audit?.Build(result) + }; + } + + request.Header.RefreshId(); + var tcpResult = await ResolveQueryAsync(servers.ToList(), settings, _tcpFallbackHandler, request, audit, cancellationToken: cancellationToken).ConfigureAwait(false); + if (tcpResult is TruncatedQueryResponse) + { + throw new DnsResponseException("Unexpected truncated result from TCP response.") + { + AuditTrail = audit?.Build(tcpResult) + }; } - return ResolveQueryAsync(useServers, settings, handler, request, cancellationToken: cancellationToken); + return tcpResult; } - // making it internal for unit testing - internal IDnsQueryResponse ResolveQuery(IReadOnlyCollection servers, DnsQuerySettings settings, DnsMessageHandler handler, DnsRequestMessage request, LookupClientAudit continueAudit = null) + private IDnsQueryResponse ResolveQuery( + IReadOnlyList servers, + DnsQuerySettings settings, + DnsMessageHandler handler, + DnsRequestMessage request, + LookupClientAudit audit = null) { if (request == null) { @@ -714,56 +796,53 @@ internal IDnsQueryResponse ResolveQuery(IReadOnlyCollection servers, { throw new ArgumentNullException(nameof(handler)); } - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - if (servers == null || servers.Count == 0) - { - throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); - } - LookupClientAudit audit = null; - if (settings.EnableAuditTrail) + for (var serverIndex = 0; serverIndex < servers.Count; serverIndex++) { - audit = continueAudit ?? new LookupClientAudit(settings); - } + var serverInfo = servers[serverIndex]; + var isLastServer = serverIndex >= servers.Count - 1; - DnsResponseException lastDnsResponseException = null; - Exception lastException = null; - DnsQueryResponse lastQueryResponse = null; + if (serverIndex > 0) + { + request.Header.RefreshId(); + } + + if (settings.EnableAuditTrail && !isLastServer) + { + audit?.AuditRetryNextServer(serverInfo); + } - foreach (var serverInfo in servers) - { var cacheKey = string.Empty; if (settings.UseCache) { cacheKey = ResponseCache.GetCacheKey(request.Question); - var item = Cache.Get(cacheKey); - if (item != null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug(c_eventQueryCachedResult, "Got cached result for query {0} => {1}.", request.Header.Id, request.Question); - } - return item; + if (TryGetCachedResult(cacheKey, request, settings, out var cachedResponse)) + { + return cachedResponse; } } var tries = 0; do { + if (tries > 0) + { + request.Header.RefreshId(); + } + tries++; - lastDnsResponseException = null; - lastException = null; + var isLastTry = tries > settings.Retries; + + IDnsQueryResponse lastQueryResponse = null; if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( c_eventQuery, - "TryResolve {0} => {1} on {2}, try {3}/{4}.", + "TryResolve {0} via {1} => {2} on {3}, try {4}/{5}.", request.Header.Id, + handler.Type, request.Question, serverInfo, tries, @@ -776,271 +855,219 @@ internal IDnsQueryResponse ResolveQuery(IReadOnlyCollection servers, DnsResponseMessage response = handler.Query(serverInfo.IPEndPoint, request, settings.Timeout); - response.Audit = audit; + lastQueryResponse = ProcessResponseMessage( + audit, + request, + response, + settings, + serverInfo, + handler.Type, + servers.Count, + isLastServer, + out var retryQueryHint); - if (response.Header.ResultTruncated && settings.UseTcpFallback && !handler.GetType().Equals(typeof(DnsTcpMessageHandler))) + if (lastQueryResponse is TruncatedQueryResponse) { - audit?.AuditTruncatedRetryTcp(); - - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation(c_eventQueryTruncated, "Query {0} using UDP was truncated, re-trying with TCP.", request.Header.Id); - } - - return ResolveQuery(new[] { serverInfo }, settings, _tcpFallbackHandler, request, audit); + // Return right away and try to fallback to TCP. + return lastQueryResponse; } - audit?.AuditResolveServers(servers.Count); - audit?.AuditResponseHeader(response.Header); + audit?.AuditEnd(lastQueryResponse, serverInfo); + audit?.Build(lastQueryResponse); - if (response.Header.ResponseCode != DnsResponseCode.NoError) + if (lastQueryResponse.HasError) { - if (settings.EnableAuditTrail) - { - audit.AuditResponseError(response.Header.ResponseCode); - } - - if (_logger.IsEnabled(LogLevel.Information)) + throw new DnsResponseException((DnsResponseCode)response.Header.ResponseCode) { - _logger.LogInformation( - c_eventQueryResponseError, - "Query {0} => {1} got a response error {2} from the server {3}.", - request.Header.Id, - request.Question, - response.Header.ResponseCode, - serverInfo); - } + AuditTrail = audit?.Build() + }; } - else - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug( - c_eventQuerySuccess, - "Query {0} => {1} on {2} received result with {3} answers.", - request.Header.Id, - request.Question, - serverInfo, - response.Answers.Count); - } - } - - HandleOptRecords(settings, audit, serverInfo, response); - - audit?.AuditResponse(); - audit?.AuditEnd(response, serverInfo); - - DnsQueryResponse queryResponse = response.AsQueryResponse(serverInfo, settings); - lastQueryResponse = queryResponse; - if (response.Header.ResponseCode != DnsResponseCode.NoError && - (settings.ThrowDnsErrors || settings.ContinueOnDnsError)) + if (retryQueryHint == HandleError.RetryNextServer) { - throw new DnsResponseException(response.Header.ResponseCode); + break; } if (settings.UseCache) { - Cache.Add(cacheKey, queryResponse); + Cache.Add(cacheKey, lastQueryResponse); } - return queryResponse; + return lastQueryResponse; } - catch (DnsResponseException ex) + catch (DnsResponseParseException ex) { - ex.AuditTrail = audit?.Build(null); - lastDnsResponseException = ex; - - if (settings.ContinueOnDnsError) + var handle = HandleDnsResponeParseException(ex, request, handler.Type, isLastServer: isLastServer); + if (handle == HandleError.RetryNextServer) { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - c_eventQueryRetryError, - "Query {0} => {1} on {2} returned a response error. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); - } - break; } - - if (_logger.IsEnabled(LogLevel.Information)) + else if (handle == HandleError.ReturnResponse) { - _logger.LogInformation( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} returned a response error, throwing the error because ContinueOnDnsError=True.", - request.Header.Id, - request.Question, - serverInfo); + return new TruncatedQueryResponse(); } throw; } - catch (DnsResponseParseException ex) + catch (DnsResponseException ex) { - // Response parsing can be retried on the same server... - lastException = ex; - this._logger.LogWarning( - c_eventQueryRetryError, - ex, - "Error parsing the response. Re-trying {0}/{1}...", - tries, - settings.Retries + 1); + // Response error handling and logging + var handle = HandleDnsResponseException(ex, request, settings, serverInfo, handler.Type, isLastServer: isLastServer, isLastTry: isLastTry, tries); - continue; - } - catch (SocketException ex) when ( - ex.SocketErrorCode == SocketError.AddressFamilyNotSupported - || ex.SocketErrorCode == SocketError.ConnectionRefused - || ex.SocketErrorCode == SocketError.ConnectionReset) - { - if (_logger.IsEnabled(LogLevel.Warning)) + if (handle == HandleError.Throw) { - _logger.LogWarning( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} failed to connect. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); + throw; + } + else if (handle == HandleError.RetryCurrentServer) + { + continue; + } + else if (handle == HandleError.RetryNextServer) + { + break; } - break; + // This should not happen, but might if something upstream throws this exception before + // we got to parse the response, which, again, should not happen. + if (lastQueryResponse == null) + { + throw; + } + + // If its the last server, return. + return lastQueryResponse; } catch (Exception ex) when ( - ex is TimeoutException + ex is TimeoutException timeoutEx || handler.IsTransientException(ex) || ex is OperationCanceledException) { - if (_logger.IsEnabled(LogLevel.Information)) + var handle = HandleTimeoutException(ex, request, settings, serverInfo, handler.Type, isLastServer: isLastServer, isLastTry: isLastTry, currentTry: tries); + + if (handle == HandleError.RetryCurrentServer) { - _logger.LogInformation( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} timed out or is a transient error. Re-trying {3}/{4}...", - request.Header.Id, - request.Question, - serverInfo, - tries, - settings.Retries + 1); + continue; + } + else if (handle == HandleError.RetryNextServer) + { + break; } - continue; + throw new DnsResponseException( + DnsResponseCode.ConnectionTimeout, + $"Query {request.Header.Id} => {request.Question} on {serverInfo} timed out or is a transient error.", + ex) + { + AuditTrail = audit?.Build() + }; + } + catch (ArgumentException) + { + // Don't retry argument exceptions. This should not happen anyways unless we messed up somewhere. + throw; } + catch (InvalidOperationException) + { + // Don't retry invalid ops exceptions. This should not happen anyways unless we messed up somewhere. + throw; + } + // Any other exception will not be retried on the same server. e.g. Socket Exceptions catch (Exception ex) { audit?.AuditException(ex); - lastException = ex; + var handle = HandleUnhandledException(ex, request, serverInfo, handler.Type, isLastServer); - if (_logger.IsEnabled(LogLevel.Warning)) + if (handle == HandleError.RetryNextServer) + { + break; + } + if (handle == HandleError.RetryCurrentServer) { - _logger.LogWarning( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} failed with an unhandled error. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); + continue; } - break; + throw new DnsResponseException( + DnsResponseCode.Unassigned, + $"Query {request.Header.Id} => {request.Question} on {serverInfo} failed with an error.", + ex) + { + AuditTrail = audit?.Build() + }; } } while (tries <= settings.Retries); + } // next server - 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) - }; - } - + // 1.3.0: With the error handling, this should never be reached. throw new DnsResponseException(DnsResponseCode.ConnectionTimeout, $"No connection could be established to any of the following name servers: {string.Join(", ", servers)}.") { - AuditTrail = audit?.Build(null) + AuditTrail = audit?.Build() }; } - internal async Task ResolveQueryAsync(IReadOnlyCollection servers, DnsQuerySettings settings, DnsMessageHandler handler, DnsRequestMessage request, LookupClientAudit continueAudit = null, CancellationToken cancellationToken = default) + private async Task ResolveQueryAsync( + IReadOnlyList servers, + DnsQuerySettings settings, + DnsMessageHandler handler, + DnsRequestMessage request, + LookupClientAudit audit = null, + CancellationToken cancellationToken = default) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - if (settings == null) - { - throw new ArgumentNullException(nameof(settings)); - } - if (servers == null || servers.Count == 0) + if (request == null) { - throw new ArgumentOutOfRangeException(nameof(servers), "List of configured name servers must not be empty."); + throw new ArgumentNullException(nameof(request)); } - LookupClientAudit audit = null; - if (settings.EnableAuditTrail) + for (var serverIndex = 0; serverIndex < servers.Count; serverIndex++) { - audit = continueAudit ?? new LookupClientAudit(settings); - } + var serverInfo = servers[serverIndex]; + var isLastServer = serverIndex >= servers.Count - 1; - DnsResponseException lastDnsResponseException = null; - Exception lastException = null; - DnsQueryResponse lastQueryResponse = null; + if (serverIndex > 0) + { + request.Header.RefreshId(); + } + + if (settings.EnableAuditTrail && serverIndex > 0 && !isLastServer) + { + audit?.AuditRetryNextServer(serverInfo); + } - foreach (var serverInfo in servers) - { var cacheKey = string.Empty; if (settings.UseCache) { cacheKey = ResponseCache.GetCacheKey(request.Question); - var item = Cache.Get(cacheKey); - if (item != null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug(c_eventQueryCachedResult, "Got cached result for query {0} => {1}.", request.Header.Id, request.Question); - } - return item; + if (TryGetCachedResult(cacheKey, request, settings, out var cachedResponse)) + { + return cachedResponse; } } var tries = 0; do { + if (tries > 0) + { + request.Header.RefreshId(); + } + tries++; - lastDnsResponseException = null; - lastException = null; + var isLastTry = tries > settings.Retries; + + IDnsQueryResponse lastQueryResponse = null; if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug( c_eventQuery, - "TryResolve {0} => {1} on {2}, try {3}/{4}.", + "TryResolve {0} via {1} => {2} on {3}, try {4}/{5}.", request.Header.Id, + handler.Type, request.Question, serverInfo, tries, @@ -1050,7 +1077,6 @@ internal async Task ResolveQueryAsync(IReadOnlyCollection ResolveQueryAsync(IReadOnlyCollection ResolveQueryAsync(IReadOnlyCollection {1} got a response error {2} from the server {3}.", - request.Header.Id, - request.Question, - response.Header.ResponseCode, - serverInfo); - } - } - else + if (lastQueryResponse.HasError) { - if (_logger.IsEnabled(LogLevel.Debug)) + throw new DnsResponseException((DnsResponseCode)response.Header.ResponseCode) { - _logger.LogDebug( - c_eventQuerySuccess, - "Query {0} => {1} on {2} received result with {3} answers.", - request.Header.Id, - request.Question, - serverInfo, - response.Answers.Count); - } + AuditTrail = audit?.Build() + }; } - HandleOptRecords(settings, audit, serverInfo, response); - - audit?.AuditResponse(); - audit?.AuditEnd(response, serverInfo); - - DnsQueryResponse queryResponse = response.AsQueryResponse(serverInfo, settings); - lastQueryResponse = queryResponse; - - if (response.Header.ResponseCode != DnsResponseCode.NoError && - (settings.ThrowDnsErrors || settings.ContinueOnDnsError)) + if (retryQueryHint == HandleError.RetryNextServer) { - throw new DnsResponseException(response.Header.ResponseCode); + break; } if (settings.UseCache) { - Cache.Add(cacheKey, queryResponse); + Cache.Add(cacheKey, lastQueryResponse); } - return queryResponse; + return lastQueryResponse; } - catch (DnsResponseException ex) + catch (DnsResponseParseException ex) { - ex.AuditTrail = audit?.Build(null); - lastDnsResponseException = ex; - - if (settings.ContinueOnDnsError) + var handle = HandleDnsResponeParseException(ex, request, handler.Type, isLastServer: isLastServer); + if (handle == HandleError.RetryNextServer) { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - c_eventQueryRetryError, - "Query {0} => {1} on {2} returned a response error. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); - } - break; } - - if (_logger.IsEnabled(LogLevel.Information)) + else if (handle == HandleError.ReturnResponse) { - _logger.LogInformation( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} returned a response error, throwing the error because ContinueOnDnsError=True.", - request.Header.Id, - request.Question, - serverInfo); + return new TruncatedQueryResponse(); } throw; } - catch (DnsResponseParseException ex) + catch (DnsResponseException ex) { - // Response parsing can be retried on the same server... - lastException = ex; - this._logger.LogWarning( - c_eventQueryRetryError, - ex, - "Error parsing the response. Re-trying {0}/{1}...", - tries, - settings.Retries + 1); + // Response error handling and logging + var handle = HandleDnsResponseException(ex, request, settings, serverInfo, handler.Type, isLastServer: isLastServer, isLastTry: isLastTry, tries); - continue; - } - catch (SocketException ex) when ( - ex.SocketErrorCode == SocketError.AddressFamilyNotSupported - || ex.SocketErrorCode == SocketError.ConnectionRefused - || ex.SocketErrorCode == SocketError.ConnectionReset) - { - if (_logger.IsEnabled(LogLevel.Warning)) + if (handle == HandleError.Throw) + { + throw; + } + else if (handle == HandleError.RetryCurrentServer) { - _logger.LogWarning( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} failed to connect. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); + continue; + } + else if (handle == HandleError.RetryNextServer) + { + break; } - break; + // This should not happen, but might if something upstream throws this exception before + // we got to parse the response, which, again, should not happen. + if (lastQueryResponse == null) + { + throw; + } + + // If its the last server, return. + return lastQueryResponse; } 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) + if (!cancellationToken.IsCancellationRequested) { - throw new OperationCanceledException(cancellationToken); - } + var handle = HandleTimeoutException(ex, request, settings, serverInfo, handler.Type, isLastServer: isLastServer, isLastTry: isLastTry, currentTry: tries); - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} timed out or is a transient error. Re-trying {3}/{4}...", - request.Header.Id, - request.Question, - serverInfo, - tries, - settings.Retries + 1); + if (handle == HandleError.RetryCurrentServer) + { + continue; + } + else if (handle == HandleError.RetryNextServer) + { + break; + } } - continue; + throw new DnsResponseException( + DnsResponseCode.ConnectionTimeout, + $"Query {request.Header.Id} => {request.Question} on {serverInfo} timed out or is a transient error.", + ex) + { + AuditTrail = audit?.Build() + }; + } + catch (ArgumentException) + { + // Don't retry argument exceptions. This should not happen anyways unless we messed up somewhere. + throw; + } + catch (InvalidOperationException) + { + // Don't retry invalid ops exceptions. This should not happen anyways unless we messed up somewhere. + throw; } + // Any other exception will not be retried on the same server. e.g. Socket Exceptions catch (Exception ex) { - 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; - if (_logger.IsEnabled(LogLevel.Warning)) + var handle = HandleUnhandledException(ex, request, serverInfo, handler.Type, isLastServer); + + if (handle == HandleError.RetryNextServer) { - _logger.LogWarning( - c_eventQueryFail, - ex, - "Query {0} => {1} on {2} failed with an unhandled error. Trying next server (if any)...", - request.Header.Id, - request.Question, - serverInfo); + break; + } + if (handle == HandleError.RetryCurrentServer) + { + continue; } - break; + throw new DnsResponseException( + DnsResponseCode.Unassigned, + $"Query {request.Header.Id} => {request.Question} on {serverInfo} failed with an error.", + ex) + { + AuditTrail = audit?.Build() + }; } - } while (tries <= settings.Retries && !cancellationToken.IsCancellationRequested); + } while (tries <= settings.Retries); + } // next server + + // 1.3.0: With the error handling, this should never be reached. + throw new DnsResponseException(DnsResponseCode.ConnectionTimeout, $"No connection could be established to any of the following name servers: {string.Join(", ", servers)}.") + { + AuditTrail = audit?.Build() + }; + } + + private enum HandleError + { + None, + Throw, + RetryCurrentServer, + RetryNextServer, + ReturnResponse + } + + private HandleError HandleDnsResponseException(DnsResponseException ex, DnsRequestMessage request, DnsQuerySettings settings, NameServer nameServer, DnsMessageHandleType handleType, bool isLastServer, bool isLastTry, int currentTry) + { + var handle = isLastServer ? settings.ThrowDnsErrors ? HandleError.Throw : HandleError.ReturnResponse : HandleError.RetryNextServer; + + if (!settings.ContinueOnDnsError) + { + handle = settings.ThrowDnsErrors ? HandleError.Throw : HandleError.ReturnResponse; + } + else if (!isLastTry && + (ex.Code == DnsResponseCode.ServerFailure || ex.Code == DnsResponseCode.FormatError)) + { + handle = HandleError.RetryCurrentServer; + } + + if (_logger.IsEnabled(LogLevel.Information)) + { + var eventId = 0; + var message = "Query {0} via {1} => {2} on {3} returned a response error '{4}'."; - if (settings.EnableAuditTrail && servers.Count > 1 && serverInfo != servers.Last()) + switch (handle) { - audit?.AuditRetryNextServer(serverInfo); + case HandleError.Throw: + eventId = c_eventQueryFail; + message += " Throwing the error."; + break; + + case HandleError.ReturnResponse: + eventId = c_eventQueryReturnResponseError; + message += " Returning response."; + break; + + case HandleError.RetryCurrentServer: + eventId = c_eventQueryRetryErrorSameServer; + message += " Re-trying {5}/{6}...."; + break; + + case HandleError.RetryNextServer: + eventId = c_eventQueryRetryErrorNextServer; + message += " Trying next server."; + break; + } + + if (handle == HandleError.RetryCurrentServer) + { + _logger.LogInformation(eventId, message, request.Header.Id, handleType, request.Question, nameServer, ex.DnsError, currentTry + 1, settings.Retries + 1); + } + else + { + _logger.LogInformation(eventId, message, request.Header.Id, handleType, request.Question, nameServer, ex.DnsError); } } - if (lastDnsResponseException != null && settings.ThrowDnsErrors) + return handle; + } + + private HandleError HandleDnsResponeParseException(DnsResponseParseException ex, DnsRequestMessage request, DnsMessageHandleType handleType, bool isLastServer) + { + // Don't try to fallback to TCP if we already are on TCP + if (handleType == DnsMessageHandleType.UDP + // Assuming that if we only got 512 or less bytes, its probably some network issue. + && (ex.ResponseData.Length <= DnsQueryOptions.MinimumBufferSize + // Second assumption: If the parser tried to read outside the provided data, this might also be a network issue. + || ex.ReadLength + ex.Index > ex.ResponseData.Length)) + { + // lets assume the response was truncated and retry with TCP. + // (Not retrying other servers as it is very unlikely they would provide better results on this network) + this._logger.LogError( + c_eventQueryBadTruncation, + ex, + "Query {0} via {1} => {2} error parsing the response. The response seems to be truncated without TC flag set! Re-trying via TCP anyways.", + request.Header.Id, + handleType, + request.Question); + + // In this case the caller should return TruncatedResponseMessage + return HandleError.ReturnResponse; + } + + if (isLastServer) { - throw lastDnsResponseException; + this._logger.LogError( + c_eventQueryFail, + ex, + "Query {0} via {1} => {2} error parsing the response. Throwing the error.", + request.Header.Id, + handleType, + request.Question); + + return HandleError.Throw; } - if (lastQueryResponse != null) + // Otherwise, lets continue at least with the next server + this._logger.LogWarning( + c_eventQueryRetryErrorNextServer, + ex, + "Query {0} via {1} => {2} error parsing the response. Trying next server.", + request.Header.Id, + handleType, + request.Question); + + return HandleError.RetryNextServer; + } + + private HandleError HandleTimeoutException(Exception ex, DnsRequestMessage request, DnsQuerySettings settings, NameServer nameServer, DnsMessageHandleType handleType, bool isLastServer, bool isLastTry, int currentTry) + { + if (isLastTry && isLastServer) { - return lastQueryResponse; + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation( + c_eventQueryFail, + ex, + "Query {0} via {1} => {2} on {3} timed out or is a transient error. Throwing the error.", + request.Header.Id, + handleType, + request.Question, + nameServer); + } + + return HandleError.Throw; + } + else if (isLastTry) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation( + c_eventQueryRetryErrorNextServer, + ex, + "Query {0} via {1} => {2} on {3} timed out or is a transient error. Trying next server", + request.Header.Id, + handleType, + request.Question, + nameServer); + } + + return HandleError.RetryNextServer; + } + + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation( + c_eventQueryRetryErrorSameServer, + ex, + "Query {0} via {1} => {2} on {3} timed out or is a transient error. Re-trying {4}/{5}...", + request.Header.Id, + handleType, + request.Question, + nameServer, + currentTry, + settings.Retries + 1); + } + + return HandleError.RetryCurrentServer; + } + + private HandleError HandleUnhandledException(Exception ex, DnsRequestMessage request, NameServer nameServer, DnsMessageHandleType handleType, bool isLastServer) + { + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning( + isLastServer ? c_eventQueryFail : c_eventQueryRetryErrorNextServer, + ex, + "Query {0} via {1} => {2} on {3} failed with an error." + + (isLastServer ? " Throwing the error." : " Trying next server."), + handleType, + request.Header.Id, + request.Question, + nameServer); } - if (lastException != null) + if (isLastServer) { - throw new DnsResponseException(DnsResponseCode.Unassigned, "Unhandled exception", lastException) + return HandleError.Throw; + } + + return HandleError.RetryNextServer; + } + + private IDnsQueryResponse ProcessResponseMessage( + LookupClientAudit audit, + DnsRequestMessage request, + DnsResponseMessage response, + DnsQuerySettings settings, + NameServer nameServer, + DnsMessageHandleType handleType, + int serverCount, + bool isLastServer, + out HandleError handleError) + { + handleError = HandleError.None; + + if (response.Header.ResultTruncated) + { + audit?.AuditTruncatedRetryTcp(); + + if (_logger.IsEnabled(LogLevel.Information)) { - AuditTrail = audit?.Build(null) - }; + _logger.LogInformation(c_eventQueryTruncated, "Query {0} via {1} => {2} was truncated, re-trying with TCP.", request.Header.Id, handleType, request.Question); + } + + return new TruncatedQueryResponse(); } - throw new DnsResponseException(DnsResponseCode.ConnectionTimeout, $"No connection could be established to any of the following name servers: {string.Join(", ", servers)}.") + if (request.Header.Id != response.Header.Id) { - AuditTrail = audit?.Build(null) - }; + throw new DnsResponseException("Header id mismatch."); + } + + audit?.AuditResolveServers(serverCount); + audit?.AuditResponseHeader(response.Header); + + if (response.Header.ResponseCode != DnsHeaderResponseCode.NoError) + { + audit?.AuditResponseError(response.Header.ResponseCode); + } + else + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + c_eventQuerySuccess, + "Got {0} answers for query {1} via {2} => {3} from {4}.", + response.Answers.Count, + request.Header.Id, + handleType, + request.Question, + nameServer); + } + } + + HandleOptRecords(settings, audit, nameServer, response); + + var result = response.AsQueryResponse(nameServer, settings); + + // Set retry next server hint in case the question hasn't been answered. + // Only if there are more servers and the response doesn't have an error as that gets retried already per configuration. + // Also, only do this if the setting is enabled. + if (!result.HasError && !isLastServer && settings.ContinueOnEmptyResponse) + { + // Try next server, if the success result has zero answers. + if (result.Answers.Count == 0) + { + handleError = HandleError.RetryNextServer; + } + // Try next server if the question isn't answered (ignoring ANY and AXFR queries) + else if (request.Question.QuestionType != QueryType.ANY + && request.Question.QuestionType != QueryType.AXFR + && !result.Answers.OfRecordType((ResourceRecordType)request.Question.QuestionType).Any()) + { + handleError = HandleError.RetryNextServer; + } + + if (handleError == HandleError.RetryNextServer + && _logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation( + c_eventQuerySuccessEmpty, + "Got no answer for query {0} via {1} => {2} from {3}. Trying next server.", + request.Header.Id, + handleType, + request.Question, + nameServer); + } + } + + return result; } - private static DnsQuestion GetReverseQuestion(IPAddress ipAddress) + private bool TryGetCachedResult(string cacheKey, DnsRequestMessage request, DnsQuerySettings settings, out IDnsQueryResponse response) { - if (ipAddress == null) + response = null; + if (settings.UseCache) { - throw new ArgumentNullException(nameof(ipAddress)); + response = Cache.Get(cacheKey); + if (response != null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(c_eventQueryCachedResult, "Got cached result for query {0} => {1}.", request.Header.Id, request.Question); + } + + if (settings.EnableAuditTrail) + { + var cacheAudit = new LookupClientAudit(settings); + cacheAudit.AuditCachedItem(response); + cacheAudit.Build(response); + } + + return true; + } } - var arpa = ipAddress.GetArpaName(); - return new DnsQuestion(arpa, QueryType.PTR, QueryClass.IN); + return false; } private void HandleOptRecords(DnsQuerySettings settings, LookupClientAudit audit, NameServer serverInfo, DnsResponseMessage response) { - // The returned options are never used for anything and purely cosmetic at this point - // => let's iterate the results only if needed (if auditing is enabled). - if (settings.EnableAuditTrail) + if (settings.UseExtendedDns) { var record = response.Additionals.OfRecordType(Protocol.ResourceRecordType.OPT).FirstOrDefault(); - if (record != null && record is OptRecord optRecord) + if (record == null) + { + _logger.LogWarning(c_eventResponseMissingOpt, "Response {0} => {1} is missing the requested OPT record.", response.Header.Id, response.Questions.FirstOrDefault()); + } + else if (record is OptRecord optRecord) { - audit.AuditOptPseudo(); + audit?.AuditOptPseudo(); serverInfo.SupportedUdpPayloadSize = optRecord.UdpSize; - audit.AuditEdnsOpt(optRecord.UdpSize, optRecord.Version, optRecord.ResponseCodeEx); + audit?.AuditEdnsOpt(optRecord.UdpSize, optRecord.Version, optRecord.IsDnsSecOk, optRecord.ResponseCodeEx); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug( + c_eventResponseOpt, + "Response {0} => {1} opt record sets buffer of {2} to {3}.", + response.Header.Id, + response.Questions.FirstOrDefault(), + serverInfo, + optRecord.UdpSize); + } } } } - private ushort GetNextUniqueId() + private static DnsQuestion GetReverseQuestion(IPAddress ipAddress) { - return (ushort)_random.Next(1, ushort.MaxValue); + if (ipAddress == null) + { + throw new ArgumentNullException(nameof(ipAddress)); + } + + var arpa = ipAddress.GetArpaName(); + return new DnsQuestion(arpa, QueryType.PTR, QueryClass.IN); } } internal class LookupClientAudit { - private const string c_placeHolder = "$$REPLACEME$$"; private static readonly int s_printOffset = -32; private StringBuilder _auditWriter = new StringBuilder(); private Stopwatch _swatch; @@ -1360,6 +1620,28 @@ public LookupClientAudit(DnsQuerySettings settings) Settings = settings ?? throw new ArgumentNullException(nameof(settings)); } + public void AuditCachedItem(IDnsQueryResponse response) + { + if (!Settings.EnableAuditTrail) + { + return; + } + + StartTimer(); + _auditWriter.AppendLine($"; (cached result)"); + AuditResponseHeader(response.Header); + + AuditOptPseudo(); + + var record = response.Additionals.OfRecordType(Protocol.ResourceRecordType.OPT).FirstOrDefault(); + if (record != null && record is OptRecord optRecord) + { + AuditEdnsOpt(optRecord.UdpSize, optRecord.Version, optRecord.IsDnsSecOk, optRecord.ResponseCodeEx); + } + + AuditEnd(response, response.NameServer); + } + public void StartTimer() { if (!Settings.EnableAuditTrail) @@ -1381,63 +1663,20 @@ public void AuditResolveServers(int count) _auditWriter.AppendLine($"; ({count} server found)"); } - public string Build(IDnsQueryResponse queryResponse) + public string Build(IDnsQueryResponse response = null) { if (!Settings.EnableAuditTrail) { return string.Empty; } - var writer = new StringBuilder(); - - if (queryResponse != null) + var audit = _auditWriter.ToString(); + if (response != 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(); - } - - var additionals = queryResponse.Additionals.Where(p => !(p is OptRecord)).ToArray(); - if (additionals.Length > 0) - { - writer.AppendLine(";; ADDITIONALS SECTION:"); - foreach (var additional in additionals) - { - writer.AppendLine(additional.ToString(s_printOffset)); - } - writer.AppendLine(); - } + DnsQueryResponse.SetAuditTrail(response, audit); } - var all = _auditWriter.ToString(); - var dynamic = writer.ToString(); - - return all.Replace(c_placeHolder, dynamic); + return audit; } public void AuditTruncatedRetryTcp() @@ -1451,14 +1690,14 @@ public void AuditTruncatedRetryTcp() _auditWriter.AppendLine(); } - public void AuditResponseError(DnsResponseCode responseCode) + public void AuditResponseError(DnsHeaderResponseCode responseCode) { if (!Settings.EnableAuditTrail) { return; } - _auditWriter.AppendLine($";; ERROR: {DnsResponseCodeText.GetErrorText(responseCode)}"); + _auditWriter.AppendLine($";; ERROR: {DnsResponseCodeText.GetErrorText((DnsResponseCode)responseCode)}"); } public void AuditOptPseudo() @@ -1487,28 +1726,17 @@ public void AuditResponseHeader(DnsResponseHeader header) _auditWriter.AppendLine(); } - public void AuditEdnsOpt(short udpSize, byte version, DnsResponseCode responseCodeEx) + public void AuditEdnsOpt(short udpSize, byte version, bool doFlag, DnsResponseCode responseCode) { if (!Settings.EnableAuditTrail) { return; } - // TODO: flags - _auditWriter.AppendLine($"; EDNS: version: {version}, flags:; udp: {udpSize}"); + _auditWriter.AppendLine($"; EDNS: version: {version}, flags:{(doFlag ? " do" : string.Empty)}; udp: {udpSize}; code: {responseCode}"); } - public void AuditResponse() - { - if (!Settings.EnableAuditTrail) - { - return; - } - - _auditWriter.AppendLine(c_placeHolder); - } - - public void AuditEnd(DnsResponseMessage queryResponse, NameServer nameServer) + public void AuditEnd(IDnsQueryResponse queryResponse, NameServer nameServer) { if (queryResponse is null) { @@ -1526,6 +1754,52 @@ public void AuditEnd(DnsResponseMessage queryResponse, NameServer nameServer) } var elapsed = _swatch.ElapsedMilliseconds; + + // TODO: find better way to print the actual ttl of cached values + if (queryResponse != null) + { + if (queryResponse.Questions.Count > 0) + { + _auditWriter.AppendLine(";; QUESTION SECTION:"); + foreach (var question in queryResponse.Questions) + { + _auditWriter.AppendLine(question.ToString(s_printOffset)); + } + _auditWriter.AppendLine(); + } + + if (queryResponse.Answers.Count > 0) + { + _auditWriter.AppendLine(";; ANSWER SECTION:"); + foreach (var answer in queryResponse.Answers) + { + _auditWriter.AppendLine(answer.ToString(s_printOffset)); + } + _auditWriter.AppendLine(); + } + + if (queryResponse.Authorities.Count > 0) + { + _auditWriter.AppendLine(";; AUTHORITIES SECTION:"); + foreach (var auth in queryResponse.Authorities) + { + _auditWriter.AppendLine(auth.ToString(s_printOffset)); + } + _auditWriter.AppendLine(); + } + + var additionals = queryResponse.Additionals.Where(p => !(p is OptRecord)).ToArray(); + if (additionals.Length > 0) + { + _auditWriter.AppendLine(";; ADDITIONALS SECTION:"); + foreach (var additional in additionals) + { + _auditWriter.AppendLine(additional.ToString(s_printOffset)); + } + _auditWriter.AppendLine(); + } + } + _auditWriter.AppendLine($";; Query time: {elapsed} msec"); _auditWriter.AppendLine($";; SERVER: {nameServer.Address}#{nameServer.Port}"); _auditWriter.AppendLine($";; WHEN: {DateTime.UtcNow.ToString("ddd MMM dd HH:mm:ss K yyyy", CultureInfo.InvariantCulture)}"); @@ -1567,7 +1841,7 @@ public void AuditRetryNextServer(NameServer current) } _auditWriter.AppendLine(); - _auditWriter.AppendLine($"; SERVER: {current.Address}#{current.Port} failed; Retrying with the next server."); + _auditWriter.AppendLine($"; Trying next server."); } } } \ No newline at end of file diff --git a/src/DnsClient/NameServer.cs b/src/DnsClient/NameServer.cs index fcfe038f..24ce0782 100644 --- a/src/DnsClient/NameServer.cs +++ b/src/DnsClient/NameServer.cs @@ -5,6 +5,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; +using DnsClient.Internal; namespace DnsClient { @@ -160,7 +161,7 @@ internal static IReadOnlyCollection Convert(IReadOnlyCollection public override string ToString() { - return $"{Address}:{Port} (Udp: {SupportedUdpPayloadSize ?? 512})"; + return $"{Address}:{Port}"; } /// @@ -200,12 +201,16 @@ public static IReadOnlyCollection ResolveNameServers(bool skipIPv6Si List exceptions = new List(); + var logger = Logging.LoggerFactory?.CreateLogger(typeof(NameServer).FullName); + + logger?.LogDebug("Starting to resolve NameServers, skipIPv6SiteLocal:{0}.", skipIPv6SiteLocal); try { endPoints = QueryNetworkInterfaces(); } catch (Exception ex) { + logger?.LogInformation(ex, "Resolving name servers using .NET framework failed."); exceptions.Add(ex); } @@ -219,6 +224,7 @@ public static IReadOnlyCollection ResolveNameServers(bool skipIPv6Si } catch (Exception ex) { + logger?.LogInformation(ex, "Resolving name servers using native implementation failed."); exceptions.Add(ex); } } @@ -236,6 +242,11 @@ public static IReadOnlyCollection ResolveNameServers(bool skipIPv6Si } } + if (endPoints == null) + { + endPoints = new IPAddress[0]; + } + var filtered = endPoints .Where(p => (p.AddressFamily == AddressFamily.InterNetwork || p.AddressFamily == AddressFamily.InterNetworkV6) && (!p.IsIPv6SiteLocal || !skipIPv6SiteLocal)) @@ -244,6 +255,7 @@ public static IReadOnlyCollection ResolveNameServers(bool skipIPv6Si if (filtered.Length == 0 && fallbackToGooglePublicDns) { + logger?.LogWarning("Could not resolve any NameServers, falling back to Google public servers."); return new NameServer[] { GooglePublicDnsIPv6, @@ -253,6 +265,7 @@ public static IReadOnlyCollection ResolveNameServers(bool skipIPv6Si }; } + logger?.LogDebug("Resolved {0} name servers: [{1}].", filtered.Length, string.Join(",", filtered.AsEnumerable())); return filtered; } @@ -283,11 +296,6 @@ public static IReadOnlyCollection ResolveNameServersNative() addresses = Linux.StringParsingHelpers.ParseDnsAddressesFromResolvConfFile(EtcResolvConfFile).ToArray(); } - ////Console.WriteLine($"{string.Join(",", addresses.Select(p => p.AddressFamily))} " + - //// $"| ipV6LinkLocal: {string.Join(",", addresses.Select(p => p.IsIPv6LinkLocal))}" + - //// $"| ipV6SiteLocal: {string.Join(",", addresses.Select(p => p.IsIPv6SiteLocal))}" + - //// $"({string.Join(",", addresses.Select(p => p.ToString()))})"); - return addresses; } @@ -299,21 +307,11 @@ private static IReadOnlyCollection QueryNetworkInterfaces() var adapters = NetworkInterface.GetAllNetworkInterfaces(); - ////foreach (var adapter in adapters) - ////{ - //// Console.WriteLine($"{adapter.Name}: {adapter.OperationalStatus} {adapter.NetworkInterfaceType} ({string.Join(",", adapter.GetIPProperties().DnsAddresses)})"); - ////} - foreach (NetworkInterface networkInterface in adapters .Where(p => (p.OperationalStatus == OperationalStatus.Up || p.OperationalStatus == OperationalStatus.Unknown) && p.NetworkInterfaceType != NetworkInterfaceType.Loopback)) { - ////Console.WriteLine($"{string.Join(",", networkInterface.GetIPProperties().DnsAddresses.Select(p => p.AddressFamily))} " + - //// $"| ipV6LinkLocal: {string.Join(",", networkInterface.GetIPProperties().DnsAddresses.Select(p => p.IsIPv6LinkLocal))}" + - //// $"| ipV6SiteLocal: {string.Join(",", networkInterface.GetIPProperties().DnsAddresses.Select(p => p.IsIPv6SiteLocal))}" + - //// $"({string.Join(",", networkInterface.GetIPProperties().DnsAddresses)})"); - foreach (IPAddress dnsAddress in networkInterface .GetIPProperties() .DnsAddresses) diff --git a/src/DnsClient/Protocol/DnsResourceRecord.cs b/src/DnsClient/Protocol/DnsResourceRecord.cs index 29ae390e..e9451bc6 100644 --- a/src/DnsClient/Protocol/DnsResourceRecord.cs +++ b/src/DnsClient/Protocol/DnsResourceRecord.cs @@ -85,13 +85,13 @@ public int TimeToLive { get { - var curTicks = Environment.TickCount & int.MaxValue; + var curTicks = (int)((Environment.TickCount & int.MaxValue) / 1000d); if (curTicks < _ticks) { return 0; } - var ttl = InitialTimeToLive - ((curTicks - _ticks) / 1000); + var ttl = InitialTimeToLive - (curTicks - _ticks); return ttl < 0 ? 0 : ttl; } } @@ -136,7 +136,7 @@ public ResourceRecordInfo(DnsString domainName, ResourceRecordType recordType, Q RecordClass = recordClass; RawDataLength = rawDataLength; InitialTimeToLive = timeToLive; - _ticks = Environment.TickCount; + _ticks = (int)(Environment.TickCount / 1000d); } } } \ No newline at end of file diff --git a/src/DnsClient/Protocol/Options/OptRecord.cs b/src/DnsClient/Protocol/Options/OptRecord.cs index 7ac976f3..f307f79f 100644 --- a/src/DnsClient/Protocol/Options/OptRecord.cs +++ b/src/DnsClient/Protocol/Options/OptRecord.cs @@ -38,6 +38,8 @@ 6.1.3. OPT Record TTL Field Use 2: | DO| Z | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + DO flag => https://tools.ietf.org/html/rfc3225 + EXTENDED-RCODE Forms the upper 8 bits of extended 12-bit RCODE (together with the 4 bits defined in [RFC1035]. Note that EXTENDED-RCODE value 0 @@ -121,10 +123,19 @@ public bool IsDnsSecOk public byte[] Data { get; } - public OptRecord(int size = 4096, int version = 0, int length = 0, byte[] data = null) + // used to initiate + public OptRecord(int size = 4096, int version = 0, bool doFlag = false, int length = 0, byte[] data = null) : base(new ResourceRecordInfo(DnsString.RootLabel, ResourceRecordType.OPT, (QueryClass)size, version, length)) { - this.Data = data; + Data = data; + IsDnsSecOk = doFlag; + } + + // returned record + public OptRecord(int size, int ttlFlag, int length, byte[] data) + : base(new ResourceRecordInfo(DnsString.RootLabel, ResourceRecordType.OPT, (QueryClass)size, ttlFlag, length)) + { + Data = data; } private protected override string RecordToString() diff --git a/src/DnsClient/Protocol/UnknownRecord.cs b/src/DnsClient/Protocol/UnknownRecord.cs new file mode 100644 index 00000000..52a10195 --- /dev/null +++ b/src/DnsClient/Protocol/UnknownRecord.cs @@ -0,0 +1,50 @@ +using System; +using System.Text; + +namespace DnsClient.Protocol +{ + /* + https://tools.ietf.org/html/rfc1035#section-3.3.10: + 3.3.10. NULL RDATA format (EXPERIMENTAL) + + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + / / + / / + +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + + Anything at all may be in the RDATA field so long as it is 65535 octets + or less. + */ + + /// + /// Experimental RR, not sure if the implementation is actually correct either (not tested). + /// + /// RFC 1035 + public class UnknownRecord : DnsResourceRecord + { + /// + /// Gets any data stored in this record. + /// + /// + /// The byte array. + /// + public byte[] Data { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The information. + /// The raw data. + /// If or is null. + public UnknownRecord(ResourceRecordInfo info, byte[] data) + : base(info) + { + Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + private protected override string RecordToString() + { + return Convert.ToBase64String(Data); + } + } +} \ No newline at end of file diff --git a/src/DnsClient/ResponseCache.cs b/src/DnsClient/ResponseCache.cs index 3bce5a72..cbc5a4bd 100644 --- a/src/DnsClient/ResponseCache.cs +++ b/src/DnsClient/ResponseCache.cs @@ -115,6 +115,7 @@ public bool Add(string key, IDnsQueryResponse response) if (MinimumTimout == Timeout.InfiniteTimeSpan) { + // TODO: Log warning once? minTtl = s_maxTimeout.TotalMilliseconds; } else if (MinimumTimout.HasValue && minTtl < MinimumTimout.Value.TotalMilliseconds) diff --git a/src/DnsClient/Tracing.cs b/src/DnsClient/Tracing.cs index 97e2ef93..bef5af5e 100644 --- a/src/DnsClient/Tracing.cs +++ b/src/DnsClient/Tracing.cs @@ -8,7 +8,6 @@ namespace DnsClient public static class Tracing { -#if !NETSTANDARD1_3 public static TraceSource Source { get; } = new TraceSource("DnsClient", SourceLevels.Error); // Logger factory which creates a logger writing to the TraceSource above. @@ -97,26 +96,15 @@ private TraceEventType GetTraceEventType(LogLevel logLevel) } } } -#endif } public static class Logging { -#if !NETSTANDARD1_3 /// /// Gets or sets the DnsClient should use. /// Per default it will log to . /// public static ILoggerFactory LoggerFactory { get; set; } = new Tracing.TraceLoggerFactory(); -#else - - /// - /// Gets or sets the DnsClient should use. - /// Per default it will not do anything in netstandard 1.3 targeted projects. - /// - public static ILoggerFactory LoggerFactory { get; set; } = new NullLoggerFactory(); - -#endif } } diff --git a/test/ApiDesign.OldReference/ApiDesign.OldReference.csproj b/test/ApiDesign.OldReference/ApiDesign.OldReference.csproj index 196a8461..bd7e60c0 100644 --- a/test/ApiDesign.OldReference/ApiDesign.OldReference.csproj +++ b/test/ApiDesign.OldReference/ApiDesign.OldReference.csproj @@ -1,18 +1,18 @@ - + - net471;netcoreapp2.1;netcoreapp3.0 + net45;net471;netcoreapp3.0 ../../tools/key.snk true - + - + diff --git a/test/ApiDesign/ApiDesign.csproj b/test/ApiDesign/ApiDesign.csproj index 882ff5a4..05d0fb97 100644 --- a/test/ApiDesign/ApiDesign.csproj +++ b/test/ApiDesign/ApiDesign.csproj @@ -1,7 +1,7 @@  - netcoreapp3.0 + netcoreapp3.1 ApiDesign Exe ApiDesign @@ -13,7 +13,6 @@ - diff --git a/test/ApiDesign/Program.cs b/test/ApiDesign/Program.cs index 92c22050..92b34bf5 100644 --- a/test/ApiDesign/Program.cs +++ b/test/ApiDesign/Program.cs @@ -16,32 +16,39 @@ public static void Main(string[] args) var client = new LookupClient(IPAddress.Loopback); // version b + + var x = client.QueryServer(new NameServer[] { IPAddress.Loopback }, "query", QueryType.A); client = new LookupClient(new LookupClientOptions( - NameServer.Cloudflare, NameServer.Cloudflare2) + NameServer.Cloudflare, NameServer.Cloudflare2) { UseCache = true, ContinueOnDnsError = true, EnableAuditTrail = true, - UseRandomNameServer = false + UseRandomNameServer = false, + ExtendedDnsBufferSize = 4000, + RequestDnsSecRecords = true, + Recursion = true, + MaximumCacheTimeout = TimeSpan.FromSeconds(5) }); - var x = client.QueryServer(new NameServer[] { IPAddress.Loopback }, "query", QueryType.A); - while (true) { - var result = client.Query("google.com", QueryType.A); - var result2 = client.Query("google.com", QueryType.A, queryOptions: new DnsQueryAndServerOptions() - { - ContinueOnDnsError = true - }); - + var result = client.QueryServer(new[] { IPAddress.Loopback }, "mcnet.com", QueryType.ANY); Console.WriteLine(result.AuditTrail); + ////Task.Delay(1003).GetAwaiter().GetResult(); + ////var result2 = client.Query("dnsclient.michaco.net", QueryType.A, queryOptions: new DnsQueryAndServerOptions() + ////{ + //// ContinueOnDnsError = true, + //// EnableAuditTrail = true + ////}); + + ////Console.WriteLine(result2.AuditTrail); if (result.HasError) { Console.WriteLine("Error response: " + result.ErrorMessage); } - Console.WriteLine(); - Task.Delay(1000).GetAwaiter().GetResult(); + WriteLongLine(); + Task.Delay(1300).GetAwaiter().GetResult(); } } diff --git a/test/Benchmarks/Benchmarks.csproj b/test/Benchmarks/Benchmarks.csproj index 44500e7a..d3e58108 100644 --- a/test/Benchmarks/Benchmarks.csproj +++ b/test/Benchmarks/Benchmarks.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;net472 + netcoreapp3.1;net471 true Benchmarks diff --git a/test/Benchmarks/DnsClientBenchmarks.DatagramReader.cs b/test/Benchmarks/DnsClientBenchmarks.DatagramReader.cs index a49cde0e..392b223d 100644 --- a/test/Benchmarks/DnsClientBenchmarks.DatagramReader.cs +++ b/test/Benchmarks/DnsClientBenchmarks.DatagramReader.cs @@ -119,13 +119,10 @@ private class TestMessageHandler : DnsMessageHandler 95, 207, 129, 128, 0, 1, 0, 11, 0, 0, 0, 1, 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 255, 0, 1, 192, 12, 0, 1, 0, 1, 0, 0, 1, 8, 0, 4, 172, 217, 17, 238, 192, 12, 0, 28, 0, 1, 0, 0, 0, 71, 0, 16, 42, 0, 20, 80, 64, 22, 8, 13, 0, 0, 0, 0, 0, 0, 32, 14, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 17, 0, 50, 4, 97, 108, 116, 52, 5, 97, 115, 112, 109, 120, 1, 108, 192, 12, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 4, 0, 10, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 30, 4, 97, 108, 116, 50, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 20, 4, 97, 108, 116, 49, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 40, 4, 97, 108, 116, 51, 192, 91, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 51, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 50, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 52, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 49, 192, 12, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0 }; - public TestMessageHandler() - { - } + public override DnsMessageHandleType Type { get; } = DnsMessageHandleType.UDP; - public override bool IsTransientException(T exception) + public TestMessageHandler() { - return false; } public override DnsResponseMessage Query( diff --git a/test/Benchmarks/DnsClientBenchmarks.RequestResponseParsing.cs b/test/Benchmarks/DnsClientBenchmarks.RequestResponseParsing.cs index 2fc7d832..00421deb 100644 --- a/test/Benchmarks/DnsClientBenchmarks.RequestResponseParsing.cs +++ b/test/Benchmarks/DnsClientBenchmarks.RequestResponseParsing.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -12,27 +14,25 @@ public static partial class DnsClientBenchmarks { public class RequestResponseParsing { - private static readonly DnsRequestMessage _request = new DnsRequestMessage( - new DnsRequestHeader(123, DnsOpCode.Query), - new DnsQuestion("google.com", QueryType.ANY, QueryClass.IN)); + private const int OpsPerMultiRun = 32; + private static readonly DnsQuestion _question = new DnsQuestion("google.com", QueryType.ANY, QueryClass.IN); - private static readonly BenchmarkMessageHandler _handler = new BenchmarkMessageHandler(); - private static readonly BenchmarkMessageHandler _handlerRequest = new BenchmarkMessageHandler(true, false); - private static readonly BenchmarkMessageHandler _handlerResponse = new BenchmarkMessageHandler(false, true); - - private static readonly LookupClient _lookup = new LookupClient(new LookupClientOptions(IPAddress.Loopback) - { - UseCache = false - }); + private static readonly LookupClient _lookup = new LookupClient( + new LookupClientOptions(IPAddress.Loopback) + { + UseCache = false + }, + new BenchmarkMessageHandler(), + new BenchmarkMessageHandler(type: DnsMessageHandleType.TCP)); public RequestResponseParsing() { } [Benchmark(Baseline = true)] - public void RequestAndResponse() + public void QuerySync() { - var result = _lookup.ResolveQuery(_lookup.Settings.NameServers, _lookup.Settings, _handler, _request); + var result = _lookup.Query(_question); if (result.Answers.Count != 11) { throw new InvalidOperationException(); @@ -48,15 +48,69 @@ public void RequestAndResponse() } [Benchmark] - public void Request() + public async Task QueryAsync() { - var result = _lookup.ResolveQuery(_lookup.Settings.NameServers, _lookup.Settings, _handlerRequest, _request); + var result = await _lookup.QueryAsync(_question); + if (result.Answers.Count != 11) + { + throw new InvalidOperationException(); + } + if (result.Questions.Count != 1) + { + throw new InvalidOperationException(); + } + if (!result.Questions[0].QueryName.Equals("google.com.")) + { + throw new InvalidOperationException(); + } } - [Benchmark] - public void Response() + [Benchmark(OperationsPerInvoke = OpsPerMultiRun)] + public void QuerySyncMulti() { - var result = _lookup.ResolveQuery(_lookup.Settings.NameServers, _lookup.Settings, _handlerResponse, _request); + Parallel.Invoke(Enumerable.Repeat(() => Query(), OpsPerMultiRun).ToArray()); + + void Query() + { + var result = _lookup.Query(_question); + if (result.Answers.Count != 11) + { + throw new InvalidOperationException(); + } + if (result.Questions.Count != 1) + { + throw new InvalidOperationException(); + } + if (!result.Questions[0].QueryName.Equals("google.com.")) + { + throw new InvalidOperationException(); + } + } + } + + [Benchmark(OperationsPerInvoke = OpsPerMultiRun)] + public void QueryAsyncMulti() + { + Parallel.Invoke(Enumerable.Repeat(() => Query(), OpsPerMultiRun).ToArray()); + + Task Query() + { + var result = _lookup.QueryAsync(_question).GetAwaiter().GetResult(); + if (result.Answers.Count != 11) + { + throw new InvalidOperationException(); + } + if (result.Questions.Count != 1) + { + throw new InvalidOperationException(); + } + if (!result.Questions[0].QueryName.Equals("google.com.")) + { + throw new InvalidOperationException(); + } + + return Task.CompletedTask; + } } } } @@ -70,42 +124,43 @@ internal class BenchmarkMessageHandler : DnsMessageHandler //result private static readonly byte[] _answer = new byte[] { 95, 207, 129, 128, 0, 1, 0, 11, 0, 0, 0, 1, 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 255, 0, 1, 192, 12, 0, 1, 0, 1, 0, 0, 1, 8, 0, 4, 172, 217, 17, 238, 192, 12, 0, 28, 0, 1, 0, 0, 0, 71, 0, 16, 42, 0, 20, 80, 64, 22, 8, 13, 0, 0, 0, 0, 0, 0, 32, 14, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 17, 0, 50, 4, 97, 108, 116, 52, 5, 97, 115, 112, 109, 120, 1, 108, 192, 12, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 4, 0, 10, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 30, 4, 97, 108, 116, 50, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 20, 4, 97, 108, 116, 49, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 40, 4, 97, 108, 116, 51, 192, 91, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 51, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 50, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 52, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 49, 192, 12, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0 }; - private readonly bool _request; - private readonly bool _response; - private const int MaxSize = 4096; + public override DnsMessageHandleType Type { get; } - public BenchmarkMessageHandler(bool request = true, bool response = true) + public BenchmarkMessageHandler(DnsMessageHandleType type = DnsMessageHandleType.UDP) { - _request = request; - _response = response; - } - - public override bool IsTransientException(T exception) - { - return false; + Type = type; } public override DnsResponseMessage Query( - IPEndPoint server, - DnsRequestMessage request, - TimeSpan timeout) + IPEndPoint server, + DnsRequestMessage request, + TimeSpan timeout) { - if (_request) + using (var writer = new DnsDatagramWriter()) { - using (var writer = new DnsDatagramWriter()) + GetRequestData(request, writer); + if (writer.Data.Count != _questionRaw.Length) { - GetRequestData(request, writer); + throw new Exception(); } } - if (_response) + // Allocation test will probably include this copy of the array, but the data has to be copied, otherwise the ID change would be shared... + using (var writer = new DnsDatagramWriter(new ArraySegment(_answer.ToArray()))) { - var response = GetResponseMessage(new ArraySegment(_answer, 0, _answer.Length)); + writer.Index = 0; + writer.WriteInt16NetworkOrder((short)request.Header.Id); + writer.Index = _answer.Length; + + var response = GetResponseMessage(writer.Data); + + if (response.Header.Id != request.Header.Id) + { + throw new Exception(); + } return response; } - - return new DnsResponseMessage(new DnsResponseHeader(0, 0, 0, 0, 0, 0), 0); } public override Task QueryAsync( @@ -114,7 +169,6 @@ public override Task QueryAsync( CancellationToken cancellationToken, Action cancelationCallback) { - // no need to run async here as we don't do any IO return Task.FromResult(Query(server, request, Timeout.InfiniteTimeSpan)); } } diff --git a/test/DnsClient.Tests/ApiCompatibilityTest.cs b/test/DnsClient.Tests/ApiCompatibilityTest.cs index c16584ed..4e04b038 100644 --- a/test/DnsClient.Tests/ApiCompatibilityTest.cs +++ b/test/DnsClient.Tests/ApiCompatibilityTest.cs @@ -5,8 +5,11 @@ using System.Threading.Tasks; using Xunit; +#if !NETCOREAPP1_1 + namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class ApiCompatibilityTest { [Fact] @@ -27,4 +30,14 @@ public async Task API_1_1_0_CompatAsync() Assert.Null(error); } } -} \ No newline at end of file +} + +#else +namespace System.Diagnostics.CodeAnalysis +{ + public class ExcludeFromCodeCoverageAttribute : Attribute + { + } +} + +#endif \ No newline at end of file diff --git a/test/DnsClient.Tests/DatagramReaderTest.cs b/test/DnsClient.Tests/DatagramReaderTest.cs index 71a018b8..c5e58eb1 100644 --- a/test/DnsClient.Tests/DatagramReaderTest.cs +++ b/test/DnsClient.Tests/DatagramReaderTest.cs @@ -1,9 +1,12 @@ using System; using System.Linq; +using System.Threading.Tasks; +using DnsClient.Internal; using Xunit; namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DatagramReaderTest { private static byte[] ReferenceBitData = new byte[] @@ -17,6 +20,11 @@ public class DatagramReaderTest 0, // 35 }; + static DatagramReaderTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + [Fact] public void DatagramReader_LabelTest_DnsName() { @@ -56,25 +64,30 @@ public void DatagramReader_DnsName_FromBytesInvalidLength() var bytes = new byte[] { 3, 90, 90, 90, 6, 56, 56, 0 }; var reader = new DnsDatagramReader(new ArraySegment(bytes)); - var ex = Assert.ThrowsAny(() => reader.ReadDnsName()); + + Action act = () => reader.ReadDnsName(); + var ex = Assert.ThrowsAny(act); Assert.Equal(5, ex.Index); - Assert.Equal(6, ex.Length); + Assert.Equal(6, ex.ReadLength); } [Fact] public void DatagramReader_DnsName_FromBytesInvalidOffset() { var reader = new DnsDatagramReader(new ArraySegment(new byte[] { 2 })); - var ex = Assert.ThrowsAny(() => reader.ReadDnsName()); + + Action act = () => reader.ReadDnsName(); + var ex = Assert.ThrowsAny(act); Assert.Equal(1, ex.Index); Assert.Single(ex.ResponseData); - Assert.Equal(2, ex.Length); + Assert.Equal(2, ex.ReadLength); } [Fact] public void DatagramReader_IndexOutOfRange() { - Assert.ThrowsAny(() => new DnsDatagramReader(new ArraySegment(new byte[10]), 11)); + Action act = () => new DnsDatagramReader(new ArraySegment(new byte[10]), 11); + Assert.ThrowsAny(act); } [Fact] @@ -83,16 +96,18 @@ public void DatagramReader_ReadByte_IndexOutOfRange() var reader = new DnsDatagramReader(new ArraySegment(new byte[10]), 9); reader.ReadByte(); - var ex = Assert.ThrowsAny(() => reader.ReadByte()); + Action act = () => reader.ReadByte(); + var ex = Assert.ThrowsAny(act); Assert.Equal(10, ex.Index); - Assert.Equal(1, ex.Length); + Assert.Equal(1, ex.ReadLength); } [Fact] public void DatagramReader_IndexOutOfRangeNegativ() { - Assert.ThrowsAny(() => new DnsDatagramReader(new ArraySegment(new byte[10]), -1)); + Action act = () => new DnsDatagramReader(new ArraySegment(new byte[10]), -1); + Assert.ThrowsAny(act); } [Fact] @@ -123,10 +138,12 @@ public void DatagramReader_ReadUIntIndexOutOfRange() var reader = new DnsDatagramReader(new ArraySegment(new byte[2] { 0, 1 })); var result = reader.ReadUInt16(); - var ex = Assert.ThrowsAny(() => reader.ReadUInt16()); + + Action act = () => reader.ReadUInt16(); + var ex = Assert.ThrowsAny(act); Assert.Equal(2, ex.Index); - Assert.Equal(2, ex.Length); + Assert.Equal(2, ex.ReadLength); } [Fact] @@ -135,10 +152,12 @@ public void DatagramReader_ReadUIntReverseIndexOutOfRange() var reader = new DnsDatagramReader(new ArraySegment(new byte[2] { 0, 1 })); var result = reader.ReadUInt16NetworkOrder(); - var ex = Assert.ThrowsAny(() => reader.ReadUInt16NetworkOrder()); + + Action act = () => reader.ReadUInt16NetworkOrder(); + var ex = Assert.ThrowsAny(act); Assert.Equal(2, ex.Index); - Assert.Equal(2, ex.Length); + Assert.Equal(2, ex.ReadLength); } [Fact] @@ -163,9 +182,42 @@ public void DatagramReader_IndexBounds() reader.Advance(4); Assert.False(reader.DataAvailable); - var ex = Assert.Throws(() => reader.Advance(1)); + Action act = () => reader.Advance(1); + var ex = Assert.Throws(act); Assert.Equal(4, ex.Index); - Assert.Equal(1, ex.Length); + Assert.Equal(1, ex.ReadLength); + } + + [Fact] + public void Pool_ParallelTest() + { + for (var i = 0; i < 100; i++) + { + Parallel.Invoke( + new ParallelOptions() + { + MaxDegreeOfParallelism = 16 + }, + Enumerable.Repeat(() => BuildSomething(), 200).ToArray()); + } + + void BuildSomething() + { + var a = StringBuilderObjectPool.Default.Get(); + var b = StringBuilderObjectPool.Default.Get(); + + for (var i = 0; i < 100; i++) + { + a.Append("something"); + b.Append("something else"); + } + + var x = a.ToString(); + var y = b.ToString(); + + StringBuilderObjectPool.Default.Return(a); + StringBuilderObjectPool.Default.Return(b); + } } } } \ No newline at end of file diff --git a/test/DnsClient.Tests/DnsClient.Tests.csproj b/test/DnsClient.Tests/DnsClient.Tests.csproj index 5cef5f3e..354dadfd 100644 --- a/test/DnsClient.Tests/DnsClient.Tests.csproj +++ b/test/DnsClient.Tests/DnsClient.Tests.csproj @@ -1,7 +1,6 @@  - - net471;netcoreapp3.0;netcoreapp2.1 + ;net471;netcoreapp3.0; DnsClient.Tests ../../tools/key.snk true @@ -23,18 +22,29 @@ + + + + - + all runtime; build; native; contentfiles; analyzers - + $(DefineConstants);ENABLE_REMOTE_DNS diff --git a/test/DnsClient.Tests/DnsMessageHandlerTest.cs b/test/DnsClient.Tests/DnsMessageHandlerTest.cs index a46f56f2..fa23c8b9 100644 --- a/test/DnsClient.Tests/DnsMessageHandlerTest.cs +++ b/test/DnsClient.Tests/DnsMessageHandlerTest.cs @@ -1,12 +1,32 @@ -using System.Linq; +using System; +using System.Linq; using System.Net; using DnsClient.Protocol; +using DnsClient.Protocol.Options; using Xunit; namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DnsMessageHandlerTest { + static DnsMessageHandlerTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + + // https://github.com/MichaCo/DnsClient.NET/issues/51 + [Fact] + public void DnsRecordFactory_Issue51() + { + var value = "UsWBgAABAAMAAAAECF9tb25nb2RiBF90Y3ANcXNsLWRldi1wYXJ2dQVhenVyZQdtb25nb2RiA25ldAAAIQABCF9tb25nb2RiBF90Y3ANcXNsLWRldi1wYXJ2dQVhenVyZQdtb25nb2RiA25ldAAAIQABAAAAHgAzAAAAAGmJGXFzbC1kZXYtc2hhcmQtMDAtMDAtcGFydnUFYXp1cmUHbW9uZ29kYgNuZXQACF9tb25nb2RiBF90Y3ANcXNsLWRldi1wYXJ2dQVhenVyZQdtb25nb2RiA25ldAAAIQABAAAAHgAzAAAAAGmJGXFzbC1kZXYtc2hhcmQtMDAtMDEtcGFydnUFYXp1cmUHbW9uZ29kYgNuZXQACF9tb25nb2RiBF90Y3ANcXNsLWRldi1wYXJ2dQVhenVyZQdtb25nb2RiA25ldAAAIQABAAAAHgAzAAAAAGmJGXFzbC1kZXYtc2hhcmQtMDAtMDItcGFydnUFYXp1cmUHbW9uZ29kYgNuZXQAAAApEAAAAAAAABT/nAAQnAgTYxmUVUGBo1503OWO6Blxc2wtZGV2LXNoYXJkLTAwLTAxLXBhcnZ1BWF6dXJlB21vbmdvZGIDbmV0AAABAAEAAAAeAAQoVtVbGXFzbC1kZXYtc2hhcmQtMDAtMDAtcGFydnUFYXp1cmUHbW9uZ29kYgNuZXQAAAEAAQAAAB4ABChFZfIZcXNsLWRldi1zaGFyZC0wMC0wMi1wYXJ2dQVhenVyZQdtb25nb2RiA25ldAAAAQABAAAAHgAENOVxjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + var bytes = Convert.FromBase64String(value); + var handle = new DnsUdpMessageHandler(true); + var result = handle.GetResponseMessage(new ArraySegment(bytes)).AsQueryResponse(new NameServer(IPAddress.Parse("127.0.0.1")), null); + + Assert.Equal(20, result.Additionals.OfType().First().Data.Length); + } + [Fact] public void DnsRecordFactory_ResolveARecord() { diff --git a/test/DnsClient.Tests/DnsRecordFactoryTest.cs b/test/DnsClient.Tests/DnsRecordFactoryTest.cs index d2efa11e..69a41ee2 100644 --- a/test/DnsClient.Tests/DnsRecordFactoryTest.cs +++ b/test/DnsClient.Tests/DnsRecordFactoryTest.cs @@ -8,8 +8,14 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DnsRecordFactoryTest { + static DnsRecordFactoryTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + internal DnsRecordFactory GetFactory(byte[] data) { return new DnsRecordFactory(new DnsDatagramReader(new ArraySegment(data))); @@ -22,7 +28,9 @@ public void DnsRecordFactory_PTRRecordNotEnoughData() var factory = GetFactory(data); var info = new ResourceRecordInfo("query.example.com", ResourceRecordType.PTR, QueryClass.IN, 0, data.Length); - Assert.ThrowsAny(() => factory.GetRecord(info)); + Action act = () => factory.GetRecord(info); + var ex = Assert.ThrowsAny(act); + Assert.Equal(0, ex.Index); } [Fact] @@ -76,7 +84,7 @@ public void DnsRecordFactory_ARecordNotEnoughData() var ex = Assert.ThrowsAny(() => factory.GetRecord(info)); Assert.Contains("IPv4", ex.Message); Assert.Equal(0, ex.Index); - Assert.Equal(4, ex.Length); + Assert.Equal(4, ex.ReadLength); } [Fact] @@ -101,7 +109,7 @@ public void DnsRecordFactory_AAAARecordNotEnoughData() var ex = Assert.ThrowsAny(() => factory.GetRecord(info)); Assert.Contains("IPv6", ex.Message); Assert.Equal(0, ex.Index); - Assert.Equal(16, ex.Length); + Assert.Equal(16, ex.ReadLength); } [Fact] diff --git a/test/DnsClient.Tests/DnsRequestHeaderTest.cs b/test/DnsClient.Tests/DnsRequestHeaderTest.cs index ef5ae3bb..962c9da9 100644 --- a/test/DnsClient.Tests/DnsRequestHeaderTest.cs +++ b/test/DnsClient.Tests/DnsRequestHeaderTest.cs @@ -3,14 +3,14 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DnsRequestHeaderTest { [Fact] public void DnsRequestHeader_ValidateCtor1() { - var header = new DnsRequestHeader(23, DnsOpCode.Notify); + var header = new DnsRequestHeader(DnsOpCode.Notify); - Assert.Equal(23, header.Id); Assert.True(header.UseRecursion); Assert.Equal(DnsOpCode.Notify, header.OpCode); } @@ -18,9 +18,8 @@ public void DnsRequestHeader_ValidateCtor1() [Fact] public void DnsRequestHeader_ValidateCtor2() { - var header = new DnsRequestHeader(23, true, DnsOpCode.Notify); + var header = new DnsRequestHeader(true, DnsOpCode.Notify); - Assert.Equal(23, header.Id); Assert.True(header.UseRecursion); Assert.Equal(DnsOpCode.Notify, header.OpCode); } @@ -28,7 +27,7 @@ public void DnsRequestHeader_ValidateCtor2() [Fact] public void DnsRequestHeader_ChangeRecursion() { - var header = new DnsRequestHeader(23, true, DnsOpCode.Notify); + var header = new DnsRequestHeader(true, DnsOpCode.Notify); Assert.Equal(8448, header.RawFlags); @@ -48,7 +47,6 @@ public void DnsRequestHeader_ChangeRecursion() Assert.True(header.UseRecursion); Assert.Equal(8448, header.RawFlags); - Assert.Equal(23, header.Id); Assert.Equal(DnsOpCode.Notify, header.OpCode); } } diff --git a/test/DnsClient.Tests/DnsResponseParsingTest.cs b/test/DnsClient.Tests/DnsResponseParsingTest.cs index 211f7e18..063d26a5 100644 --- a/test/DnsClient.Tests/DnsResponseParsingTest.cs +++ b/test/DnsClient.Tests/DnsResponseParsingTest.cs @@ -8,23 +8,28 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DnsResponseParsingTest { - private static readonly DnsRequestMessage _nullRequestMessage = - new DnsRequestMessage(new DnsRequestHeader(0, DnsOpCode.Query), new DnsQuestion("bla", QueryType.A, QueryClass.IN)); + private static readonly DnsQuestion s_question = new DnsQuestion("query", QueryType.A); private readonly LookupClient _client; + static DnsResponseParsingTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + public DnsResponseParsingTest() { - _client = new LookupClient(IPAddress.Loopback); + _client = new LookupClient(new LookupClientOptions(IPAddress.Loopback), new TestMessageHandler(), new TestMessageHandler(DnsMessageHandleType.TCP)); } [Fact] public void DnsRecordFactory_McnetValidateSupport() { var types = (ResourceRecordType[])Enum.GetValues(typeof(ResourceRecordType)); - var result = _client.ResolveQuery(_client.Settings.ShuffleNameServers(), _client.Settings, new TestMessageHandler(), _nullRequestMessage); + var result = _client.Query(s_question); var ignore = new ResourceRecordType[] { @@ -50,9 +55,7 @@ public void DnsRecordFactory_McnetValidateSupport() [Fact] public void DnsRecordFactory_McnetA() { - var result = _client.ResolveQuery(_client.Settings.ShuffleNameServers(), _client.Settings, new TestMessageHandler(), _nullRequestMessage); - - var records = result.Answers.ARecords().ToArray(); + var records = GetTypedRecords(); Assert.Contains(records, p => p.Address.Equals(IPAddress.Parse("192.168.178.1"))); Assert.Contains(records, p => p.Address.Equals(IPAddress.Parse("192.168.178.2"))); @@ -302,36 +305,10 @@ public void DnsRecordFactory_McnetWKS() Assert.NotNull(validateRecord); } - [Fact] - public void DnsRecordPerformance() - { - var client = new LookupClient( - new LookupClientOptions() - { - UseTcpOnly = false, - UseTcpFallback = false, - UseCache = false - }, - new TestMessageHandler(), - new TestMessageHandler()); - - var source = new CancellationTokenSource(1000); - - var count = 0; - while (!source.Token.IsCancellationRequested) - { - var result = client.Query("mcnet.com", QueryType.ANY); - count++; - } - - Assert.True(count > 100); - } - private TRecord[] GetTypedRecords() where TRecord : DnsResourceRecord { - var result = _client.ResolveQuery(_client.Settings.ShuffleNameServers(), _client.Settings, new TestMessageHandler(), _nullRequestMessage); - + var result = _client.Query(s_question); var records = result.Answers.OfType().ToArray(); return records; } @@ -346,13 +323,11 @@ private class TestMessageHandler : DnsMessageHandler 92,159,132,128,0,1,0,65,0,0,0,1,5,109,99,110,101,116,3,99,111,109,0,0,252,0,1,192,12,0,6,0,1,0,0,0,100,0,39,3,110,115,49,192,12,10,104,111,115,116,109,97,115,116,101,114,192,12,120,57,35,168,0,9,58,128,0,1,81,128,0,36,234,0,0,9,58,128,192,12,0,16,0,1,0,0,0,100,0,68,4,109,111,114,101,11,40,116,101,120,116,32,119,105,116,104,41,17,34,115,112,101,99,105,97,108,34,32,195,182,195,164,195,156,33,6,194,167,36,37,92,47,6,64,115,116,117,102,102,4,59,97,110,100,13,59,102,97,107,101,32,99,111,109,109,101,110,116,192,12,0,16,0,1,0,0,0,100,0,53,4,115,111,109,101,4,116,101,120,116,12,115,101,112,97,114,97,116,101,100,32,98,121,5,115,112,97,99,101,4,119,105,116,104,3,110,101,119,4,108,105,110,101,5,115,116,117,102,102,3,116,111,111,192,12,0,16,0,1,0,0,0,100,0,76,7,97,110,111,116,104,101,114,4,109,111,114,101,11,40,116,101,120,116,32,119,105,116,104,41,17,34,115,112,101,99,105,97,108,34,32,195,182,195,164,195,156,33,6,194,167,36,37,92,47,6,64,115,116,117,102,102,4,59,97,110,100,13,59,102,97,107,101,32,99,111,109,109,101,110,116,192,12,0,16,0,1,0,0,0,100,0,49,48,115,111,109,101,32,108,111,110,103,32,116,101,120,116,32,119,105,116,104,32,115,111,109,101,32,115,116,117,102,102,32,105,110,32,105,116,32,123,108,97,108,97,58,98,108,117,98,125,192,12,0,7,0,1,0,0,0,100,0,6,3,115,114,118,192,12,192,12,0,7,0,1,0,0,0,100,0,62,15,108,195,156,195,164,39,108,97,195,188,195,182,35,50,120,22,88,78,45,45,67,76,67,72,67,48,69,65,48,66,50,71,50,65,57,71,67,68,11,88,78,45,45,48,90,87,77,53,54,68,5,109,99,110,101,116,3,99,111,109,0,192,12,0,8,0,1,0,0,0,100,0,9,6,104,105,100,100,101,110,192,12,192,12,0,9,0,1,0,0,0,100,0,10,7,104,105,100,100,101,110,50,192,12,192,12,0,9,0,1,0,0,0,100,0,60,10,120,110,45,45,52,103,98,114,105,109,26,120,110,45,45,45,45,121,109,99,98,97,97,97,106,108,99,54,100,106,55,98,120,110,101,50,99,10,120,110,45,45,119,103,98,104,49,99,5,109,99,110,101,116,3,99,111,109,0,192,12,0,33,0,1,0,0,0,100,0,22,0,1,0,1,31,144,4,115,114,118,52,5,109,99,110,101,116,3,99,111,109,0,194,90,0,12,0,1,0,0,1,244,0,2,194,90,194,90,0,12,0,1,0,0,1,244,0,2,193,220,194,90,0,14,0,1,0,0,0,100,0,4,193,220,193,241,194,90,1,1,0,1,0,0,1,244,0,31,1,6,112,111,108,105,99,121,49,46,51,46,54,46,49,46,52,46,49,46,51,53,52,48,53,46,54,54,54,46,49,194,90,1,1,0,1,0,0,1,244,0,77,129,3,116,98,115,77,68,73,71,65,49,85,69,74,81,89,74,89,73,90,73,65,87,85,68,66,65,73,66,66,67,65,88,122,74,103,80,97,111,84,55,70,101,88,97,80,122,75,118,54,109,73,50,68,48,121,105,108,105,102,43,55,87,104,122,109,104,77,71,76,101,47,111,66,65,61,61,194,90,1,1,0,1,0,0,1,244,0,54,255,16,115,111,109,101,116,104,105,110,103,115,116,114,97,110,103,101,84,104,101,118,97,108,117,101,83,116,105,110,103,119,105,116,104,195,156,98,101,114,83,112,101,99,105,195,182,108,118,97,108,117,101,46,194,90,1,0,0,1,0,0,1,94,0,30,0,10,0,1,102,116,112,58,47,47,115,114,118,46,109,99,110,101,116,46,99,111,109,47,112,117,98,108,105,99,194,90,0,10,0,1,0,0,1,94,0,117,101,109,115,48,49,46,121,111,117,114,45,102,114,101,101,100,111,109,46,100,101,59,85,83,59,49,57,56,46,50,53,53,46,51,48,46,50,52,50,59,48,59,53,55,51,48,59,100,101,102,97,117,108,116,44,118,111,108,117,109,101,44,110,111,114,116,104,97,109,101,114,105,99,97,44,105,110,116,101,114,97,99,116,105,118,101,44,118,111,105,112,44,111,112,101,110,118,112,110,44,112,112,116,112,44,115,111,99,107,115,53,44,102,114,101,101,59,194,90,0,44,0,1,0,0,0,200,0,22,1,1,157,186,85,206,163,184,225,85,40,102,90,103,129,202,124,53,25,12,240,236,194,90,0,17,0,1,0,0,0,100,0,37,3,109,105,97,1,99,5,109,99,110,101,116,3,99,111,109,0,4,97,100,100,114,3,109,105,97,5,109,99,110,101,116,3,99,111,109,0,194,90,0,17,0,1,0,0,0,100,0,39,4,97,108,101,120,1,98,5,109,99,110,101,116,3,99,111,109,0,4,97,100,100,114,4,97,108,101,120,5,109,99,110,101,116,3,99,111,109,0,194,90,0,17,0,1,0,0,0,100,0,19,4,108,117,99,121,1,99,5,109,99,110,101,116,3,99,111,109,0,0,194,90,0,17,0,1,0,0,0,100,0,36,5,109,105,99,104,97,1,99,5,109,99,110,101,116,3,99,111,109,0,5,109,105,99,104,97,5,109,99,110,101,116,3,99,111,109,0,196,204,0,18,0,1,0,0,0,100,0,18,0,1,4,115,114,118,50,5,109,99,110,101,116,3,99,111,109,0,196,234,0,18,0,1,0,0,0,100,0,18,0,2,4,115,114,118,51,5,109,99,110,101,116,3,99,111,109,0,197,8,0,11,0,1,0,0,0,100,0,72,192,168,178,25,6,0,84,64,64,0,0,0,0,0,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,197,8,0,11,0,1,0,0,0,100,15,216,192,168,178,26,17,1,0,0,0,0,0,4,0,0,0,128,0,0,4,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,197,8,0,13,0,1,0,0,0,100,0,17,8,73,110,116,101,108,45,73,55,7,87,73,78,68,79,87,83,197,8,0,13,0,1,0,0,0,100,0,14,8,83,112,97,114,99,45,49,48,4,85,78,73,88,197,8,0,2,0,1,0,9,58,128,0,2,192,39,197,8,0,2,0,1,0,9,58,128,0,6,3,110,115,50,197,8,197,8,0,1,0,1,0,1,81,128,0,4,192,168,178,50,197,8,0,28,0,1,0,0,0,100,0,16,254,128,0,0,0,0,0,0,36,201,142,20,157,244,163,20,197,8,0,15,0,1,0,0,3,232,0,9,0,10,4,109,97,105,108,197,8,197,8,0,15,0,1,0,0,3,232,0,9,0,10,4,109,97,105,108,196,198,8,95,97,102,115,51,45,112,114,4,95,116,99,112,5,109,99,110,101,116,3,99,111,109,0,0,33,0,1,0,0,0,100,0,22,0,0,0,0,27,90,4,115,114,118,52,5,109,99,110,101,116,3,99,111,109,0,8,95,97,102,115,51,45,118,108,214,1,0,33,0,1,0,0,0,100,0,22,0,0,0,0,27,91,4,115,114,118,50,5,109,99,110,101,116,3,99,111,109,0,8,95,97,102,115,51,45,112,114,4,95,117,100,112,5,109,99,110,101,116,3,99,111,109,0,0,33,0,1,0,0,0,100,0,22,0,0,0,0,27,90,4,115,114,118,51,5,109,99,110,101,116,3,99,111,109,0,196,120,0,1,0,1,0,0,0,100,0,4,192,168,178,61,196,115,0,16,0,1,0,0,0,100,0,19,18,69,114,110,115,116,32,40,49,50,51,41,32,52,53,54,55,56,57,4,109,97,105,108,196,120,0,15,0,1,0,0,0,100,0,4,0,10,213,220,193,220,0,1,0,1,0,0,0,100,0,4,192,168,178,24,193,241,0,1,0,1,0,0,0,100,0,4,192,168,178,24,213,220,0,1,0,1,0,1,81,128,0,4,192,168,178,3,196,70,0,1,0,1,0,0,0,100,0,4,192,168,178,62,196,65,0,16,0,1,0,0,0,100,0,17,16,89,101,121,32,40,49,50,51,41,32,52,53,54,55,56,57,4,109,97,105,108,196,70,0,15,0,1,0,0,0,100,0,4,0,10,213,220,196,198,0,5,0,1,0,0,0,100,0,2,214,138,4,97,100,100,114,196,198,0,16,0,1,0,0,0,100,0,18,17,66,111,115,115,32,40,49,50,51,41,32,52,53,54,55,56,57,213,241,0,1,0,1,0,0,0,100,0,4,192,168,178,4,3,115,117,98,196,198,0,5,0,1,0,0,0,200,0,4,1,50,196,198,3,119,119,119,196,198,0,5,0,1,0,0,0,100,0,2,214,138,192,39,0,1,0,1,0,1,81,128,0,4,192,168,178,1,213,156,0,1,0,1,0,1,81,128,0,4,192,168,178,2,5,112,104,111,110,101,214,138,0,1,0,1,0,0,0,100,0,4,192,168,178,20,193,128,0,1,0,1,0,0,0,100,0,4,192,168,178,21,193,128,0,13,0,1,0,0,0,100,0,17,8,73,110,116,101,108,45,73,53,7,87,73,78,68,79,87,83,214,76,0,1,0,1,0,0,0,100,0,4,192,168,178,25,214,133,0,1,0,1,0,0,0,100,0,4,192,168,178,26,214,33,0,1,0,1,0,0,0,100,0,4,192,168,178,27,4,117,98,101,114,214,138,0,1,0,1,0,0,0,100,0,4,192,168,178,30,3,119,119,119,214,138,0,5,0,1,0,0,1,244,0,2,214,138,193,162,0,1,0,1,0,0,0,100,0,4,192,168,178,100,214,138,0,6,0,1,0,0,0,100,0,24,192,39,192,45,120,57,35,168,0,9,58,128,0,1,81,128,0,36,234,0,0,9,58,128,0,0,41,16,0,0,0,0,0,0,0 }; - public TestMessageHandler() - { - } + public override DnsMessageHandleType Type { get; } - public override bool IsTransientException(T exception) + public TestMessageHandler(DnsMessageHandleType type = DnsMessageHandleType.UDP) { - return false; + Type = type; } public override DnsResponseMessage Query( @@ -360,7 +335,17 @@ public override DnsResponseMessage Query( DnsRequestMessage request, TimeSpan timeout) { - var response = GetResponseMessage(new ArraySegment(ZoneData, 0, ZoneData.Length)); + var writer = new DnsDatagramWriter(new ArraySegment(ZoneData.ToArray())); + writer.Index = 0; + writer.WriteInt16NetworkOrder((short)request.Header.Id); + writer.Index = ZoneData.Length; + + var response = GetResponseMessage(writer.Data); + + if (response.Header.Id != request.Header.Id) + { + throw new Exception(); + } return response; } diff --git a/test/DnsClient.Tests/DnsStringTest.cs b/test/DnsClient.Tests/DnsStringTest.cs index c7a7578d..cc2c4df9 100644 --- a/test/DnsClient.Tests/DnsStringTest.cs +++ b/test/DnsClient.Tests/DnsStringTest.cs @@ -4,6 +4,7 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class DnsStringTest { [Fact] diff --git a/test/DnsClient.Tests/LookupClientRetryTest.cs b/test/DnsClient.Tests/LookupClientRetryTest.cs index 1eb2165d..f75410a9 100644 --- a/test/DnsClient.Tests/LookupClientRetryTest.cs +++ b/test/DnsClient.Tests/LookupClientRetryTest.cs @@ -1,139 +1,131 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using DnsClient.Protocol; using Xunit; namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class LookupClientRetryTest { + // https://github.com/MichaCo/DnsClient.NET/issues/52 + private static readonly byte[] s_issue52data = new byte[] { 31, 169, 129, 128, 0, 1, 0, 18, 0, 0, 0, 1, 20, 95, 97, 99, 109, 101, 45, 99, 104, 97, 108, 108, 101, 110, 103, 101, 45, 116, 101, 115, 116, 8, 97, 122, 117, 114, 101, 100, 110, 115, 4, 115, 105, 116, 101, 0, 0, 16, 0, 1, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 42, 41, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 51, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 42, 41, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 52, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 43, 42, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 100, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 118, 66, 73, 57, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 43, 42, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 118, 97, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 43, 42, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 118, 115, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 43, 42, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 118, 119, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 43, 42, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 107, 55, 86, 85, 76, 82, 119, 101, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 44, 43, 87, 84, 98, 112, 74, 121, 122, 81, 87, 112, 85, 112, 82, 97, 45, 108, 73, 95, 118, 73, 95, 121, 51, 95, 68, 101, 75, 80, 48, 77, 114, 109, 121, 87, 52, 55, 86, 85, 76, 82, 118, 66, 73, 192, 12, 0, 16, 0, 1, 0, 0, 0, 60, 0, 44, 43, 87, 84, 98, 112, 74, 121, 122, 81 }; + + static LookupClientRetryTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + [Fact] - public void Lookup_PreserveNameServerOrder() + public void PreserveNameServerOrder() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { UseRandomNameServer = false, UseCache = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NoError, 0, 0, 0, 0), + new DnsResponseHeader(req.Header.Id, (int)DnsResponseCode.NoError, 0, 0, 0, 0), 0); }); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + // calling multiple times will always use the first server when it returns NoError. // No other servers should be called for (var i = 0; i < 3; i++) { - var request = new DnsRequestMessage( - new DnsRequestHeader(i, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - - var result = lookup.ResolveQuery(servers, lookup.Settings, messageHandler, request); + var result = lookup.Query("test.com", QueryType.A); Assert.False(result.HasError); } - Assert.Equal(3, calledIps.Count); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[0]); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[1]); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[2]); + Assert.Equal(9, calledIps.Count); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[0]); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[3]); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[6]); } [Fact] - public async Task Lookup_PreserveNameServerOrderAsync() + public async Task PreserveNameServerOrderAsync() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { UseRandomNameServer = false, UseCache = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NoError, 0, 0, 0, 0), + new DnsResponseHeader(req.Header.Id, (int)DnsResponseCode.NoError, 0, 0, 0, 0), 0); }); - // calling multiple times will always use the first server when it returns NoError. - // No other servers should be called + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + for (var i = 0; i < 3; i++) { - var request = new DnsRequestMessage( - new DnsRequestHeader(i, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - - var result = await lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); Assert.False(result.HasError); } - Assert.Equal(3, calledIps.Count); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[0]); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[1]); - Assert.Equal(IPAddress.Parse("127.0.0.1"), calledIps[2]); + // 3 servers get called 3 times because none returns a response. Order should be the same + // so every first server of each loop should be our first one. + Assert.Equal(9, calledIps.Count); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[0]); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[3]); + Assert.Equal(IPAddress.Parse("127.0.10.1"), calledIps[6]); } [Fact] - public void Lookup_ShuffleNameServerOrder() + public void ShuffleNameServerOrder() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { UseRandomNameServer = true, UseCache = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); var uniqueIps = new HashSet(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); uniqueIps.Add(ip.Address); return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.BadAlgorithm, 0, 0, 0, 0), + new DnsResponseHeader(req.Header.Id, (int)DnsResponseCode.BadAlgorithm, 0, 0, 0, 0), 0); }); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + // consecutive calls should cycle through the servers (not always using the first one) // because this is only one thread, the order is always the same. But this can be very // different with multiple tasks) for (var i = 0; i < 6; i++) { - var request = new DnsRequestMessage( - new DnsRequestHeader(i, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - - var result = lookup.ResolveQuery(servers, lookup.Settings, messageHandler, request); + var result = lookup.Query("test.com", QueryType.A); Assert.True(result.HasError); } @@ -142,40 +134,34 @@ public void Lookup_ShuffleNameServerOrder() } [Fact] - public async Task Lookup_ShuffleNameServerOrderAsync() + public async Task ShuffleNameServerOrderAsync() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { UseRandomNameServer = true, UseCache = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.BadAlgorithm, 0, 0, 0, 0), + new DnsResponseHeader(req.Header.Id, (int)DnsResponseCode.BadAlgorithm, 0, 0, 0, 0), 0); }); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + // consecutive calls should cycle through the servers (not always using the first one) // because this is only one thread, the order is always the same. But this can be very // different with multiple tasks) for (var i = 0; i < 6; i++) { - var request = new DnsRequestMessage( - new DnsRequestHeader(i, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - - var result = await lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); Assert.True(result.HasError); } @@ -183,350 +169,1429 @@ public async Task Lookup_ShuffleNameServerOrderAsync() } [Fact] - public void Lookup_RetryOnDnsError() + public void DnsResponseException_Continue_DontThrow() { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3")), - new NameServer(IPAddress.Parse("127.0.0.4")), - new NameServer(IPAddress.Parse("127.0.0.5")), - new NameServer(IPAddress.Parse("127.0.0.6")), - new NameServer(IPAddress.Parse("127.0.0.7"))) + new NameServer(ip1), + new NameServer(ip2), + new NameServer(IPAddress.Parse("127.0.10.4"))) { EnableAuditTrail = true, ContinueOnDnsError = true, - UseCache = false + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { + var status = DnsHeaderResponseCode.NotExistentDomain; + + // response from ip1 & 2 should be retried with same server. + if (ip.Address == ip1) + { + status = DnsHeaderResponseCode.FormatError; + } + if (ip.Address == ip2) + { + status = DnsHeaderResponseCode.ServerFailure; + } + calledIps.Add(ip.Address); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (ushort)status, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - var result = lookup.ResolveQuery(servers, lookup.Settings, messageHandler, request); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); // no exception but error in the response after calling all endpoints! Assert.True(result.HasError); - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Header.ResponseCode); + Assert.Equal(DnsHeaderResponseCode.NotExistentDomain, result.Header.ResponseCode); - // ensure the client tried all the configured endpoints - Assert.Equal(7, calledIps.Count); + // 1x 1 try, 2x 4 tries + Assert.Equal(9, calledIps.Count); } [Fact] - public async Task Lookup_RetryOnDnsErrorAsync() + public async Task DnsResponseException_Continue_DontThrow_Async() { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3")), - new NameServer(IPAddress.Parse("127.0.0.4")), - new NameServer(IPAddress.Parse("127.0.0.5")), - new NameServer(IPAddress.Parse("127.0.0.6")), - new NameServer(IPAddress.Parse("127.0.0.7"))) + new NameServer(ip1), + new NameServer(ip2), + new NameServer(IPAddress.Parse("127.0.10.4"))) { EnableAuditTrail = true, ContinueOnDnsError = true, - UseCache = false + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { + var status = DnsHeaderResponseCode.NotExistentDomain; + + // response from ip1 & 2 should be retried with same server. + if (ip.Address == ip1) + { + status = DnsHeaderResponseCode.FormatError; + } + if (ip.Address == ip2) + { + status = DnsHeaderResponseCode.ServerFailure; + } + calledIps.Add(ip.Address); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (ushort)status, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - var result = await lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); // no exception but error in the response after calling all 3 endpoints! Assert.True(result.HasError); - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Header.ResponseCode); + Assert.Equal(DnsHeaderResponseCode.NotExistentDomain, result.Header.ResponseCode); - // ensure the client tried all the configured endpoints - Assert.Equal(7, calledIps.Count); + // 1x 1 try, 2x 4 tries + Assert.Equal(9, calledIps.Count); } [Fact] - public void Lookup_RetryOnDnsErrorAndThrow() + public void DnsResponseException_Continue_Throw() { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(ip1), + new NameServer(ip2), + new NameServer(IPAddress.Parse("127.0.10.4"))) { EnableAuditTrail = true, ContinueOnDnsError = true, ThrowDnsErrors = true, - UseCache = false, - UseRandomNameServer = false + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { - calledIps.Add(ip.Address); + var status = DnsHeaderResponseCode.NotExistentDomain; - if (ip.Address.Equals(IPAddress.Parse("127.0.0.3"))) + // response from ip1 & 2 should be retried with same server. + if (ip.Address == ip1) { - // last one returns different result - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.FormatError, 0, 0, 0, 0), - 0); + status = DnsHeaderResponseCode.FormatError; + } + if (ip.Address == ip2) + { + status = DnsHeaderResponseCode.ServerFailure; } - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (ushort)status, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var request = new DnsQuestion("test.com", QueryType.A, QueryClass.IN); // all three servers have been called and we get the last exception thrown - var result = Assert.ThrowsAny(() => lookup.ResolveQuery(lookup.Settings.NameServers, lookup.Settings, messageHandler, request)); + var result = Assert.ThrowsAny(() => lookup.Query(request)); // ensure the error is the one from the last call - Assert.Equal(DnsResponseCode.FormatError, result.Code); + Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); - // ensuer all 3 configured servers were called before throwing the exception - Assert.Equal(3, calledIps.Count); + Assert.Equal(9, calledIps.Count); } [Fact] - public async Task Lookup_RetryOnDnsErrorAndThrowAsync() + public async Task DnsResponseException_Continue_Throw_Async() { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(ip1), + new NameServer(ip2), + new NameServer(IPAddress.Parse("127.0.10.4"))) { EnableAuditTrail = true, ContinueOnDnsError = true, ThrowDnsErrors = true, - UseRandomNameServer = false + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { - calledIps.Add(ip.Address); + var status = DnsHeaderResponseCode.NotExistentDomain; - if (ip.Address.Equals(IPAddress.Parse("127.0.0.3"))) + // response from ip1 & 2 should be retried with same server. + if (ip.Address == ip1) + { + status = DnsHeaderResponseCode.FormatError; + } + if (ip.Address == ip2) { - // last one returns different result - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.FormatError, 0, 0, 0, 0), - 0); + status = DnsHeaderResponseCode.ServerFailure; } - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (ushort)status, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - // all three servers have been called and we get the last exception thrown - var servers = lookup.Settings.ShuffleNameServers(); - var result = await Assert.ThrowsAnyAsync(() => lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request)); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var request = new DnsQuestion("test.com", QueryType.A, QueryClass.IN); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(request)); // ensure the error is the one from the last call - Assert.Equal(DnsResponseCode.FormatError, result.Code); + Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); - // ensuer all 3 configured servers were called before throwing the exception - Assert.Equal(3, calledIps.Count); + Assert.Equal(9, calledIps.Count); } [Fact] - public void Lookup_NoRetryOnDnsError() + public void DnsResponseException_DontContinue_DontThrow() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { EnableAuditTrail = true, - ContinueOnDnsError = false // disable retry/continue on dns error + ContinueOnDnsError = false, + ThrowDnsErrors = false, + Retries = 5, + UseCache = true, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NotExistentDomain, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - var result = lookup.ResolveQuery(servers, lookup.Settings, messageHandler, request); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); Assert.True(result.HasError); - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Header.ResponseCode); + Assert.Equal(DnsHeaderResponseCode.NotExistentDomain, result.Header.ResponseCode); // ensure we got the error right from the first server call Assert.Single(calledIps); } [Fact] - public async Task Lookup_NoRetryOnDnsErrorAsync() + public async Task DnsResponseException_DontContinue_DontThrow_Async() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { EnableAuditTrail = true, - ContinueOnDnsError = false // disable retry/continue on dns error + ContinueOnDnsError = false, + ThrowDnsErrors = false, + Retries = 5, + UseCache = true, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); - var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NotExistentDomain, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - - var servers = lookup.Settings.ShuffleNameServers(); - var result = await lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); Assert.True(result.HasError); - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Header.ResponseCode); + Assert.Equal(DnsHeaderResponseCode.NotExistentDomain, result.Header.ResponseCode); // ensure we got the error right from the first server call Assert.Single(calledIps); } [Fact] - public void Lookup_NoRetryOnDnsErrorAndThrow() + public void DnsResponseException_DontContinue_Throw() { var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) { EnableAuditTrail = true, - ContinueOnDnsError = false, // disable retry/continue on dns error - ThrowDnsErrors = true // enable throw dns error (should throw the error of the last one + ContinueOnDnsError = false, + ThrowDnsErrors = true, + Retries = 5, + UseCache = true, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NotExistentDomain, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var request = new DnsQuestion("test.com", QueryType.A, QueryClass.IN); + var result = Assert.ThrowsAny(() => lookup.Query(request)); + + Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); + + Assert.Single(calledIps); + } + + [Fact] + public async Task DnsResponseException_DontContinue_Throw_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = true, + Retries = 5, + UseCache = true, + UseRandomNameServer = false, + UseTcpFallback = false + }; var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NotExistentDomain, 0, 0, 0, 0), 0); + }); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var request = new DnsQuestion("test.com", QueryType.A, QueryClass.IN); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(request)); + + Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); + Assert.Single(calledIps); + } + + /* ContinueOnEmptyResponse */ + + [Fact] + public void ContinueOnEmptyResponse_ShouldTryNextServer_OnEmptyResponse() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = true, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = true, + Retries = 5, + UseCache = false, + UseRandomNameServer = false, + UseTcpFallback = false + }; + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = lookup.Query("test.com", QueryType.A); - var servers = lookup.Settings.ShuffleNameServers(); - var result = Assert.ThrowsAny(() => lookup.ResolveQuery(servers, lookup.Settings, messageHandler, request)); + Assert.Equal(3, calledIps.Count); + } - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); + [Fact] + public async Task ContinueOnEmptyResponse_ShouldTryNextServer_OnEmptyResponse_Async() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = true, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = true, + Retries = 5, + UseCache = false, + UseRandomNameServer = false, + UseTcpFallback = false + }; + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await lookup.QueryAsync("test.com", QueryType.A); + + Assert.Equal(3, calledIps.Count); + } + + [Fact] + public void ContinueOnEmptyResponse_ShouldNotTryNextServer_OnOkResponse() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = true, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = true, + Retries = 5, + UseCache = false, + UseRandomNameServer = false, + UseTcpFallback = false + }; + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + var response = new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 1, 0, 0), 0); + response.AddAnswer(new ARecord(new ResourceRecordInfo("google.com", ResourceRecordType.A, QueryClass.IN, 100, 50), IPAddress.Parse("172.217.22.238"))); + return response; + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = lookup.Query("test.com", QueryType.A); Assert.Single(calledIps); } [Fact] - public async Task Lookup_NoRetryOnDnsErrorAndThrowAsync() + public async Task ContinueOnEmptyResponse_ShouldNotTryNextServer_OnOkResponse_Async() { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); var options = new LookupClientOptions( - new NameServer(IPAddress.Parse("127.0.0.1")), - new NameServer(IPAddress.Parse("127.0.0.2")), - new NameServer(IPAddress.Parse("127.0.0.3"))) + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) { + ContinueOnEmptyResponse = true, EnableAuditTrail = true, ContinueOnDnsError = false, - ThrowDnsErrors = true + ThrowDnsErrors = true, + Retries = 5, + UseCache = false, + UseRandomNameServer = false, + UseTcpFallback = false }; - var lookup = new LookupClient(options); + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + var response = new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 1, 0, 0), 0); + response.AddAnswer(new ARecord(new ResourceRecordInfo("google.com", ResourceRecordType.A, QueryClass.IN, 100, 50), IPAddress.Parse("172.217.22.238"))); + return response; + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await lookup.QueryAsync("test.com", QueryType.A); + + Assert.Single(calledIps); + } + + /* DNS response parse error handling and retries */ + + // https://github.com/MichaCo/DnsClient.NET/issues/52 + [Fact] + public void DnsResponseParseException_ShouldTryTcp_Issue52() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = false, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; var calledIps = new List(); - var messageHandler = new TestMessageHandler((ip, req) => + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => { calledIps.Add(ip.Address); - return new DnsResponseMessage( - new DnsResponseHeader(1, (int)DnsResponseCode.NotExistentDomain, 0, 0, 0, 0), - 0); + calledUdp = true; + + // A result which didn't fit into 512 bytes and which was not marked via TC as truncated. + // The server returned 512 bytes because the response was truncated by a firewall or so. + // This should retry the full request via TCP + var handle = new DnsUdpMessageHandler(true); + return handle.GetResponseMessage(new ArraySegment(s_issue52data)); }); - var request = new DnsRequestMessage( - new DnsRequestHeader(0, DnsOpCode.Query), - new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); - var servers = lookup.Settings.ShuffleNameServers(); - var result = await Assert.ThrowsAnyAsync(() => lookup.ResolveQueryAsync(servers, lookup.Settings, messageHandler, request)); + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); - Assert.Equal(DnsResponseCode.NotExistentDomain, result.Code); + Assert.False(result.HasError); - Assert.Single(calledIps); + Assert.True(calledUdp); + Assert.True(calledTcp); + + // It should fallback to TCP right away and call the first server twice, and then try all other servers because we don't return any answers + Assert.Equal(2, calledIps.Count); + Assert.Equal(new[] { ip1, ip1 }, calledIps); } - } - [ExcludeFromCodeCoverage] - internal class TestMessageHandler : DnsMessageHandler - { - private readonly Func _onQuery; + [Fact] + public async Task DnsResponseParseException_ShouldTryTcp_Issue52_Async() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = false, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + + // A result which didn't fit into 512 bytes and which was not marked via TC as truncated. + // The server returned 512 bytes because the response was truncated by a firewall or so. + // This should retry the full request via TCP + var handle = new DnsUdpMessageHandler(true); + return handle.GetResponseMessage(new ArraySegment(s_issue52data)); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + + Assert.False(result.HasError); + + Assert.True(calledUdp); + Assert.True(calledTcp); - public TestMessageHandler(Func onQuery) + // It should fallback to TCP right away and call the first server twice. + Assert.Equal(2, calledIps.Count); + Assert.Equal(new[] { ip1, ip1 }, calledIps); + } + + [Fact] + public void DnsResponseParseException_ShouldTryTcp_LargerResponse() { - _onQuery = onQuery ?? throw new ArgumentNullException(nameof(onQuery)); + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = true, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + + // This should retry the full request via TCP. + throw new DnsResponseParseException("Some error at the end", new byte[1000], 995, 10); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + + Assert.False(result.HasError); + Assert.True(calledUdp); + Assert.True(calledTcp); + + Assert.Equal(4, calledIps.Count); + Assert.Equal(new[] { ip1, ip1, ip2, ip3 }, calledIps); } - public override bool IsTransientException(T exception) + [Fact] + public async Task DnsResponseParseException_ShouldTryTcp_LargerResponse_Async() { - return false; + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = true, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + + // This should retry the full request via TCP. + throw new DnsResponseParseException("Some error at the end", new byte[1000], 995, 10); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + + Assert.False(result.HasError); + Assert.True(calledUdp); + Assert.True(calledTcp); + + // It should fallback to TCP right away and call the first server twice, + // and then try all other servers because we don't return any answers because ContinueOnEmptyResponse = true, + Assert.Equal(4, calledIps.Count); + Assert.Equal(new[] { ip1, ip1, ip2, ip3 }, calledIps); + } + + [Fact] + public void DnsResponseParseException_FailWithoutTcpFallback() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false // Disabled => will throw on fallback. + }; + + var calledIps = new List(); + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + + var handle = new DnsUdpMessageHandler(true); + return handle.GetResponseMessage(new ArraySegment(s_issue52data)); + }); + + var lookup = new LookupClient(options, udpMessageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + Assert.Contains("truncated and UseTcpFallback is disabled", result.Message); + } + + [Fact] + public async Task DnsResponseParseException_FailWithoutTcpFallback_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false + }; + + var calledIps = new List(); + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + + var handle = new DnsUdpMessageHandler(true); + return handle.GetResponseMessage(new ArraySegment(s_issue52data)); + }); + + var lookup = new LookupClient(options, udpMessageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + Assert.Contains("truncated and UseTcpFallback is disabled", result.Message); + } + + [Fact] + public void DnsResponseParseException_ShouldTryNextServer_ThenThrow() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true // although this is set, the error shoudld be thrown. + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + + // If the result is a parser error which isn't network related (as to our best guess), + // The error handling should retry the next server before it falls back to TCP. + // Any result with a small byte result (<= 512) is treated as network issue. Let's pass in a bigger array... + throw new DnsResponseParseException("Some error in the middle", new byte[1000], 20, 10); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Contains("1000 bytes available", result.Message); + + Assert.True(calledUdp); + Assert.False(calledTcp); + + // 3 servers are configured, it should not fallback to TCP after trying each one of them once. + Assert.Equal(3, calledIps.Count); + } + + [Fact] + public async Task DnsResponseParseException_ShouldTryNextServer_ThenThrow_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true // although this is set, the error shoudld be thrown. + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + + // If the result is a parser error which isn't network related (as to our best guess), + // The error handling should retry the next server before it falls back to TCP. + // Any result with a small byte result (<= 512) is treated as network issue. Let's pass in a bigger array... + throw new DnsResponseParseException("Some error in the middle", new byte[1000], 20, 10); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Contains("1000 bytes available", result.Message); + + Assert.True(calledUdp); + Assert.False(calledTcp); + + // 3 servers are configured, it should not fallback to TCP after trying each one of them once. + Assert.Equal(3, calledIps.Count); + } + + /* Normal truncated response (TC flag) */ + + [Fact] + public void TruncatedResponse_ShouldTryTcp() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = false, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderFlag.ResultTruncated, 0, 0, 0, 0), 0); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + + Assert.False(result.HasError); + + Assert.True(calledUdp); + Assert.True(calledTcp); + + // The TCP handler doesn't return an asnwer, but ContinueOnEmptyResponse is false, => should only call it once. + Assert.Equal(2, calledIps.Count); + Assert.Equal(new[] { ip1, ip1 }, calledIps); + } + + [Fact] + public async Task TruncatedResponse_ShouldTryTcp_Async() + { + var ip1 = IPAddress.Parse("127.0.10.1"); + var ip2 = IPAddress.Parse("127.0.10.2"); + var ip3 = IPAddress.Parse("127.0.10.3"); + var options = new LookupClientOptions( + new NameServer(ip1), + new NameServer(ip2), + new NameServer(ip3)) + { + ContinueOnEmptyResponse = false, + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + bool calledUdp = false, calledTcp = false; + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + calledUdp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderFlag.ResultTruncated, 0, 0, 0, 0), 0); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + calledIps.Add(ip.Address); + calledTcp = true; + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderResponseCode.NoError, 0, 0, 0, 0), 0); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = await lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN)); + + Assert.False(result.HasError); + + Assert.True(calledUdp); + Assert.True(calledTcp); + + Assert.Equal(2, calledIps.Count); + Assert.Equal(new[] { ip1, ip1 }, calledIps); + } + + [Fact] + public void TruncatedResponse_ShouldTryTcp_AndFail() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false // Disabling this will produce an error when we have to fall back to TCP => expected + }; + + var calledIps = new List(); + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderFlag.ResultTruncated, 0, 0, 0, 0), 0); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + throw new NotImplementedException("This shouldn't get called"); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + // This should fail right away because there is no need to ask other servers for the same truncated response + Assert.Single(calledIps); + Assert.Contains("Response was truncated and UseTcpFallback is disabled", result.Message); + } + + [Fact] + public async Task TruncatedResponse_ShouldTryTcp_AndFail_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = false // Disabling this will produce an error when we have to fall back to TCP => expected + }; + + var calledIps = new List(); + var udpMessageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + return new DnsResponseMessage(new DnsResponseHeader(req.Header.Id, (int)DnsHeaderFlag.ResultTruncated, 0, 0, 0, 0), 0); + }); + + var tcpMessageHandler = new TestMessageHandler(DnsMessageHandleType.TCP, (ip, req) => + { + throw new NotImplementedException("This shouldn't get called"); + }); + + var lookup = new LookupClient(options, udpMessageHandler, tcpMessageHandler); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + // This should fail right away because there is no need to ask other servers for the same truncated response + Assert.Single(calledIps); + Assert.Contains("Response was truncated and UseTcpFallback is disabled", result.Message); + } + + /* transient timeout errors */ + + [Fact] + public void TransientTimeoutExceptions_ShouldRetry() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + throw new TimeoutException(); + } + + throw new OperationCanceledException(); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.NotNull(result.InnerException); + Assert.IsType(result.InnerException); + Assert.Equal(12, calledIps.Count); + } + + [Fact] + public async Task TransientTimeoutExceptions_ShouldRetry_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + throw new TimeoutException(); + } + + throw new OperationCanceledException(); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.NotNull(result.InnerException); + Assert.IsType(result.InnerException); + + // 3 servers x (1 + 3 re-tries) + Assert.Equal(12, calledIps.Count); + } + + /* transient SocketExceptions + + case SocketError.TimedOut: + case SocketError.ConnectionAborted: + case SocketError.ConnectionReset: + case SocketError.OperationAborted: + case SocketError.TryAgain: + * */ + + [Fact] + public void TransientSocketExceptions_ShouldRetry() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3")), + new NameServer(IPAddress.Parse("127.0.10.4")), + new NameServer(IPAddress.Parse("127.0.10.5"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + throw new SocketException((int)SocketError.TimedOut); + } + if (count == 2) + { + throw new SocketException((int)SocketError.ConnectionAborted); + } + if (count == 3) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + if (count == 4) + { + throw new SocketException((int)SocketError.OperationAborted); + } + + throw new SocketException((int)SocketError.TryAgain); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Equal(SocketError.TryAgain, ((SocketException)result.InnerException).SocketErrorCode); + + Assert.Equal(20, calledIps.Count); + } + + [Fact] + public async Task TransientSocketExceptions_ShouldRetry_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3")), + new NameServer(IPAddress.Parse("127.0.10.4")), + new NameServer(IPAddress.Parse("127.0.10.5"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + throw new SocketException((int)SocketError.TimedOut); + } + if (count == 2) + { + throw new SocketException((int)SocketError.ConnectionAborted); + } + if (count == 3) + { + throw new SocketException((int)SocketError.ConnectionReset); + } + if (count == 4) + { + throw new SocketException((int)SocketError.OperationAborted); + } + + throw new SocketException((int)SocketError.TryAgain); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Equal(SocketError.TryAgain, ((SocketException)result.InnerException).SocketErrorCode); + + // 5 servers x (1 + 3 re-tries) + Assert.Equal(20, calledIps.Count); + } + + /* Other exceptions like SocketExceptions which we should re-try on the next server... */ + + [Fact] + public void OtherExceptions_ShouldTryNextServer() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3")), + new NameServer(IPAddress.Parse("127.0.10.4"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + // Trying some non-transient socket exceptions to make sure those do not get retried... + throw new SocketException((int)SocketError.AddressFamilyNotSupported); + } + if (count == 2) + { + throw new SocketException((int)SocketError.ConnectionRefused); + } + if (count == 3) + { + throw new FormatException("Some random exception we didn't expect."); + } + + throw new SocketException((int)SocketError.SocketError); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Equal(SocketError.SocketError, ((SocketException)result.InnerException).SocketErrorCode); + + Assert.Equal(4, calledIps.Count); + } + + [Fact] + public async Task OtherExceptions_ShouldTryNextServer_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3")), + new NameServer(IPAddress.Parse("127.0.10.4"))) + { + EnableAuditTrail = true, + ContinueOnDnsError = false, + ThrowDnsErrors = false, + UseCache = true, + Retries = 3, + UseRandomNameServer = false, + UseTcpFallback = true + }; + + var calledIps = new List(); + var count = 0; + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + count++; + if (count == 1) + { + // Trying some non-transient socket exceptions to make sure those do not get retried... + throw new SocketException((int)SocketError.AddressFamilyNotSupported); + } + if (count == 2) + { + throw new SocketException((int)SocketError.ConnectionRefused); + } + if (count == 3) + { + throw new FormatException("Some random exception we didn't expect."); + } + + throw new SocketException((int)SocketError.SocketError); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Equal(SocketError.SocketError, ((SocketException)result.InnerException).SocketErrorCode); + + Assert.Equal(4, calledIps.Count); + } + + /* Not handled exceptions. Should be thrown */ + + [Fact] + public void ArgumentExceptions_ShouldNotBeHandled() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))); + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + throw new ArgumentNullException("Some random argument exception"); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + } + + [Fact] + public async Task ArgumentExceptions_ShouldNotBeHandled_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))); + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + throw new ArgumentNullException("Some random argument exception"); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + } + + [Fact] + public void InvalidOperationExceptions_ShouldNotBeHandled() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))); + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + throw new InvalidOperationException("Some random argument exception"); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = Assert.ThrowsAny(() => lookup.Query(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + } + + [Fact] + public async Task InvalidOperationExceptions_ShouldNotBeHandled_Async() + { + var options = new LookupClientOptions( + new NameServer(IPAddress.Parse("127.0.10.1")), + new NameServer(IPAddress.Parse("127.0.10.2")), + new NameServer(IPAddress.Parse("127.0.10.3"))); + + var calledIps = new List(); + var messageHandler = new TestMessageHandler(DnsMessageHandleType.UDP, (ip, req) => + { + calledIps.Add(ip.Address); + throw new InvalidOperationException("Some random argument exception"); + }); + + var lookup = new LookupClient(options, messageHandler, TestMessageHandler.Tcp); + var result = await Assert.ThrowsAnyAsync(() => lookup.QueryAsync(new DnsQuestion("test.com", QueryType.A, QueryClass.IN))); + + Assert.Single(calledIps); + } + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal class TestMessageHandler : DnsMessageHandler + { + public static readonly TestMessageHandler Udp = new TestMessageHandler(DnsMessageHandleType.UDP, (a, b) => null); + public static readonly TestMessageHandler Tcp = new TestMessageHandler(DnsMessageHandleType.TCP, (a, b) => null); + + private readonly Func _onQuery; + + public override DnsMessageHandleType Type { get; } + + public TestMessageHandler(DnsMessageHandleType type, Func onQuery) + { + Type = type; + _onQuery = onQuery ?? throw new ArgumentNullException(nameof(onQuery)); } public override DnsResponseMessage Query(IPEndPoint endpoint, DnsRequestMessage request, TimeSpan timeout) diff --git a/test/DnsClient.Tests/LookupConfigurationTest.cs b/test/DnsClient.Tests/LookupConfigurationTest.cs index c1cccb38..d3d66c17 100644 --- a/test/DnsClient.Tests/LookupConfigurationTest.cs +++ b/test/DnsClient.Tests/LookupConfigurationTest.cs @@ -8,8 +8,254 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class LookupConfigurationTest { + static LookupConfigurationTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + + [Fact] + public void DnsQueryOptions_Implicit_Null() + { + Assert.Null((DnsQuerySettings)(DnsQueryOptions)null); + } + + [Fact] + public void DnsQueryAndServerOptions_Implicit_Null() + { + Assert.Null((DnsQueryAndServerSettings)(DnsQueryAndServerOptions)null); + } + + [Fact] + public void LookupClientOptions_Implicit_Null() + { + Assert.Null((LookupClientSettings)(LookupClientOptions)null); + } + + [Fact] + public void DnsQuerySettings_Equals_Ref() + { + var opt = (DnsQuerySettings)new DnsQueryOptions(); + + // typed overload + Assert.True(opt.Equals(opt)); + + // object overload + Assert.True(opt.Equals((object)opt)); + } + + [Fact] + public void DnsQuerySettings_Equals_Null() + { + var opt = (DnsQuerySettings)new DnsQueryOptions(); + + // typed overload + Assert.False(opt.Equals((DnsQuerySettings)null)); + + // object overload + Assert.False(opt.Equals((object)null)); + } + + [Fact] + public void DnsQuerySettings_Equals_Other() + { + var opt = (DnsQuerySettings)new DnsQueryOptions(); + var opt2 = (DnsQuerySettings)new DnsQueryOptions(); + + // typed overload + Assert.True(opt.Equals(opt2)); + + // object overload + Assert.True(opt.Equals((object)opt2)); + } + + [Fact] + public void DnsQueryAndServerSettings_Equals_Ref() + { + var opt = (DnsQueryAndServerSettings)new DnsQueryAndServerOptions(); + + // typed overload + Assert.True(opt.Equals(opt)); + + // object overload + Assert.True(opt.Equals((object)opt)); + } + + [Fact] + public void DnsQueryAndServerSettings_Equals_Null() + { + var opt = (DnsQueryAndServerSettings)new DnsQueryAndServerOptions(); + + // typed overload + Assert.False(opt.Equals((DnsQueryAndServerSettings)null)); + + // object overload + Assert.False(opt.Equals((object)null)); + } + + [Fact] + public void DnsQueryAndServerSettings_Equals_Other() + { + var opt = (DnsQueryAndServerSettings)new DnsQueryAndServerOptions(); + var opt2 = (DnsQueryAndServerSettings)new DnsQueryAndServerOptions(); + + // typed overload + Assert.True(opt.Equals(opt2)); + + // object overload + Assert.True(opt.Equals((object)opt2)); + } + + [Fact] + public void LookupClientSettings_Equals_Ref() + { + var opt = (LookupClientSettings)new LookupClientOptions(resolveNameServers: false); + + // typed overload + Assert.True(opt.Equals(opt)); + + // object overload + Assert.True(opt.Equals((object)opt)); + } + + [Fact] + public void LookupClientSettings_Equals_Null() + { + var opt = (LookupClientSettings)new LookupClientOptions(resolveNameServers: false); + + // typed overload + Assert.False(opt.Equals((LookupClientSettings)null)); + + // object overload + Assert.False(opt.Equals((object)null)); + } + + [Fact] + public void LookupClientSettings_Equals_Other() + { + var opt = (LookupClientSettings)new LookupClientOptions(resolveNameServers: false); + var opt2 = (LookupClientSettings)new LookupClientOptions(resolveNameServers: false); + + // typed overload + Assert.True(opt.Equals(opt2)); + + // object overload + Assert.True(opt.Equals((object)opt2)); + } + + [Fact] + public void DnsQueryAndServerSettings_Null1() + { + Assert.ThrowsAny(() => new DnsQueryAndServerSettings(null)); + } + + [Fact] + public void DnsQueryAndServerSettings_Null2() + { + Assert.ThrowsAny(() => new DnsQueryAndServerSettings(new DnsQueryAndServerOptions(), null)); + } + + [Fact] + public void DnsQueryAndServerSettings_ChangeServers() + { + var settings = new DnsQueryAndServerSettings(new DnsQueryAndServerOptions(NameServer.Cloudflare2), new NameServer[] { NameServer.Cloudflare }); + + Assert.Single(settings.NameServers); + Assert.Contains(NameServer.Cloudflare, settings.NameServers); + } + + [Fact] + public void DnsQueryAndServerSettings_AutoResolve() + { + var settings = new DnsQueryAndServerSettings(new DnsQueryAndServerOptions(resolveNameServers: true)); + + Assert.NotEmpty(settings.NameServers); + } + + [Fact] + public void DnsQueryAndServerSettings_NullServers1() + { + var settings = new DnsQueryAndServerSettings(new DnsQueryAndServerOptions((IPAddress[])null)); + + Assert.Empty(settings.NameServers); + } + + [Fact] + public void DnsQueryAndServerSettings_NullServers2() + { + var settings = new DnsQueryAndServerSettings(new DnsQueryAndServerOptions((IPEndPoint[])null)); + + Assert.Empty(settings.NameServers); + } + + [Fact] + public void DnsQueryAndServerSettings_NullServers3() + { + var settings = new DnsQueryAndServerSettings(new DnsQueryAndServerOptions((NameServer[])null)); + + Assert.Empty(settings.NameServers); + } + + public static IEnumerable AllNonDefaultConfigurations + { + get + { + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { ContinueOnDnsError = false } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { EnableAuditTrail = true } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { ExtendedDnsBufferSize = 2222 } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { MaximumCacheTimeout = TimeSpan.FromSeconds(5) } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { MinimumCacheTimeout = TimeSpan.FromSeconds(5) } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { NameServers = new List { NameServer.Cloudflare } } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { Recursion = false } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { RequestDnsSecRecords = true } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { Retries = 3 } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { ThrowDnsErrors = true } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { Timeout = TimeSpan.FromSeconds(1) } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { UseCache = false } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { UseRandomNameServer = false } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { UseTcpFallback = false } }; + yield return new object[] { new LookupClientOptions(resolveNameServers: false) { UseTcpOnly = true } }; + yield return new object[] { new LookupClientOptions(NameServer.CloudflareIPv6) }; + yield return new object[] { new LookupClientOptions(IPAddress.Loopback) }; + yield return new object[] { new LookupClientOptions(new IPEndPoint(IPAddress.Loopback, 1111)) }; + yield return new object[] { new LookupClientOptions(new[] { NameServer.CloudflareIPv6 }) }; + } + } + + [Theory] + [MemberData(nameof(AllNonDefaultConfigurations))] + public void LookupClientSettings_NotEqual(LookupClientOptions otherOptions) + { + var opt = (LookupClientSettings)new LookupClientOptions(resolveNameServers: false); + var opt2 = (LookupClientSettings)otherOptions; + + Assert.NotStrictEqual(opt, opt2); + + // typed overload + Assert.False(opt.Equals(opt2)); + + // object overload + Assert.False(opt.Equals((object)opt2)); + } + + [Theory] + [MemberData(nameof(AllNonDefaultConfigurations))] + public void LookupClientSettings_Equal(LookupClientOptions options) + { + var opt = (LookupClientSettings)options; + var opt2 = (LookupClientSettings)options; + + Assert.StrictEqual(opt, opt2); + + // typed overload + Assert.True(opt.Equals(opt2)); + + // object overload + Assert.True(opt.Equals((object)opt2)); + } + [Fact] public void LookupClientOptions_Defaults() { @@ -22,12 +268,15 @@ public void LookupClientOptions_Defaults() Assert.Null(options.MaximumCacheTimeout); Assert.True(options.Recursion); Assert.False(options.ThrowDnsErrors); - Assert.Equal(5, options.Retries); + Assert.Equal(2, options.Retries); Assert.Equal(options.Timeout, TimeSpan.FromSeconds(5)); Assert.True(options.UseTcpFallback); Assert.False(options.UseTcpOnly); Assert.True(options.ContinueOnDnsError); + Assert.True(options.ContinueOnEmptyResponse); Assert.True(options.UseRandomNameServer); + Assert.Equal(DnsQueryOptions.MaximumBufferSize, options.ExtendedDnsBufferSize); + Assert.False(options.RequestDnsSecRecords); } [Fact] @@ -42,12 +291,15 @@ public void LookupClientOptions_DefaultsNoResolve() Assert.Null(options.MaximumCacheTimeout); Assert.True(options.Recursion); Assert.False(options.ThrowDnsErrors); - Assert.Equal(5, options.Retries); + Assert.Equal(2, options.Retries); Assert.Equal(options.Timeout, TimeSpan.FromSeconds(5)); Assert.True(options.UseTcpFallback); Assert.False(options.UseTcpOnly); Assert.True(options.ContinueOnDnsError); + Assert.True(options.ContinueOnEmptyResponse); Assert.True(options.UseRandomNameServer); + Assert.Equal(DnsQueryOptions.MaximumBufferSize, options.ExtendedDnsBufferSize); + Assert.False(options.RequestDnsSecRecords); } [Fact] @@ -58,6 +310,7 @@ public void LookupClient_SettingsValid() var options = new LookupClientOptions(resolveNameServers: true) { ContinueOnDnsError = !defaultOptions.ContinueOnDnsError, + ContinueOnEmptyResponse = !defaultOptions.ContinueOnEmptyResponse, EnableAuditTrail = !defaultOptions.EnableAuditTrail, MinimumCacheTimeout = TimeSpan.FromMinutes(1), MaximumCacheTimeout = TimeSpan.FromMinutes(42), @@ -68,13 +321,16 @@ public void LookupClient_SettingsValid() UseCache = !defaultOptions.UseCache, UseRandomNameServer = !defaultOptions.UseRandomNameServer, UseTcpFallback = !defaultOptions.UseTcpFallback, - UseTcpOnly = !defaultOptions.UseTcpOnly + UseTcpOnly = !defaultOptions.UseTcpOnly, + ExtendedDnsBufferSize = 1234, + RequestDnsSecRecords = true }; var client = new LookupClient(options); Assert.Equal(defaultOptions.NameServers, client.NameServers); Assert.Equal(!defaultOptions.ContinueOnDnsError, client.Settings.ContinueOnDnsError); + Assert.Equal(!defaultOptions.ContinueOnEmptyResponse, client.Settings.ContinueOnEmptyResponse); Assert.Equal(!defaultOptions.EnableAuditTrail, client.Settings.EnableAuditTrail); Assert.Equal(TimeSpan.FromMinutes(1), client.Settings.MinimumCacheTimeout); Assert.Equal(TimeSpan.FromMinutes(42), client.Settings.MaximumCacheTimeout); @@ -86,12 +342,78 @@ public void LookupClient_SettingsValid() Assert.Equal(!defaultOptions.UseRandomNameServer, client.Settings.UseRandomNameServer); Assert.Equal(!defaultOptions.UseTcpFallback, client.Settings.UseTcpFallback); Assert.Equal(!defaultOptions.UseTcpOnly, client.Settings.UseTcpOnly); + Assert.Equal(1234, client.Settings.ExtendedDnsBufferSize); + Assert.Equal(!defaultOptions.RequestDnsSecRecords, client.Settings.RequestDnsSecRecords); Assert.Equal(options, client.Settings); } [Fact] - public void Lookup_Query_InvalidTimeout() + public void QueryOptions_EdnsDisabled_WithSmallBuffer() + { + var options = (DnsQuerySettings)new DnsQueryOptions() + { + ExtendedDnsBufferSize = DnsQueryOptions.MinimumBufferSize, + RequestDnsSecRecords = false + }; + + Assert.False(options.UseExtendedDns); + } + + [Fact] + public void QueryOptions_Edns_SmallerBufferFallback() + { + var options = (DnsQuerySettings)new DnsQueryOptions() + { + // Anything less then mimimum falls back to minimum. + ExtendedDnsBufferSize = DnsQueryOptions.MinimumBufferSize - 1, + RequestDnsSecRecords = false + }; + + Assert.False(options.UseExtendedDns); + Assert.Equal(DnsQueryOptions.MinimumBufferSize, options.ExtendedDnsBufferSize); + } + + [Fact] + public void QueryOptions_Edns_LargerBufferFallback() + { + var options = (DnsQuerySettings)new DnsQueryOptions() + { + // Anything more then max falls back to max. + ExtendedDnsBufferSize = DnsQueryOptions.MaximumBufferSize + 1, + RequestDnsSecRecords = false + }; + + Assert.True(options.UseExtendedDns); + Assert.Equal(DnsQueryOptions.MaximumBufferSize, options.ExtendedDnsBufferSize); + } + + [Fact] + public void QueryOptions_EdnsEnabled_ByLargeBuffer() + { + var options = (DnsQuerySettings)new DnsQueryOptions() + { + ExtendedDnsBufferSize = DnsQueryOptions.MinimumBufferSize + 1, + RequestDnsSecRecords = false + }; + + Assert.True(options.UseExtendedDns); + } + + [Fact] + public void QueryOptions_EdnsEnabled_ByRequestDnsSec() + { + var options = (DnsQuerySettings)new DnsQueryOptions() + { + ExtendedDnsBufferSize = DnsQueryOptions.MinimumBufferSize, + RequestDnsSecRecords = true + }; + + Assert.True(options.UseExtendedDns); + } + + [Fact] + public void LookupClientOptions_InvalidTimeout() { var options = new LookupClientOptions(); @@ -100,6 +422,135 @@ public void Lookup_Query_InvalidTimeout() Assert.ThrowsAny(act); } + [Fact] + public void LookupClientOptions_InvalidTimeout2() + { + var options = new LookupClientOptions(); + + Action act = () => options.Timeout = TimeSpan.FromMilliseconds(-23); + + Assert.ThrowsAny(act); + } + + [Fact] + public void LookupClientOptions_InvalidTimeout3() + { + var options = new LookupClientOptions(); + + Action act = () => options.Timeout = TimeSpan.FromDays(25); + + Assert.ThrowsAny(act); + } + + [Fact] + public void LookupClientOptions_MinimumCacheTimeout_ZeroIgnored() + { + var options = new LookupClientOptions(); + + options.MinimumCacheTimeout = TimeSpan.Zero; + + Assert.Null(options.MinimumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_MinimumCacheTimeout_AcceptsInfinite() + { + var options = new LookupClientOptions() + { + MinimumCacheTimeout = TimeSpan.Zero + }; + + options.MinimumCacheTimeout = Timeout.InfiniteTimeSpan; + + Assert.Equal(Timeout.InfiniteTimeSpan, options.MinimumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_MinimumCacheTimeout_AcceptsNull() + { + var options = new LookupClientOptions() + { + MinimumCacheTimeout = Timeout.InfiniteTimeSpan + }; + + options.MinimumCacheTimeout = null; + + Assert.Null(options.MinimumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_InvalidMinimumCacheTimeout1() + { + var options = new LookupClientOptions(); + + Action act = () => options.MinimumCacheTimeout = TimeSpan.FromMilliseconds(-23); + + Assert.ThrowsAny(act); + } + + [Fact] + public void LookupClientOptions_InvalidMinimumCacheTimeout2() + { + var options = new LookupClientOptions(); + + Action act = () => options.MinimumCacheTimeout = TimeSpan.FromDays(25); + + Assert.ThrowsAny(act); + } + + [Fact] + public void LookupClientOptions_MaximumCacheTimeout_ZeroIgnored() + { + var options = new LookupClientOptions(); + + options.MaximumCacheTimeout = TimeSpan.Zero; + + Assert.Null(options.MaximumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_MaximumCacheTimeout_AcceptsNull() + { + var options = new LookupClientOptions() + { + MaximumCacheTimeout = Timeout.InfiniteTimeSpan + }; + + options.MaximumCacheTimeout = null; + + Assert.Null(options.MaximumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_MaximumCacheTimeout_AcceptsInfinite() + { + var options = new LookupClientOptions(); + + options.MaximumCacheTimeout = Timeout.InfiniteTimeSpan; + + Assert.Equal(Timeout.InfiniteTimeSpan, options.MaximumCacheTimeout); + } + + [Fact] + public void LookupClientOptions_InvalidMaximumCacheTimeout1() + { + var options = new LookupClientOptions(); + + Action act = () => options.MaximumCacheTimeout = TimeSpan.FromMilliseconds(-23); + + Assert.ThrowsAny(act); + } + + [Fact] + public void LookupClientOptions_InvalidMaximumCacheTimeout2() + { + var options = new LookupClientOptions(); + + Action act = () => options.MaximumCacheTimeout = TimeSpan.FromDays(25); + + Assert.ThrowsAny(act); + } + public static IEnumerable All { get @@ -163,8 +614,8 @@ public void ConfigMatrix_ServersQueriesExpectServers(TestMatrixItem test) [MemberData(nameof(AllWithServers))] public void ConfigMatrix_ServersCannotBeNull(TestMatrixItem test) { - var unresolvedOptions = new LookupClientOptions(resolveNameServers: true); - Assert.Throws("servers", () => test.InvokeNoDefaults(lookupClientOptions: unresolvedOptions, useOptions: null, useServers: null)); + var options = new LookupClientOptions(resolveNameServers: true); + Assert.Throws("servers", () => test.InvokeNoDefaults(lookupClientOptions: options, useOptions: null, useServers: null)); } [Theory] @@ -246,7 +697,9 @@ public void ConfigMatrix_VerifyOverrideWithServerFallback(TestMatrixItem test) UseCache = !defaultOptions.UseCache, UseRandomNameServer = !defaultOptions.UseRandomNameServer, UseTcpFallback = !defaultOptions.UseTcpFallback, - UseTcpOnly = !defaultOptions.UseTcpOnly + UseTcpOnly = !defaultOptions.UseTcpOnly, + RequestDnsSecRecords = true, + ExtendedDnsBufferSize = 3333 }; var result = test.Invoke(lookupClientOptions: defaultOptions, useOptions: queryOptions); @@ -413,8 +866,8 @@ public class TestClient { public TestClient(LookupClientOptions options) { - UdpHandler = new ConfigurationTrackingMessageHandler(false); - TcpHandler = new ConfigurationTrackingMessageHandler(true); + UdpHandler = new ConfigurationTrackingMessageHandler(); + TcpHandler = new ConfigurationTrackingMessageHandler(DnsMessageHandleType.TCP); Client = new LookupClient(options, UdpHandler, TcpHandler); } @@ -433,22 +886,18 @@ internal class ConfigurationTrackingMessageHandler : DnsMessageHandler 95, 207, 129, 128, 0, 1, 0, 11, 0, 0, 0, 1, 6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 0, 0, 255, 0, 1, 192, 12, 0, 1, 0, 1, 0, 0, 1, 8, 0, 4, 172, 217, 17, 238, 192, 12, 0, 28, 0, 1, 0, 0, 0, 71, 0, 16, 42, 0, 20, 80, 64, 22, 8, 13, 0, 0, 0, 0, 0, 0, 32, 14, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 17, 0, 50, 4, 97, 108, 116, 52, 5, 97, 115, 112, 109, 120, 1, 108, 192, 12, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 4, 0, 10, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 30, 4, 97, 108, 116, 50, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 20, 4, 97, 108, 116, 49, 192, 91, 192, 12, 0, 15, 0, 1, 0, 0, 2, 30, 0, 9, 0, 40, 4, 97, 108, 116, 51, 192, 91, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 51, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 50, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 52, 192, 12, 192, 12, 0, 2, 0, 1, 0, 4, 31, 116, 0, 6, 3, 110, 115, 49, 192, 12, 0, 0, 41, 16, 0, 0, 0, 0, 0, 0, 0 }; - public IReadOnlyList UsedSettings { get; } + public override DnsMessageHandleType Type { get; } - public bool IsTcp { get; } + public IReadOnlyList UsedSettings { get; } + public IPEndPoint LastServer { get; private set; } public DnsRequestMessage LastRequest { get; private set; } - public ConfigurationTrackingMessageHandler(bool isTcp) + public ConfigurationTrackingMessageHandler(DnsMessageHandleType type = DnsMessageHandleType.UDP) { - IsTcp = isTcp; - } - - public override bool IsTransientException(T exception) - { - return false; + Type = type; } public override DnsResponseMessage Query( @@ -458,7 +907,18 @@ public override DnsResponseMessage Query( { LastServer = server; LastRequest = request; - var response = GetResponseMessage(new ArraySegment(ZoneData, 0, ZoneData.Length)); + + var writer = new DnsDatagramWriter(new ArraySegment(ZoneData.ToArray())); + writer.Index = 0; + writer.WriteInt16NetworkOrder((short)request.Header.Id); + writer.Index = ZoneData.Length; + + var response = GetResponseMessage(writer.Data); + + if (response.Header.Id != request.Header.Id) + { + throw new Exception(); + } return response; } diff --git a/test/DnsClient.Tests/LookupTest.cs b/test/DnsClient.Tests/LookupTest.cs index a2158ce8..e2e2fa84 100644 --- a/test/DnsClient.Tests/LookupTest.cs +++ b/test/DnsClient.Tests/LookupTest.cs @@ -4,23 +4,45 @@ using System.Threading; using System.Threading.Tasks; using DnsClient.Protocol; +using DnsClient.Protocol.Options; using Xunit; namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class LookupTest { - private static readonly IPAddress DoesNotExist = IPAddress.Parse("192.0.21.43"); + private static readonly IPAddress s_doesNotExist = IPAddress.Parse("192.168.21.43"); + private static readonly TimeSpan s_timeout = TimeSpan.FromMilliseconds(1); + + static LookupTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } [Fact] public void Lookup_Query_QuestionCannotBeNull() { - IDnsQuery client = new LookupClient(); + IDnsQuery client = new LookupClient(NameServer.GooglePublicDns); Assert.Throws("question", () => client.Query(null)); Assert.ThrowsAsync("question", () => client.QueryAsync(null)); } + [Fact] + public void Lookup_Query_SettingsCannotBeNull() + { + IDnsQuery client = new LookupClient(NameServer.GooglePublicDns); + + Assert.Throws("queryOptions", () => client.Query("query", QueryType.A, null)); + Assert.ThrowsAsync("queryOptions", () => client.QueryAsync("query", QueryType.A, null)); + + Assert.Throws("queryOptions", () => client.QueryReverse(IPAddress.Loopback, null)); + Assert.ThrowsAsync("queryOptions", () => client.QueryReverseAsync(IPAddress.Loopback, null)); + } + +#if !NET461 + [Fact] public void NativeDnsServerResolution() { @@ -28,6 +50,8 @@ public void NativeDnsServerResolution() Assert.Null(ex); } +#endif + [Fact] public async Task Lookup_GetHostAddresses_Local() { @@ -57,57 +81,34 @@ public void Lookup_GetHostAddresses_Local_Sync() } [Fact] - public async Task Lookup_GetHostAddresses_LocalReverse_NoResult() + public void Lookup_DisabledEdns_NoAdditionals() { - // expecting no result as reverse lookup must be explicit - var client = new LookupClient( - new LookupClientOptions() - { - Timeout = TimeSpan.FromMilliseconds(500), - EnableAuditTrail = true - }); + var dns = new LookupClient(NameServer.GooglePublicDns); - var result = await client.QueryAsync("127.0.0.1", QueryType.A); + var result = dns.Query("google.com", QueryType.A, queryOptions: new DnsQueryAndServerOptions() + { + RequestDnsSecRecords = false, + ExtendedDnsBufferSize = 512 + }); - Assert.Equal(QueryClass.IN, result.Questions.First().QuestionClass); - Assert.Equal(QueryType.A, result.Questions.First().QuestionType); - Assert.True(result.Header.AnswerCount == 0); + Assert.Empty(result.Additionals); } [Fact] - public void Lookup_GetHostAddresses_LocalReverse_NoResult_Sync() + public void Lookup_EnabledEdns_DoFlag() { - // expecting no result as reverse lookup must be explicit - var client = new LookupClient( - new LookupClientOptions() - { - Timeout = TimeSpan.FromMilliseconds(500), - EnableAuditTrail = true - }); + var dns = new LookupClient(NameServer.GooglePublicDns); - var result = client.Query("127.0.0.1", QueryType.A); + var result = dns.Query("google.com", QueryType.A, queryOptions: new DnsQueryAndServerOptions() + { + RequestDnsSecRecords = true + }); - Assert.Equal(QueryClass.IN, result.Questions.First().QuestionClass); - Assert.Equal(QueryType.A, result.Questions.First().QuestionType); - Assert.True(result.Header.AnswerCount == 0); + Assert.True(result.Additionals.OfType().First().IsDnsSecOk); } #if ENABLE_REMOTE_DNS - [Fact(Skip = "Required bind")] - public void Lookup_LargeAXFR() - { - var client = new LookupClient( - new LookupClientOptions(IPAddress.Loopback) - { - UseTcpOnly = true - }); - - var result = client.Query("mcnet.com", QueryType.AXFR); - - Assert.True(result.Answers.Count > 0); - } - // see #10, wrong TCP result handling if the result contains like 1000 answers [Fact] public void Lookup_LargeResultWithTCP() @@ -242,20 +243,16 @@ public void Lookup_MultiServer_IPv4_and_IPv6_TCP() Assert.True(resultB.Answers.Count > 0); } -#endif - [Fact] - public void Lookup_ThrowDnsErrors() + public async Task Lookup_ThrowDnsErrors() { var client = new LookupClient( - new LookupClientOptions() + new LookupClientOptions(NameServer.GooglePublicDns) { ThrowDnsErrors = true }); - Action act = () => client.QueryAsync("lalacom", (QueryType)12345).GetAwaiter().GetResult(); - - var ex = Record.Exception(act) as DnsResponseException; + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lalacom", (QueryType)12345)); Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); } @@ -264,14 +261,12 @@ public void Lookup_ThrowDnsErrors() public void Lookup_ThrowDnsErrors_Sync() { var client = new LookupClient( - new LookupClientOptions() + new LookupClientOptions(NameServer.GooglePublicDns) { ThrowDnsErrors = true }); - Action act = () => client.Query("lalacom", (QueryType)12345); - - var ex = Record.Exception(act) as DnsResponseException; + var ex = Assert.ThrowsAny(() => client.Query("lalacom", (QueryType)12345)); Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); } @@ -282,217 +277,192 @@ public class QueryTimesOutTests public async Task Lookup_QueryTimesOut_Udp_Async() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(NameServer.GooglePublicDns) { - Timeout = TimeSpan.FromMilliseconds(200), + Timeout = s_timeout, Retries = 0, UseTcpFallback = false }); - var exe = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A)); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A)); - var ex = exe as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.ConnectionTimeout, ex.Code); - Assert.Contains("No connection", ex.Message); + Assert.Contains("timed out", ex.Message); } [Fact] public void Lookup_QueryTimesOut_Udp_Sync() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(NameServer.GooglePublicDns) { - Timeout = TimeSpan.FromMilliseconds(200), + Timeout = s_timeout, Retries = 0, UseTcpFallback = false }); - var exe = Record.Exception(() => client.Query("lala.com", QueryType.A)); + var ex = Assert.ThrowsAny(() => client.Query("lala.com", QueryType.A)); - var ex = exe as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.ConnectionTimeout, ex.Code); - Assert.Contains("No connection", ex.Message); + Assert.Contains("timed out", ex.Message); } [Fact] public async Task Lookup_QueryTimesOut_Tcp_Async() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(NameServer.GooglePublicDns) { - Timeout = TimeSpan.FromMilliseconds(200), + Timeout = s_timeout, Retries = 0, UseTcpOnly = true }); - var exe = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A)); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A)); - var ex = exe as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.ConnectionTimeout, ex.Code); - Assert.Contains("No connection", ex.Message); + Assert.Contains("timed out", ex.Message); } [Fact] public void Lookup_QueryTimesOut_Tcp_Sync() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(NameServer.GooglePublicDns) { - Timeout = TimeSpan.FromMilliseconds(200), + Timeout = s_timeout, Retries = 0, UseTcpOnly = true }); - var exe = Record.Exception(() => client.Query("lala.com", QueryType.A)); + var ex = Assert.ThrowsAny(() => client.Query("lala.com", QueryType.A)); - var ex = exe as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.ConnectionTimeout, ex.Code); - Assert.Contains("No connection", ex.Message); + Assert.Contains("timed out", ex.Message); } } - [Fact] - public void Lookup_QueryCanceled_Udp() - { - var client = new LookupClient( - new LookupClientOptions() - { - UseTcpFallback = false - }); - - var tokenSource = new CancellationTokenSource(); - var token = tokenSource.Token; - Action act = () => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token).GetAwaiter().GetResult(); - tokenSource.Cancel(); - - var ex = Record.Exception(act) as OperationCanceledException; - - Assert.NotNull(ex); - Assert.Equal(ex.CancellationToken, token); - } - - [Fact] - public void Lookup_QueryCanceled_Tcp() - { - var client = new LookupClient( - new LookupClientOptions() - { - UseTcpOnly = true - }); - - var tokenSource = new CancellationTokenSource(); - var token = tokenSource.Token; - Action act = () => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token).GetAwaiter().GetResult(); - tokenSource.Cancel(); - - var ex = Record.Exception(act) as OperationCanceledException; - - Assert.NotNull(ex); - Assert.Equal(ex.CancellationToken, token); - } - public class DelayCancelTest { [Fact] public async Task Lookup_QueryDelayCanceled_Udp() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(s_doesNotExist) { Timeout = TimeSpan.FromMilliseconds(1000), UseTcpFallback = false }); // should hit the cancelation timeout, not the 1sec timeout - var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + var tokenSource = new CancellationTokenSource(s_timeout); var token = tokenSource.Token; - var ex = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); - Assert.True(ex is OperationCanceledException); - Assert.Equal(token, ((OperationCanceledException)ex).CancellationToken); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); + Assert.NotNull(ex.InnerException); } [Fact] public async Task Lookup_QueryDelayCanceled_Tcp() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(s_doesNotExist) { Timeout = TimeSpan.FromMilliseconds(1000), UseTcpOnly = true }); // should hit the cancelation timeout, not the 1sec timeout - var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + var tokenSource = new CancellationTokenSource(s_timeout); var token = tokenSource.Token; - var ex = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); - Assert.True(ex is OperationCanceledException); - Assert.Equal(token, ((OperationCanceledException)ex).CancellationToken); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); + Assert.NotNull(ex.InnerException); } [Fact] public async Task Lookup_QueryDelayCanceledWithUnlimitedTimeout_Udp() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(s_doesNotExist) { Timeout = Timeout.InfiniteTimeSpan, UseTcpFallback = false }); // should hit the cancelation timeout, not the 1sec timeout - var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + var tokenSource = new CancellationTokenSource(s_timeout); var token = tokenSource.Token; - var ex = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); - Assert.True(ex is OperationCanceledException); - Assert.Equal(token, ((OperationCanceledException)ex).CancellationToken); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); + Assert.NotNull(ex.InnerException); } [Fact] public async Task Lookup_QueryDelayCanceledWithUnlimitedTimeout_Tcp() { var client = new LookupClient( - new LookupClientOptions(DoesNotExist) + new LookupClientOptions(s_doesNotExist) { Timeout = Timeout.InfiniteTimeSpan, UseTcpOnly = true }); // should hit the cancelation timeout, not the 1sec timeout - var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + var tokenSource = new CancellationTokenSource(s_timeout); var token = tokenSource.Token; - var ex = await Record.ExceptionAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); - Assert.True(ex is OperationCanceledException); - Assert.Equal(token, ((OperationCanceledException)ex).CancellationToken); + var ex = await Assert.ThrowsAnyAsync(() => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token)); + Assert.NotNull(ex.InnerException); } } - private async Task GetDnsEntryAsync() + [Fact] + public async Task Lookup_QueryCanceled_Udp() + { + var client = new LookupClient( + new LookupClientOptions(NameServer.GooglePublicDns) + { + UseTcpFallback = false + }); + + var tokenSource = new CancellationTokenSource(); + var token = tokenSource.Token; + Func act = () => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token); + tokenSource.Cancel(); + + var ex = await Assert.ThrowsAnyAsync(act); + + Assert.NotNull(ex.InnerException); + } + + [Fact] + public async Task Lookup_QueryCanceled_Tcp() { - // retries the normal host name (without domain) - var hostname = Dns.GetHostName(); - var hostIp = await Dns.GetHostAddressesAsync(hostname); + var client = new LookupClient( + new LookupClientOptions(NameServer.GooglePublicDns) + { + UseTcpOnly = true + }); + + var tokenSource = new CancellationTokenSource(); + var token = tokenSource.Token; + Func act = () => client.QueryAsync("lala.com", QueryType.A, cancellationToken: token); + tokenSource.Cancel(); - // find the actual IP of the adapter used for inter networking - var ip = hostIp.Where(p => p.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).First(); - return ip; + var ex = await Assert.ThrowsAnyAsync(act); + + Assert.NotNull(ex.InnerException); } [Fact] public async Task GetHostName() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); string hostName = await client.GetHostNameAsync(IPAddress.Parse("8.8.8.8")); Assert.Equal("dns.google", hostName); @@ -516,10 +486,12 @@ public void Lookup_ReverseSync() Assert.Equal("localhost.", result.Answers.PtrRecords().First().PtrDomainName.Value); } +#endif + [Fact] public async Task Lookup_Query_AAAA() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.AAAA); Assert.NotEmpty(result.Answers.AaaaRecords()); @@ -529,7 +501,7 @@ public async Task Lookup_Query_AAAA() [Fact] public void Lookup_Query_AAAA_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.AAAA); Assert.NotEmpty(result.Answers.AaaaRecords()); @@ -539,29 +511,27 @@ public void Lookup_Query_AAAA_Sync() [Fact] public async Task Lookup_Query_Any() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.ANY); Assert.NotEmpty(result.Answers); Assert.NotEmpty(result.Answers.ARecords()); - Assert.NotEmpty(result.Answers.NsRecords()); } [Fact] public void Lookup_Query_Any_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.ANY); Assert.NotEmpty(result.Answers); Assert.NotEmpty(result.Answers.ARecords()); - Assert.NotEmpty(result.Answers.NsRecords()); } [Fact] public async Task Lookup_Query_Mx() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.MX); Assert.NotEmpty(result.Answers.MxRecords()); @@ -572,7 +542,7 @@ public async Task Lookup_Query_Mx() [Fact] public void Lookup_Query_Mx_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.MX); Assert.NotEmpty(result.Answers.MxRecords()); @@ -583,7 +553,7 @@ public void Lookup_Query_Mx_Sync() [Fact] public async Task Lookup_Query_NS() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.NS); Assert.NotEmpty(result.Answers.NsRecords()); @@ -593,7 +563,7 @@ public async Task Lookup_Query_NS() [Fact] public void Lookup_Query_NS_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.NS); Assert.NotEmpty(result.Answers.NsRecords()); @@ -603,7 +573,7 @@ public void Lookup_Query_NS_Sync() [Fact] public async Task Lookup_Query_TXT() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.TXT); Assert.NotEmpty(result.Answers.TxtRecords()); @@ -614,7 +584,7 @@ public async Task Lookup_Query_TXT() [Fact] public void Lookup_Query_TXT_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.TXT); Assert.NotEmpty(result.Answers.TxtRecords()); @@ -625,7 +595,7 @@ public void Lookup_Query_TXT_Sync() [Fact] public async Task Lookup_Query_SOA() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("google.com", QueryType.SOA); Assert.NotEmpty(result.Answers.SoaRecords()); @@ -636,7 +606,7 @@ public async Task Lookup_Query_SOA() [Fact] public void Lookup_Query_SOA_Sync() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("google.com", QueryType.SOA); Assert.NotEmpty(result.Answers.SoaRecords()); @@ -647,7 +617,7 @@ public void Lookup_Query_SOA_Sync() [Fact] public async Task Lookup_Query_Puny() { - var client = new LookupClient(IPAddress.Parse("8.8.8.8")); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.QueryAsync("müsli.de", QueryType.A); Assert.NotEmpty(result.Answers); @@ -657,7 +627,7 @@ public async Task Lookup_Query_Puny() [Fact] public void Lookup_Query_Puny_Sync() { - var client = new LookupClient(IPAddress.Parse("8.8.8.8")); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("müsli.de", QueryType.A); Assert.NotEmpty(result.Answers); @@ -667,42 +637,33 @@ public void Lookup_Query_Puny_Sync() [Fact] public void Lookup_Query_Puny2() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.Query("müsli.com", QueryType.ANY); Assert.NotEmpty(result.Answers); Assert.NotEmpty(result.Answers.ARecords()); } - //// relaxing puni code rules - ////[Fact] - ////public void Lookup_Query_InvalidPuny() - ////{ - //// var client = new LookupClient(IPAddress.Parse("8.8.8.8")); - - //// Func act = () => client.QueryAsync("müsliiscool!.de", QueryType.A).Result; - - //// Assert.ThrowsAny(act); - ////} - [Fact] public void Ip_Arpa_v4_Valid() { - var ip = IPAddress.Parse("127.0.0.1"); - var client = new LookupClient(); + var ip = IPAddress.Parse("8.8.4.4"); + var client = new LookupClient(NameServer.GooglePublicDns); var result = DnsString.Parse(ip.GetArpaName()); var queryResult = client.QueryReverse(ip); - Assert.Equal("1.0.0.127.in-addr.arpa.", result); - Assert.Equal("localhost.", queryResult.Answers.PtrRecords().First().PtrDomainName); + Assert.Equal("4.4.8.8.in-addr.arpa.", result); + Assert.Contains("dns.google", queryResult.Answers.PtrRecords().First().PtrDomainName); } +#if ENABLE_REMOTE_DNS + [Fact] public void Ip_Arpa_v6_Valid() { var ip = NameServer.GooglePublicDns2IPv6.Address; - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDnsIPv6); var result = DnsString.Parse(ip.GetArpaName()); var queryResult = client.QueryReverse(ip); @@ -711,22 +672,22 @@ public void Ip_Arpa_v6_Valid() Assert.Contains("dns.google", queryResult.Answers.PtrRecords().First().PtrDomainName); } +#endif + [Fact] public async Task GetHostEntry_ExampleSub() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var hostEntry = await client.GetHostEntryAsync("mail.google.com"); Assert.EndsWith("google.com", hostEntry.Aliases.First(), StringComparison.OrdinalIgnoreCase); Assert.Equal("mail.google.com", hostEntry.HostName); Assert.True(hostEntry.AddressList.Length > 0); } -#if ENABLE_REMOTE_DNS - [Fact] public void GetHostEntry_ByName_ManyIps_NoAlias() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.GetHostEntry("google.com"); @@ -736,9 +697,12 @@ public void GetHostEntry_ByName_ManyIps_NoAlias() } [Fact] - public void GetHostEntry_ByName_OneIp_ManyAliases() + public void GetHostEntry_ByName_ManyAliases() { - var client = new LookupClient(); + var client = new LookupClient(new LookupClientOptions(NameServer.GooglePublicDns) + { + ThrowDnsErrors = true + }); var result = client.GetHostEntry("dnsclient.michaco.net"); @@ -747,36 +711,20 @@ public void GetHostEntry_ByName_OneIp_ManyAliases() Assert.Equal("dnsclient.michaco.net", result.HostName); } - [Fact] - public void GetHostEntry_ByName_OneIp_NoAlias() - { - var client = new LookupClient(); - - var result = client.GetHostEntry("localhost"); - - Assert.True(result.AddressList.Length == 1); - Assert.True(result.Aliases.Length == 0); - Assert.Equal("localhost", result.HostName); - } - -#endif - [Fact] public void GetHostEntry_ByName_EmptyString() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); Action act = () => client.GetHostEntry(""); - var ex = Record.Exception(act); - Assert.NotNull(ex); - Assert.Contains("hostNameOrAddress", ex.Message); + Assert.Throws("hostNameOrAddress", act); } [Fact] public void GetHostEntry_ByName_HostDoesNotExist() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.GetHostEntry("lolhost"); @@ -789,22 +737,20 @@ public void GetHostEntry_ByName_HostDoesNotExist() public void GetHostEntry_ByName_HostDoesNotExist_WithThrow() { var client = new LookupClient( - new LookupClientOptions() + new LookupClientOptions(NameServer.GooglePublicDns) { ThrowDnsErrors = true }); - Action act = () => client.GetHostEntry("lolhost"); + var ex = Assert.ThrowsAny(() => client.GetHostEntry("lolhost")); - var ex = Record.Exception(act) as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); } [Fact] public void GetHostEntry_ByIp_NoHost() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = client.GetHostEntry("1.0.0.0"); @@ -815,40 +761,26 @@ public void GetHostEntry_ByIp_NoHost() public void GetHostEntry_ByIp_NoHost_WithThrow() { var client = new LookupClient( - new LookupClientOptions() + new LookupClientOptions(NameServer.GooglePublicDns) { ThrowDnsErrors = true }); Action act = () => client.GetHostEntry("1.0.0.0"); - var ex = Record.Exception(act) as DnsResponseException; + var ex = Assert.ThrowsAny(() => client.GetHostEntry("1.0.0.0")); - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); } -#if ENABLE_REMOTE_DNS - - [Fact] - public void GetHostEntry_ByIp() - { - var source = Dns.GetHostEntryAsync("localhost").Result; - var client = new LookupClient(); - var result = client.GetHostEntry(source.AddressList.First()); - - Assert.NotNull(result); - Assert.True(result.AddressList.Length == 1); - Assert.True(result.Aliases.Length == 0); - } - [Fact] public void GetHostEntry_ByManyIps() { - var client = new LookupClient(new LookupClientOptions() + var client = new LookupClient(new LookupClientOptions(NameServer.GooglePublicDns) { ThrowDnsErrors = true }); + var nsServers = client.Query("google.com", QueryType.NS).Answers.NsRecords().ToArray(); Assert.True(nsServers.Length > 0); @@ -871,7 +803,7 @@ public void GetHostEntry_ByManyIps() [Fact] public async Task GetHostEntryAsync_ByName_ManyIps_NoAlias() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.GetHostEntryAsync("google.com"); @@ -883,7 +815,7 @@ public async Task GetHostEntryAsync_ByName_ManyIps_NoAlias() [Fact] public async Task GetHostEntryAsync_ByName_OneIp_ManyAliases() { - var client = new LookupClient(); + var client = new LookupClient(NameServer.GooglePublicDns); var result = await client.GetHostEntryAsync("dnsclient.michaco.net"); @@ -904,18 +836,12 @@ public async Task GetHostEntryAsync_ByName_OneIp_NoAlias() Assert.Equal("localhost", result.HostName); } -#endif - [Fact] public async Task GetHostEntryAsync_ByName_EmptyString() { - var client = new LookupClient(); - - Func act = async () => await client.GetHostEntryAsync(""); + var client = new LookupClient(NameServer.GooglePublicDns); - var ex = await Record.ExceptionAsync(act); - Assert.NotNull(ex); - Assert.Contains("hostNameOrAddress", ex.Message); + await Assert.ThrowsAsync("hostNameOrAddress", () => client.GetHostEntryAsync("")); } [Fact] @@ -939,10 +865,8 @@ public async Task GetHostEntryAsync_ByName_HostDoesNotExist_WithThrow() ThrowDnsErrors = true }); - Func act = async () => await client.GetHostEntryAsync("lolhost"); + var ex = await Assert.ThrowsAnyAsync(() => client.GetHostEntryAsync("lolhost")); - var ex = await Record.ExceptionAsync(act) as DnsResponseException; - Assert.NotNull(ex); Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); } @@ -956,38 +880,6 @@ public async Task GetHostEntryAsync_ByIp_NoHost() Assert.Null(result); } - [Fact] - public async Task GetHostEntryAsync_ByIp_NoHost_WithThrow() - { - var client = new LookupClient( - new LookupClientOptions() - { - ThrowDnsErrors = true - }); - - Func act = async () => await client.GetHostEntryAsync("1.0.0.0"); - - var ex = await Record.ExceptionAsync(act) as DnsResponseException; - - Assert.NotNull(ex); - Assert.Equal(DnsResponseCode.NotExistentDomain, ex.Code); - } - -#if ENABLE_REMOTE_DNS - - [Fact] - public async Task GetHostEntryAsync_ByIp() - { - var source = Dns.GetHostEntryAsync("localhost").Result; - var client = new LookupClient(); - var result = await client.GetHostEntryAsync(source.AddressList.First()); - - Assert.True(result.AddressList.Length == 1); - Assert.True(result.Aliases.Length == 0); - } - -#endif - [Fact] public async Task GetHostEntryAsync_ByManyIps() { @@ -999,8 +891,6 @@ public async Task GetHostEntryAsync_ByManyIps() foreach (var server in nsServers) { var ipAddress = (await client.GetHostEntryAsync(server.NSDName)).AddressList.First(); - - Assert.NotNull(ipAddress); var result = await client.GetHostEntryAsync(ipAddress); Assert.NotNull(result); @@ -1012,5 +902,97 @@ public async Task GetHostEntryAsync_ByManyIps() Assert.Equal(server.NSDName.Value.Substring(0, server.NSDName.Value.Length - 1), result.HostName); } } + + [Fact] + public void Lookup_SettingsFallback_UseClients() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + var settings = client.GetSettings(queryOptions: null); + + Assert.Equal(client.Settings, settings); + } + + [Fact] + public void Lookup_SettingsFallback_UseClientsServers() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + // Test that the settings in the end has the name servers configured on the client above and + // still the settings provided apart from the servers (everything else will not fallback to the client's settings...) + var settings = client.GetSettings(queryOptions: new DnsQueryAndServerOptions(resolveNameServers: false) + { + ContinueOnDnsError = false, + Recursion = false, + RequestDnsSecRecords = true + }); + + Assert.Equal(client.NameServers, settings.NameServers); + Assert.Single(settings.NameServers); + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.Settings.ContinueOnDnsError, settings.ContinueOnDnsError); + Assert.NotEqual(client.Settings.Recursion, settings.Recursion); + Assert.NotEqual(client.Settings.RequestDnsSecRecords, settings.RequestDnsSecRecords); + } + + [Fact] + public void Lookup_SettingsFallback_KeepResolvedServers() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + // Should keep all the provided settings including the resolved servers. + var settings = client.GetSettings(queryOptions: new DnsQueryAndServerOptions(resolveNameServers: true)); + + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.NameServers, settings.NameServers); + } + + [Fact] + public void Lookup_SettingsFallback_KeepResolvedServers_EvenIfEmpty() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + // Trick the logic to think it was using auto resolve and should prefer these servers but then there are none + var options = new DnsQueryAndServerOptions(resolveNameServers: true); + options.NameServers.Clear(); + var settings = client.GetSettings(queryOptions: options); + + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.NameServers, settings.NameServers); + Assert.Empty(settings.NameServers); + } + + [Fact] + public void Lookup_SettingsFallback_KeepProvidedServers1() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + var settings = client.GetSettings(queryOptions: new DnsQueryAndServerOptions(NameServer.GooglePublicDns)); + + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.NameServers, settings.NameServers); + } + + [Fact] + public void Lookup_SettingsFallback_KeepProvidedServers2() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + var settings = client.GetSettings(queryOptions: new DnsQueryAndServerOptions(IPAddress.Loopback)); + + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.NameServers, settings.NameServers); + } + + [Fact] + public void Lookup_SettingsFallback_KeepProvidedServers3() + { + var client = new LookupClient(NameServer.CloudflareIPv6); + + var settings = client.GetSettings(queryOptions: new DnsQueryAndServerOptions(new IPEndPoint(IPAddress.Loopback, 33))); + + Assert.NotEqual(client.Settings, settings); + Assert.NotEqual(client.NameServers, settings.NameServers); + } } } \ No newline at end of file diff --git a/test/DnsClient.Tests/MockExampleTest.cs b/test/DnsClient.Tests/MockExampleTest.cs index 8e779329..28ce6ed3 100644 --- a/test/DnsClient.Tests/MockExampleTest.cs +++ b/test/DnsClient.Tests/MockExampleTest.cs @@ -1,4 +1,5 @@ -using System; +#if !NETCOREAPP1_1 +using System; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -8,6 +9,7 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class MockExampleTest { [Fact] @@ -41,4 +43,5 @@ public async Task MockLookup() Assert.Equal(IPAddress.Any, result.Answers.ARecords().First().Address); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/DnsClient.Tests/PooledByteTest.cs b/test/DnsClient.Tests/PooledByteTest.cs index ae507a06..d7652aef 100644 --- a/test/DnsClient.Tests/PooledByteTest.cs +++ b/test/DnsClient.Tests/PooledByteTest.cs @@ -5,6 +5,7 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class PooledByteTest { [Fact] diff --git a/test/DnsClient.Tests/ResponseCacheTest.cs b/test/DnsClient.Tests/ResponseCacheTest.cs index e3536f6a..fdd21956 100644 --- a/test/DnsClient.Tests/ResponseCacheTest.cs +++ b/test/DnsClient.Tests/ResponseCacheTest.cs @@ -8,8 +8,14 @@ namespace DnsClient.Tests { + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] public class ResponseCacheTest { + static ResponseCacheTest() + { + Tracing.Source.Switch.Level = System.Diagnostics.SourceLevels.All; + } + [Fact] public void Cache_InvalidLessThanZero() { @@ -41,25 +47,42 @@ public void Cache_SupportsInfinite() } [Fact] - public async Task Cache_DoesCacheWithMinimumDefined() + public void Cache_DoesCacheWithMinimumDefined() { var minTtl = 2000; var cache = new ResponseCache(true, TimeSpan.FromMilliseconds(minTtl)); - var record = new EmptyRecord(new ResourceRecordInfo("a", ResourceRecordType.A, QueryClass.IN, 1, 100)); + var record = new EmptyRecord(new ResourceRecordInfo("a", ResourceRecordType.A, QueryClass.IN, 0, 100)); var response = new DnsResponseMessage(new DnsResponseHeader(1, 256, 1, 1, 0, 0), 0); response.AddAnswer(record); cache.Add("key", response.AsQueryResponse(new NameServer(IPAddress.Any), null)); - await Task.Delay(1200); var item = cache.Get("key", out double? effectiveTtl); - // should not be null although TTL is zero, mimimum timeout is set to 2000ms - // TTL of the record should be zero because the initial TTL is 100 + Assert.NotNull(item); Assert.Equal(0, item.Answers.First().TimeToLive); Assert.Equal(minTtl, effectiveTtl); } + [Fact] + public void Cache_RespectsMaximumTtl() + { + var maxTtl = 2000; + var cache = new ResponseCache(true, maximumTimeout: TimeSpan.FromMilliseconds(maxTtl)); + var record = new EmptyRecord(new ResourceRecordInfo("a", ResourceRecordType.A, QueryClass.IN, 60 * 60 * 24, 100)); + var response = new DnsResponseMessage(new DnsResponseHeader(1, 256, 1, 1, 0, 0), 0); + response.AddAnswer(record); + + cache.Add("key", response.AsQueryResponse(new NameServer(IPAddress.Any), null)); + + var item = cache.Get("key", out double? effectiveTtl); + + Assert.NotNull(item); + Assert.Equal(1, cache.Count); + Assert.Equal(60 * 60 * 24, item.Answers.First().TimeToLive); + Assert.Equal(maxTtl, effectiveTtl); + } + [Fact] public void Cache_DoesNotCacheIfDisabled() { diff --git a/tools/etc/named.conf b/tools/etc/named.conf index f1546e00..5c18fd8a 100644 --- a/tools/etc/named.conf +++ b/tools/etc/named.conf @@ -41,6 +41,9 @@ options { responses-per-second 0 ; all-per-second 0 ; }; + use-v4-udp-ports { range 9000 55000; }; + use-v6-udp-ports { range 9000 55000; }; + }; zone "mcnet.com" IN { type master;