DnsClient.NET

The DNS Resolver for .NET

Current Version: 1.5.0

Simple but Powerful

DnsClient.NET is a simple yet very powerful and high performant open source library for the .NET Framework to do DNS lookups.
It can be used in any kind of application to query the network's DNS server or any other DNS server even on non-default ports.

Cross Platform

Targeting the new NetStandard library of .NET, the DnsClient library can be used on Windows, Mac or Linux!

Try It:

Query

This runs a DNS query on the server using the latest version of DnsClient.NET and public Google DNS Servers (8.8.4.4:53, 8.8.8.8:53).

Result

Report Issues or ask Questions

If you have any questions or issues with this library, please reach out to us on GitHub.
Create a new issue in case you want to report a bug.
If you want to suggest a new feature or just ask a question, please check if there is already an issue or a discussion related to your topic, otherwise, you can simply start a new one!

Usage

The Lookup Client

The LookupClient class is the main tool of this library. It provides configuration options and methods to run DNS queries and reverse lookups.

It is important to note, that it is highly recommended to instantiate the lookup client only once per application and to always use the same reference (singleton).
Otherwise, features like result cache and connection pooling will not have any effect which can decrease the application's performance.
That being said, the lookup client is thread safe, which means it can be used in a multithreaded and/or async context without any problems.

The following code initializes a new lookup client using your local network DNS server(s).

var client = new LookupClient();
client.UseCache = true;

But you can also specify a custom DNS server running on any port:

var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8600);
var client = new LookupClient(endpoint);

For mock friendly and easier to test code and usage with injection (DI), it is recommended to have implementers only consume the interface ILookupClient or IDnsQuery instead of the concrete implementation.
Simple DI example:

services.AddSingleton<ILookupClient>(client);

Which can be consumed later by some MVC Controller for example:

public SomeController(ILookupClient client)
{
    if(client == null)
    {
        throw new ArgumentNullException(nameof(client));
    }

    _client = client;
}

Query APIs

The LookupClient's Query and QueryAsync methods can be used to run standard DNS queries for a domain name.
You have to provide a QueryType and optionally a QueryClass in addition to a valid domain name.

The following example queries for all A resource records:

var result = client.Query("domain.name.com", QueryType.A);

Resource Record Specific Types

Each type of resource record transports very specific and different data which needs custom parsing. Therefore, resource records in DnsClient also have very different properties to access the data.
Resource record collections of the LookupClient's query result are typed with the base class of all resource records, which means, to get to the QueryType specific data, each result record must be cast to the actual type.

To make that process a little bit easier, DnsClient comes with some build in extension methods for the most common types.
An A record for example always only provide an IP address. The corresponding ARecord class stores the IP address in the Address property.
The ARecords extension method filters all A records from the answers collection, and casts them to ARecords. Now, the Address property is available and can be used:

foreach (var aRecord in client.Query("google.com", QueryType.A).Answers.ARecords())
{
    Console.WriteLine(aRecord.Address);
}

This is the same as doing a .OfType<ARecord>() call on the answers collection.

The following is another example for how to manual cast and then access the resource record type specific data.

var record = _client
    .Query("domain.name.com", QueryType.A)
    .Answers.OfType<HInfoRecord>()
    .FirstOrDefault();

// or
var record = _client
    .Query("domain.name.com", QueryType.A)
    .Answers.OfRecordType(ResourceRecordType.HINFO)
    .FirstOrDefault() as HInfoRecord;

if(record != null)
{
    Console.WriteLine($"Cpu: {record.Cpu} OS: {record.OS}");
}

Additional Results

A DNS query result can have multiple sections: The answers, authority and additional records.
Answers are the actually requested resource record(s), if any, while additional records might contain resource records which contain additional information to the answer records.

A good example is the SRV record. The actual SRV record transports the port and domain name of the service. Which can be accessed via Port and Target properties of the SrvRecord class.
The additionals section might contain another A or CNAME record which transports the address of the requested service. To find the corresponding record in the additionals section (there could be multiple), we have to match the Target domain name of the answer with the DomainName of the additional record.
Example of how to use additional records:

var result = await _client
    .QueryAsync("_service.domain.com", QueryType.SRV);

if (result.HasError)
{
    throw new InvalidOperationException(result.ErrorMessage);
}

var srvRecord = result
    .Answers.OfType<SrvRecord>()
    .FirstOrDefault();

if (srvRecord != null)
{
    var additionalRecord = result.Additionals
        .FirstOrDefault(p => p.DomainName.Equals(srvRecord.Target));

    if (additionalRecord is ARecord aRecord)
    {
        Console.WriteLine($"Services found at {srvRecord.Target}:{srvRecord.Port} IP: {aRecord.Address}");
    }
    else if (additionalRecord is CNameRecord cname)
    {
        Console.WriteLine($"Services found at {srvRecord.Target}:{srvRecord.Port} IP: {cname.CanonicalName}");
    }
}

See also the resolve service methods below.

Synchronous and Async APIs

Async and sync API implementations are available for all query API methods.

var result = client.Query("google.com", QueryType.MX);
var host = await client.QueryReverse(ip);

// or
var result = await client.QueryAsync("google.com", QueryType.MX);
var host = await client.QueryReverseAsync(ip);

Extended Query API

Some queries might involve more complex business logic to get the results we actually want, e.g. doing a reverse lookup of an IP address to get the hostname and then querying for all IPv4 and IPv6 addresses of that hostname.

For some of those more commonly used things, this DNS client comes with a default implementation. (If you think that there is something important missing or not working as you expect, feel free to post an issue on GitHub)

GetHostName

The GetHostName or GetHostNameAsync method does a reverse lookup and returns the hostname as string, or null if not found.

Example:

var client = new LookupClient();
string hostName = await client.GetHostNameAsync(IPAddress.Parse("8.8.8.8"));

This is a shortcut for

var client = new LookupClient();
var result = await client.QueryReverseAsync(IPAddress.Parse("8.8.8.8"));
var hostName = result.Answers.PtrRecords().FirstOrDefault()?.PtrDomainName;

Get Host Entries

The GetHostEntry method accepts either an IPAddress or a string which can be an IP address or hostname.

Depending on what gets passed in to the method, it does a reverse lookup of the address first and then tries to populate a IPHostEntry instance. See the documentation remarks of GetHostEntry for more details.

Example:

var client = new LookupClient();
var hostEntry = await client.GetHostEntryAsync("mail.google.com");
If we'd run a normal query for an A or AAAA record of mail.google.com, the result would something like this:
;; ANSWER SECTION:
mail.google.com.        594243  IN      CNAME   googlemail.l.google.com.
googlemail.l.google.com. 227    IN      AAAA    2a00:1450:4016:807::2005
The entry returned by GetHostEntry in this example would have an IPv4 and IPv6 address, one Alias "googlemail.l.google.com" and the HostName property would be set to "mail.google.com".

Resolve Service APIs

The ResolveService queries for SRV records by using the syntax defined in RFC2782 to resolve available service hosts and ports.

The query syntax defines a special hostname, like _ldap._tcp.example.com which contains the service name, in this case _ldap and optionally a protocol, in this case _tcp.

Same example using the lookup client:

var client = new LookupClient();
var result = client.ResolveServiceAsync("example.com", "ldap", System.Net.Sockets.ProtocolType.Tcp);

Important to note that there should be no underscore prefixing the service name or protocol. This will be appended automatically, if the protocol is set.

One interesting use-case for this is DNS based client side service discovery. Consul for example has a DNS endpoint and supports this syntax. Instead of protocol, we can query for tags defined for the service in the Consul service registry (this is optional again).

In the following example, we'll query for the consul service itself:

var client = new LookupClient(IPAddress.Parse("127.0.0.1"), 8600);
var entry = await client.ResolveServiceAsync("service.consul", "consul");

Configuration Options

Use Cache

Setting the UseCache property to true, allows the lookup client to cache all query results depending on the minimum time to live of all returned records.

This setting is enabled per default.

client.UseCache = true;

Minimum Cache Timeout

Setting the MinimumCacheTimeout property to a positive value allows the lookup client to cache query results, even if the resource records do have no time to live defined.
This is useful in performance critical solutions where even a few seconds matter.
Cached results can be accessed with millions of hits per seconds while actual queries might only reach 10 to a few 100 thousand hits per second.

This setting has no default.

client.MinimumCacheTimeout = TimeSpan.FromSeconds(10);

Audit Trail

Enabling the EnableAuditTrail property of the lookup client, adds more information to the returned result.
Beside the collections of resource records, result.AuditTrail will contain a full log of the request and response.
The log format is similar to DNS files or the console output of the dig command line utility.

This setting is disabled per default.
Keep in mind that enabling this feature has a small performance impact and should only be used for debugging purpose.

var client = new LookupClient(NameServer.GooglePublicDns, NameServer.GooglePublicDns2);

client.EnableAuditTrail = true;
client.UseCache = true;

var result = await client.QueryAsync("google.com", QueryType.A);

Console.WriteLine(result.AuditTrail);

TCP Settings

Enabling the UseTcpFallback property allows the client to run the same query via TCP in case the UDP result was truncated.

This can happen in cases where the UDP result exceeds the maximum supported size of the DNS server can return.
The default size is usually 4096 or 512 bytes.

This setting is enabled per default.

Setting UseTcpOnly to true would disable any UDP communication. This can be useful in case UDP transport is disabled through firewall rules for example.

Continue and Retry on DNS Error

If multiple DNS name servers are configured for the ILookupClient instance, per default, the client will try the next server (in random or configured order depending on UseRandomNameServer) if the current server responded with a DNS error.

To change that behavior and stop after the first response, disable the ContinueOnDnsError option.

Throw on DNS Errors

Enabling the ThrowDnsErrors property would throw a DnsResponseException if the result of a DNS query does contain a response code in the header other than NoError.

This setting is disabled per default.

This setting can be useful if you prefer to handle such errors with try-catch blocks instead of inspecting the result object.

Also, some methods and extensions of IDnsQuery do not return a DnsResponseMessage with the full details but may return a string or null in case of an error. Enabling this property would throw an exception instead.

Handle multiple DNS Nameservers

If multiple DNS name servers are configured for the ILookupClient instance, per default, the servers are used in random order.

To change that behavior and always start with the first one and then fall back to the next, disable the UseRandomNameServer option.