Creating an HttpClient that uses DNS over Https

 
 
  • Gérald Barré

DNS is a key component of the Internet. It's used to translate a domain name to an IP address. For instance, when you type https://www.meziantou.net in your browser, the browser will query the DNS server to get the IP address of the server hosting the website. Then, the browser will connect to the server using the IP address.

A good practice is to rely on the OS configuration to query the DNS server. However, sometimes you may want to use a specific DNS server to bypass configuration issues. In this case, you can use your own DNS client. Also, to be sure to bypass any DNS configuration or firewall rules, you may want to use DNS over HTTPS (DoH). In this post, I describe how to use DoH in .NET.

First, you need to add the NuGet package TurnerSoftware.DinoDNS to your project. This package contains a DNS client that can query DNS servers using UDP, TCP, DoT or DoH. Then, you can use SocketsHttpHandler.ConnectCallback to open the socket manually. So, you need to query the DNS server to get the IP address of the server you want to connect to and open the socket using the address.

Let's create the project and add the package:

Shell
dotnet new console
dotnet add package TurnerSoftware.DinoDNS

Then, you can use the following code to create the HttpClient:

C#
using System.Net;
using System.Net.Sockets;
using TurnerSoftware.DinoDNS;
using TurnerSoftware.DinoDNS.Protocol;

// Initialize the DNS client to use Cloudflare on DNS over Https.
// You can specify more than one server if needed.
var dnsClient = new DnsClient([NameServers.Cloudflare.IPv4.GetPrimary(ConnectionType.DoH)], DnsMessageOptions.Default);
var sockerHttpHandler = new SocketsHttpHandler()
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        // Query DNS and get the list of IPAddress
        var dnsMessage = await dnsClient.QueryAsync(context.DnsEndPoint.Host, DnsQueryType.A, cancellationToken: cancellationToken);
        var records = dnsMessage.Answers;
        if (records.Count is 0)
            throw new Exception($"Cannot resolve domain '{context.DnsEndPoint.Host}'");

        var addresses = new List<IPAddress>();
        foreach (var record in records)
        {
            if (record.Type is DnsType.A or DnsType.AAAA)
            {
                addresses.Add(new IPAddress(record.Data.Span));
            }
        }

        // Connect to the remote host
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
        {
            // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
            NoDelay = true
        };

        try
        {
            await socket.ConnectAsync(addresses.ToArray(), context.DnsEndPoint.Port, cancellationToken);
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
};

var httpClient = new HttpClient(sockerHttpHandler, disposeHandler: true);

Finally, you can use the HttpClient as usual:

C#
Console.WriteLine(await httpClient.GetStringAsync("https://www.meziantou.net"));

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub