DOH everything

Two days ago I wrote about a new small utility crate for doing DNS over HTTPS. When I started with the code on Monday I had an actual use case in mind.

Due to some combination of SmartOS, Ubuntu 17.04 and glibc my server currently has problems resolving DNS. It turns out a server without a working DNS resolver is kind of a pain, adding hostnames and their IPs to /etc/hosts gets tiring pretty fast.

Luckily, glibc's gethostbyname makes us of external plugins to do the actual resolving. That's how /etc/hosts and an external resolver work in the first place. The documentation on the exact plugin mechanism is quite sparse, but luckily systemd comes with its own resolve plugin: nss-resolve.c.

All it takes to build a resolver plugin is to implement two functions1:

enum nss_status _nss_$plugin_gethostbyname4_r(...)
enum nss_status _nss_$plugin_gethostbyname3_r(...)

Both take a bunch of arguments, but it boils down to this:

  1. Get the hostname (and optionally the address family to look for)
  2. Resolve this hostname to its IP addresses2
  3. Copy the data into the provided buffer
  4. Let the result point somewhere into this buffer
  5. Clear the error values and return a success value

If at any point an error is encountered, several error values are set and the function returns a failure.

If all goes right, the requesting application will receive a data structure to read the different IP addresses from, which it can then use to establish a connection.

Equipped with dnsoverhttps the resolving part is suprisingly easy. First turn the passed name into a string, then call into the library and collect the results.

unsafe {
    let slice = CStr::from_ptr(orig_name);
    let name = slice.to_string_lossy();
    let addrs = match dnsoverhttps::resolve_host(&name) {
        Ok(a) => a,
        Err(_) => {
            *errnop = EINVAL;
            *h_errnop = NO_RECOVERY;
            return NSS_STATUS_UNAVAIL;
        }
    };

    // ...
}

Filling the appropiate data structures and memory buffer is a bit more complex and involves some pointer trickery. Instead of trying to find the right (unsafe) approach in Rust to do it correctly I opted to copy the systemd-code for now.

In the Rust part we can forward the received arguments and add an array of results3:

let addrs : Vec<_> = addrs.into_iter().map(ip_addr_to_tuple).collect();

write_addresses4(orig_name, pat, buffer, buflen, errnop, h_errnop, ttlp,
  addrs.as_ptr(), addrs.len())

This calls into a function written in C and compiled just before the Rust code is compiled. It's also statically linked into the resulting shared object, ready to be deployed.

In order to use it, the library has to be copied into a directory where the system can find it:

cp target/release/libnss_dnsoverhttps.so /usr/lib/libnss_dnsoverhttps.so.2

And then it can finally be used by adding it to /etc/nsswitch.conf like this:

hosts: dnsoverhttps files mymachines dns myhostname

curl and other processes calling gethostbyname will from now on get addresses resolved by this small module.



1

And forward _nss_$plugin_gethostbyname2_r and _nss_$plugin_gethostbyname_r to _nss_$plugin_gethostbyname3_r.

2

Remember that you can't use gethostbyname here, because you are gethostbyname.

3

In between turning our vector of addresses into a simpler structure.