Skip to main content

An Authoritative Server for .eth

(The repository with code is here: https://gitlab.com/aerique/dnsdist-ens/. The links to code have not been fixed.)

Introduction

This is the third exploration of resolving ENS through traditional DNS servers. The previous two articles have used DNSdist, which is a DNS load balancer. This time we’re using an authoritative server (“auth”) for several reasons:

  • The auth will, ideally, be directly querying an Ethereum client so it is in fact authoritative,
  • Lua inside DNSdist runs serialized,
  • And most importantly using the PowerDNS Authoritative Server we can use the remote backend leaving the choice of programming language to us: we can just use JavaScript and Ethereum libraries, making all the work in the previous blog posts for naught… awesome!

The previous articles:

This time the Ethereum and ENS parts will be trivial due to using libraries but you’ll get a look at old-fashioned DNS and we’ll be pondering the question of what it means to try and resolve ENS from DNS.

While one might think ENS to be a superset of DNS the two are actually not a straight fit.

Why?

“Building bridges from web2 to web31.”

1) for lack of a better word and possibly more rage-inducing than “crypto”

What’s the point of trying to resolve ENS using traditional DNS servers? Why would one want to do that?

There’s interesting stuff happening in the web3 world and to access it programs need to actively support it. Some browsers natively support ENS but most programs do not. Most programs use —for good reason— the operating system’s DNS stack to resolve domain names. ENS names look very much like traditional domain names and users will expect ENS names to work like traditional domain names.

So why not build some barebones support for ENS into DNS. The latter runs at big ISPs and telecom companies that are used by a large part of the internet population. Programs that treat an ENS name like a traditional DNS name will transparently be able to get at least some support for them.

ENS Backend for the PowerDNS Authoritative Server

(If you’re running inside the Docker image as described in Running It Yourself all the commands are done inside the ens subdirectory.)

This article comes with a minimal remote backend written in JavaScript for the PowerDNS Authoritative Server. Work on it was finished when “nick.eth”, “gregskril.eth” and “dns-test.aerique.eth” could be resolved without throwing errors. None of the instances during development have been long-running so the auth might do things later the backend does not expect. Fixing this is left as an exercise to the reader.

When the auth is fired up it expects the remote backend to be running already. So we’ll start the remote backend first with

node resolve-ens.js

and after the remote backend (as a sanity check) has resolved the “eth” ENS name the auth can be started with

pdns_server --config-dir=.

The auth will send a getAllDomains query right away which will be handled by the handleGetAllDomains function inside resolve-ens.js. The reply to this query will be

{ 'zone': 'eth.' }

which signals to the auth we’re handling the top-level “.eth” domain. After that we can send queries for ENS names to this auth using standard DNS tools like dig and get DNS responses back such as NXDOMAIN, NOERROR and of course the domain the ENS name resolved to.

The auth will also send getAllDomainMetadata queries and while the remote backend must support these it will just send an empty response back, which is enough to make the auth happy.

Three things:

  1. The dot at the end of a DNS domain is not a typo but signifies the DNS root zone and they are used here because the auth probably expects them from the remote backend (this hasn’t actually been checked) and it also makes it clear when DNS domains as opposed to ENS names are being handled.

  2. It is defined in pdns.conf how the auth can connect to the remote backend.

  3. The top-level “.eth” domain belongs to Ethiopia but is not actually in use, Ethiopia uses “.et”. The ENS DAO is trying to acquire “.eth” from Ethiopia but this will not automatically make ENS work for DNS.

    Using an unused top-level DNS domain avoids name clashes if the two are ever to work together in the future. There are several other naming systems being developed which are not taking such precautions.

Querying ENS Names

Now we come to the meat of the remote backend: handling lookups.

Lookups of ENS names like “nick.eth” are all handled by the handleLookup function. When querying the auth with

dig @localhost nick.eth

it shows in the remote backend logging a lookup/nick.eth./ANY query. When “nick.eth” is queried multiple times like this the auth should start answering from its cache and you should not see activity in the remote backend anymore (add +nocookie to the command above if you do not see cache hits).

Also if this is the first query (or some time between queries has passed) a lookup/eth./ANY query is done first. This is to refresh the top-level zone information and is responded to with a SOA record.

(The auth always does ANY queries to backends to save on doing multiple queries back and forth: https://doc.powerdns.com/authoritative/appendices/backend-writers-guide.html#notes)

The handleLookup function has some really basic heuristics for answering lookups to ENS names (and apologies for my awful JavaScript):

resolver.getContentHash().then((ens_ch) => {
    if (ens_ch != null && ens_ch.substring(0,7) == 'ipfs://') {
        var ch = contentHash.helpers.cidV0ToV1Base32(ens_ch.substring(7))
        var ipfs_link = ch + '.ipfs.dweb.link.'
        response.writeHead(200)
        response.end(JSON.stringify({'result':[ethCNAME(qname,ipfs_link)]}))
    } else {
        resolver.getText('cname').then((ens_cname) => {
            if (ens_cname != '') {
                response.writeHead(200)
                response.end(JSON.stringify({'result':[ethCNAME(qname,ens_cname)]}))
            } else {
                respondFallback(response, qname, ens_name)

It retrieves the contenthash from an ENS profile (resolver.getContentHash()):

  1. if the contenthash is not empty and starts with ipfs:// it will return a link through an IPFS gateway,
  2. otherwise it will check the ENS cname text field and if that is not empty it will return the contents as a DNS CNAME record,
  3. if both of the above actions are not applicable it will fall back to returning an ETH.LIMO subdomain (which is what respondFallback does).

Heuristics for Resolving ENS from DNS

As written in the introduction and seen in the review of the handleLookup function there isn’t a clear answer in how to answer queries for ENS names as a DNS server. The function already contained a really basic attempt at fulfilling such queries but there’s perhaps more possible.

There is a proposal for adding DNS records to ENS which would be the ideal candidate to check first in an ENS profile:

This EIP defines a resolver profile for ENS that provides features for storage and lookup of DNS records. This allows ENS to be used as a store of authoritative DNS information.

(In the draft of this article we also extracted the host part from the ENS url field. However, both my colleague Peter van Dijk and nick.eth urged me not do this because it was apparently a really bad idea. I still don’t know why and have to get this explained to me over a beer with Peter.)

For now the heuristics for resolving ENS through DNS are as follows:

  1. check for DNS records in the ENS profile and return the relevant one
    (this step is not yet possible but see the proposal mentioned above),
  2. check the ENS contenthash and return it as a DNS CNAME through an IPFS gateway, depending on the kind of contenthash:
    • IPFS: «contenthash».ipfs.dweb.link.,
    • IPNS: «contenthash».ipns.dweb.link.,
    • Swarm: not looked into for this article,
  3. check the ENS cname text field and return it as a CNAME record,
  4. if everything else fails return the ETH.LIMO subdomain for an ENS name, so for “nick.eth” return nick.eth.limo. as CNAME.

Running It Yourself

Dependencies

This post was written using:

  • NodeJS v18.16.0
  • PowerDNS Authoritative Server v4.7.4
  • Debian Bullseye Docker image
  • Docker v20.10.17 on Guix (I’m a Lisp weenie)

Usage

These examples use Docker but feel free to try and get it running without it.

Build the Docker image by running bin/build.sh.

Open three terminal windows:

  • terminal 1
    • bin/run.sh
    • cd ens
    • node resolve-ens.js
  • terminal 2
    • docker exec -it ens-dns bash
    • cd ens
    • pdns_server --config-dir=.
  • terminal 3
    • docker exec -it ens-dns dig @localhost nick.eth

Example Output

$ docker exec -it ens-dns dig @localhost gregskril.eth +nocookie +norec

; <<>> DiG 9.16.37-Debian <<>> @localhost gregskril.eth +nocookie +norec
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37078
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;gregskril.eth.			IN	A

;; ANSWER SECTION:
gregskril.eth.		60	IN	CNAME	bafybeifgil7fclzl5nsmikh6ml6lwphs3vrandqwhkm6d73yos2fb7a36y.ipfs.dweb.link.

;; Query time: 735 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Jun 02 14:51:04 UTC 2023
;; MSG SIZE  rcvd: 130
$ docker exec -it ens-dns dig @localhost dns-test.aerique.eth +nocookie +norec

; <<>> DiG 9.16.37-Debian <<>> @localhost dns-test.aerique.eth +nocookie +norec
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16146
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;dns-test.aerique.eth.		IN	A

;; ANSWER SECTION:
dns-test.aerique.eth.	60	IN	CNAME	www.aerique.net.

;; Query time: 1963 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Jun 02 14:51:57 UTC 2023
;; MSG SIZE  rcvd: 78
$ docker exec -it ens-dns dig @localhost nick.eth +nocookie +norec

; <<>> DiG 9.16.37-Debian <<>> @localhost nick.eth +nocookie +norec
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22374
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;nick.eth.			IN	A

;; ANSWER SECTION:
nick.eth.		60	IN	CNAME	nick.eth.limo.

;; Query time: 955 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Jun 02 14:52:24 UTC 2023
;; MSG SIZE  rcvd: 64

DNS TXT Records

We did not explore DNS TXT records like an earlier article but the ENSjs library has a really nice function getProfile that will fetch almost all information from an ENS profile in one call (using The Graph) and would be really useful to return as DNS TXT records.

Shout Outs!

Thanks to some of my colleagues at PowerDNS / Open-Xchange for their help: Peter van Dijk, Konrad Wojas.

Also thanks for help to: Greg Skriloff and Nick Johnson.