Skip to main content

[DRAFT] 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.)

I’m putting this out here while it is still in a draft state since I have other stuff to do and it might be a while before I get back to this. This article has not been proof-read by others and might have incorrect statements (especially about DNS and the auth). It is also somewhat disjointed and lacks a point. Still, even in this state it might be useful to others.

To Do:

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, 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 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”, “foo.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 on 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 like in this article. 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.

Handling 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 SOA and NS records.

(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

provider.getResolver(ens_name).then((resolver) => {
    resolver.getText('url').then((ens_url) => {
        if (ens_url == '') {
            respondFallback(response, qname, ens_name)
        } else {
            var pu = url.parse(ens_url)
            if (pu.path == '/') {
                response.writeHead(200)
                response.end(JSON.stringify({ 'result': [ ethCNAME(qname, pu.host) ]}))
            } else {
                respondFallback(response, qname, ens_name)
            }
        }

It retrieves the URL field from an ENS profile (resolver.getText('url')):

  1. if the URL field has not been filled it falls back to returning the “eth.limo” address of the ENS name,
  2. if the URL field only has a host part it returns that host part
    • for example, “nick.eth” has https://ens.domains/ as URL which only has a host part since nothing comes after the last “/”, so ens.domains. is returned,
  3. if the URL field also has a path it again falls back to returning the “eth.limo” address of the ENS name

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.

So a first more elaborate attempt at resolving ENS through DNS heuristics could be the following steps:

  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. possibly check the contenthash field but on a superficial reading it looks like meaningful domain names cannot be extracted from it,
  3. check the url field and if it only has a host part return that,
    • “nick.eth” has https://ens.domains/ in the URL field so we return ens.domains.,
    • “foo.eth” has https://twitter.com/foodoteth in the URL field which has both a host (twitter.com) and path (/foodoteth) so we do not return the host part since returning twitter.com. would be meaningless and we skip to the next step,
    • “aerique.eth” does not have a URL field so we skip to the next step,
  4. if everything else fails return the “eth.limo” for an ENS name, so for “nick.eth” return nick.eth.limo. as DNS CNAME.

For now, and somewhat disappointing, it looks like unless there’s a host-only URL field (and unless DNS records are added to ENS) we’ll fall back to returning an “eth.limo” address. I’m not sure yet if this is good or bad: on the one hand it would be nice to have more options to return but on the other hand extensive heuristics could get confusing.

Then again, in a browser that does not support ENS getting shown an “eth.limo” profile is better than looking at a progress bar for 30 seconds and getting told the domain could not be found.

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 nick.eth

; <<>> DiG 9.16.37-Debian <<>> @localhost nick.eth
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33364
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

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

;; ANSWER SECTION:
nick.eth.		60	IN	CNAME	ens.domains.

;; Query time: 703 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Apr 29 22:12:47 UTC 2023
;; MSG SIZE  rcvd: 62
$ docker exec -it ens-dns dig @localhost foo.eth

; <<>> DiG 9.16.37-Debian <<>> @localhost foo.eth
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 31278
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

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

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

;; Query time: 711 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Apr 29 22:13:44 UTC 2023
;; MSG SIZE  rcvd: 62

Thanks

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.