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;
}
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 ARecord
s.
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");