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:
- Part 3: DRAFT: An Authoritative Server for .eth
- Part 2: Cutting Out the Middle Man
- Part 1: Querying the Ethereum Name Service from DNS
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:
-
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.
-
It is defined in pdns.conf how the auth can connect to the remote backend.
-
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()
):
- if the contenthash is not empty and starts with
ipfs://
it will return a link through an IPFS gateway, - 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, - 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:
- 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), - 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,
- IPFS:
- check the ENS
cname
text field and return it as a CNAME record, - 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.