PKDNS

Posted on by Chris Warburton

I’ve been playing with pkarr, which stores DNS records in the “Mainline” DHT. After trying a few different setups, I’ve now got a setup that seems quite reliable; which is specified in the dnsdist module of my nix-config.

Mainline DHT

Mainline was originally created for tracking BitTorrent peers; but BEP044 extends it to allow arbitrary data (up to 1KB).

BEP044 allows two ways to store data in the Mainline DHT:

Immutable data

This cannot be changed, so its hash can be used as a stable identifier; i.e. it is content-addressed.

This isn’t super useful, since we can often just use that 1KB of data as-is, rather than introducing an unreliable network dependency; though I suppose it allows some Merkle-tree constructions.

Mutable data

Since mutable data can be changed, its hash can’t be relied on as a stable identifier. Instead, mutable storage uses public/private key pairs:

An optional salt can be added, to allow many addresses for the same private key.

pkarr

pkarr stores DNS records as mutable data in the Mainline DHT. On its own, this forms a hierarchical database, e.g. we can add all sorts of data to a pkarr address, and use CNAME records to associate various subdomains to other addresses; and so on for their subdomains. Of course, that’s exactly how DNS itself works: pkarr differs from DNS by replacing authoratitive servers with a P2P network; and adds a layer of cryptographic verification. The downside is that operations take longer, and may need a few retries.

pkdns

pkarr becomes even more useful if we use pkdns: a DNS server which can respond to queries for pkarr addresses by retrieving their records from the Mainline DHT. This lets us use pkarr addresses just like any other.

Keep in mind that the cryptographic verification doesn’t survive translation from pkarr to DNS; so we have to trust whoever’s running the pkdns server to not spoof or manipulate the responses they send back. The easiest way to ensure this is to run pkdns ourselves, rather than relying on any third-party instance!

In principle, pkdns will pass-through any non-pkarr addresses to a different (ordinary) DNS server, so we can use it as our system-wide DNS server on localhost port 53. Unfortunately, I’ve found pkdns itself to be a bit flaky, so sending all of my DNS lookups through it was causing problems.

Instead, I’m running it on a non-standard port 5300:

[general]
socket = "0.0.0.0:5300"
forward = "8.8.8.8:53"
[dns]
[dht]

If any non-pkarr queries hit this pkdns server, it will try to forward them on to Google Public DNS.

dnsdist

The server I’m running on localhost port 53 is instead a load-balancer called dnsdist. It’s able to direct queries to different servers based on regular expressions, so we can send anything that looks like a pkarr address to pkdns (on localhost port 5300), whilst sending everything else to a “normal” DNS server running on localhost port 5301:

setLocal("127.0.0.1:53")

newServer({address="127.0.0.1:5301", pool="default"})

-- pkdns handles pkarr domains (52-character z-base32 strings)
-- Disable health checks: the default health check queries
-- a.root-servers.net which pkdns cannot resolve.  Since this is a
-- local backend managed by systemd, availability monitoring is
-- better left to systemd rather than dnsdist.
newServer({address="127.0.0.1:5300", pool="pkdns", healthCheckMode="up"})

-- Route pkarr queries to pkdns.
addAction(RegexRule("^(([^.]+\\.)+)?[ybndrfg8ejkmcpqxot1uwisza345h769]{52}\\.?$"), PoolAction("pkdns"))

-- Everything else goes to the default upstream
addAction(AllRule(), PoolAction("default"))

dnsmasq

Finally, we need our “normal” DNS server, running on localhost port 5301. I’m using dnsmasq to forward queries on to the systems’s usual nameservers (which in my case are set dynamically, via DHCP):

dhcp-leasefile=/var/lib/dnsmasq/dnsmasq.leases
port=5301
resolv-file=/etc/dnsmasq-resolv.conf

That /etc/dnsmasq-resolv.conf file gets updated when the network changes, thanks to these NixOS options:

    networking.resolvconf.extraConfig = mkIf useDnsmasq ''
      dnsmasq_resolv=${dnsmasqResolv}
    '';
    networking.resolvconf.subscriberFiles = mkIf useDnsmasq [
      dnsmasqResolv
    ];

    # Pre-create the file so dnsmasq doesn't fail before resolvconf first runs
    # (dnsmasq's own preStart only touches it when resolveLocalQueries = true).
    systemd.tmpfiles.rules = mkIf useDnsmasq [
      "f ${dnsmasqResolv} 0644 root root -"
    ];

Conclusion

With this setup, normal name lookups proceed as before; except rather than using the contents of /etc/resolv.conf directly; we instead go through dnsdist, then dnsmasq, then /etc/dnsmasq-resolv.conf (whose contents is the same as /etc/resolv.conf).

Yet we can also look up pkarr addresses, e.g.

$ dig +short 7fmjpcuuzf54hw18bsgi3zihzyh4awseeuq5tmojefaezjbd64cy
34.65.109.99