[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:
- talk about ensjs’s
getProfile
and returningTXT
records for all returned items - show dns-test.aerique.eth
cname
field example
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:
-
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 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')
):
- if the URL field has not been filled it falls back to returning the “eth.limo” address of the ENS name,
- 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,
- for example, “nick.eth” has https://ens.domains/ as URL which only has
a host part since nothing comes after the last “/”, so
- if the URL field also has a path it again falls back to returning the
“eth.limo” address of the ENS name
- for example, “foo.eth” has https://twitter.com/foodoteth as URL which
has “/foodoteth” as path, so
foo.eth.limo.
is returned.
- for example, “foo.eth” has https://twitter.com/foodoteth as URL which
has “/foodoteth” as path, so
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:
- 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),
- possibly check the contenthash field but on a superficial reading it looks like meaningful domain names cannot be extracted from it,
- 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 returningtwitter.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,
- “nick.eth” has https://ens.domains/ in the URL field so we return
- 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.