DNS over HTTPS on Linux

Publish 11/15/2020, 10:24:29 PM | Author: NunoDNS over HTTPS on Linux

This is a library that offers name resolution using Cloudflare's REST API for GLibC based operative systems, DoH for short. In general, every operative system offers some form of DNS name resolution API, for instance, on GLibC you have gethostbyname, however, this library goes two steps further, one is the particularity of executing that task over an encrypted channel and the other is that will be the who will provide answers when you call gethostbyname.

Nowadays, DNS resolution is done via the user data protocol, which implies plaintext queries that are passive to be leaked to third parties capable of monitor the network. The metadata that is being given by us on the intent of which website we are trying to connect to is privacy and a human right offense that we must fight, and as developers, we have a moral responsibility to not accept this situation and push this lack of privacy to /dev/null.

We developers came with two approaches, DNS over TSL (DoT) and DNS over HTTPs traffic (DoH). Both use TLS, one is for encrypts UDP traffic and the other is to encrypt HTTP traffic.

A fast overview of this feature tells that on any distro GNU/Linux with systemd you have DoT, but is disabled by default, on Android DoH was released starting Chrome 80. Apple said that will support DoH and DoT by this fall and Windows 10 also has support for both modes.

I am be focusing on Linux, I won't deny it, it is my favorite. In Linux, we have a special file: nsswitch.conf and as Delorie puts out: "that most people ignore, few people understand, but all people generally rely on" and I add, and the documentation for developers is scarce.

This file from the Name Service Switch has the information about the services (databases as they put out) and the corresponding libraries. API calls like gethostbyname will trigger a lookup upon this file to see which library to handle the request. In this example, and according to the output of nss file the call will be handled by the files and dns database, if files do not come with an answer, the next one will be invoked, in this case, dns

# cat /etc/nsswitch.conf

# database      implementation
passwd:         files systemd
group:          files systemd
shadow:         files
gshadow:        files

hosts:          files dns
networks:       files

protocols:      db files
services:       db files
ethers:         db files
rpc:            db files

netgroup:       nis

Technically, how this delegation is achieved by GLibC? When you call gethostbyname, GLibC selects one library based on your nss configuration, the name of the selected will be used to find the shared library libnss_dns.so and then, the target method is prefixed _nss_dns_ and a call is made to the library.

# objdump -d `whereis libnss-dns` | grep '_nss_dns_.*:$'
0000000000001160 <_nss_dns_gethostbyname3_r@plt>:
0000000000001220 <_nss_dns_gethostbyname3_r@@GLIBC_PRIVATE-0x12b0>:
00000000000024d0 <_nss_dns_gethostbyname3_r@@GLIBC_PRIVATE>:
0000000000002598 <_nss_dns_gethostbyname2_r@@GLIBC_PRIVATE>:
0000000000002640 <_nss_dns_gethostbyname_r@@GLIBC_PRIVATE>:
0000000000002728 <_nss_dns_gethostbyname4_r@@GLIBC_PRIVATE>:
0000000000002a78 <_nss_dns_gethostbyaddr2_r@@GLIBC_PRIVATE>:
0000000000002e68 <_nss_dns_gethostbyaddr_r@@GLIBC_PRIVATE>:
00000000000033d8 <_nss_dns_getnetbyname_r@@GLIBC_PRIVATE>:
0000000000003558 <_nss_dns_getnetbyaddr_r@@GLIBC_PRIVATE>:
00000000000037f0 <_nss_dns_getcanonname_r@@GLIBC_PRIVATE>:

This way, the developers can add their implementation. For each database, the developer must implement the respective interface. Exists several others libraries like libnss-mysql, libnss-systemd, etc.

After showing the internals, by now, we know how to make DoH system-wide a reality. We must implement hosts API a make an HTTPs request to CloudFlare's API, and lucky for us, we do not have to implement and make HTTP requests in C since the Rust community is awesome and someone has done the Rust bindings. The interface becomes simpler.

pub trait HostHooks {
    fn get_all_entries() -> Response<Vec<Host>>;

    fn get_host_by_name(name: &str, family: AddressFamily) -> Response<Host>;

    fn get_host_by_addr(addr: IpAddr) -> Response<Host>;

The method get_host_by_name is the one that we are interested in. We receive the name and if it is been requested IPV4 or IPV6 for that name. Then, we make the HTTP request. We have used OpenSSL to encrypt the TPC traffic, on this request it's added the SNI extension, and I have parsed the HTTP protocol without using any third-party library.

To install this library, and assuming you already have Rust installed in your machine, open the terminal and type:

git clone https://github.com/NunuM/dns-over-https-nss-linux

cd dns-over-https-nss-linux/doh

cargo build --release

cd ..

cd target/release
cp libnss_doh.so libnss_doh.so.2
sudo install -m 0644 libnss_doh.so.2 /lib
sudo /sbin/ldconfig -n /lib /usr/lib

# edit nss configuration
sudo nano /etc/nsswitch.conf
#hosts: files doh 

Now, your DNS queries will be handled by this library. Note that if you have your browser open, you need to restart it, since at the time you open your browser the nss configuration was different.

How do we know that our library is the one making the DNS resolution? Well, you can ping google.com, or strace -o debug ping google.pt and examine the output of strace command. Or use syslog, by exporting debug variable and ping again and search using journalctl command.

export NSS_DOH_DEBUG=1

# see all requests
journalctl -t nss_doh

# follow last one 
journalctl -f  -t nss_doh

To conclude, it is obvious that Cloudflare will know our DNS queries, but this is a choice that I am willing to make, so do you. Besides that, this knowledge can be useful for service discovery in distributed systems, or blacklisting/whitelist domain names at the local level. I hope that you like this post and if you see any error, am I open to resolve new issues.