PKDNS
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:
- The public key is used as the address.
- The private key is used to sign the data, proving that it came from the key holder for that address.
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