Skip to main content

Querying the Ethereum Name Service from DNS

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

(Also, this article was written before The Merge and using Geth is not as straightforward anymore as shown in here.)

Introduction

The goal of this article is to show how one, with a little extra code, can query a DNS server for an Ethereum Name Service (ENS) domain and get a wallet address back.

The DNS server we are going to use, dnsdist, is actually a DNS load balancer (see What is dnsdist?). However for a client querying, it looks and acts the same as a regular DNS server and for the purpose of this blog it also does not matter. It’s just that I think dnsdist is the best place to implement this feature. Peter van Dijk, a colleague of mine, argued that an authoritative server might be a better place. Perhaps. According to him it would take a little more code but as an additional benefit the feature would be multi-threaded (at least for the PowerDNS Authoritative Server).

Now, let me first scare some fundamental anti-cryptocurrency people away: ENS stores its information on the Ethereum blockchain and ENS domains are NFTs.

As opposed to 1 ETH which you can switch for another 1 ETH and still be able to use it the same way, an NFT is unique and cannot be traded for exactly the same one.

Still here?

Cryptocurrency is a very divisive subject. It is a new technology (looking for a problem according to some) that also involves lots of money, so naturally it attracts many scams and seedy practices. Besides that there’s also the environmental footprint of Bitcoin and Ethereum. No wonder one can find a great number of passionate people for and against cryptocurrencies.

This blog post sums up the current state of affairs quite well: https://modelcitizen.substack.com/p/is-crypto-bullshit

Disclaimers:

  • I am an Open-Xchange / PowerDNS employee (the company making dnsdist and PowerDNS Authoritative Server) but this article is not endorsed by them.

  • I also own some cryptocurrencies (unfortunately I still need a full time job, disproving the myth that it is magical internet money).

  • Since two weeks ago I also own ENS tokens due to a distribution they did to ENS name holders. I was not aware of their plans when I started this evening project of querying ENS from dnsdist.

What is dnsdist?

dnsdist is a highly DNS-, DoS- and abuse-aware load balancer. Its goal in life is to route traffic to the best server, delivering top performance to legitimate users while shunting or blocking abusive traffic.

dnsdist is dynamic, its configuration language is Lua, it can be changed at runtime and its statistics can be queried from a console-like interface or an HTTP API.

From: https://dnsdist.org/

What is ENS?

The Ethereum Name Service (ENS) is a distributed, open and extensible naming system based on the Ethereum blockchain.

ENS’s job is to map human-readable names like ‘alice.eth’ to machine-readable identifiers such as Ethereum addresses, other cryptocurrency addresses, content hashes and metadata. ENS also supports ‘reverse resolution’, making it possible to associate metadata such as canonical names or interface descriptions with Ethereum addresses.

From: https://docs.ens.domains/

The Idea

As you can gather from the introduction there’s an overlap between DNS and ENS. They more or less fullfill the same function but for different platforms. DNS for the internet as we know it and ENS for the Ethereum blockchain.

What if we could make it easier to reach into the ENS space from DNS using existing tools? This way little changes would be needed at major traffic points (ISPs, telcos) while reaping the functionality that ENS provides.

That is what this proof-of-concept sets out to do.

The Goals

  1. Query a DNS server (dnsdist) with an ENS domain and get a wallet address back,
  2. Query a DNS server with a wallet address and get an ENS domain back,
  3. Query a DNS server with an ENS domain for specific metadata and get the requested information back.

Spoiler: we only reach the first goal in this blog post, both to keep it basic and because I need to spend my evenings on Diablo 2 Resurrected again (which runs more stable on my Linux machine than on my Windows gaming laptop!).

A Failed Approach: Querying Go-Ethereum Directly from dnsdist

Since I prefer to keep dependencies to a minimum I initially resolved to load ensutils.js into Go Ethereum and call getAddr through IPC from Lua. This started promising playing with geth from the commandline but ran into troubles when connecting to it from Lua.

I ran into the following issues:

  • geth is running an old web3 implementation which does not have ENS functions (hence loading ensutils.js),
  • New JSON-RPC endpoints cannot be added from JavaScript,
  • To get around the previous issues we’d have to implement functionality in Lua that is already available in existing Ethereum libraries,
  • Calling with Lua FFI into Go was also briefly explored.

A solution hack that I did not want to explore because it is just too nasty is running a geth attach command from Lua, just sending commands through standard output and getting responses back through standard input. I’m pretty sure it would have worked (for this PoC) but I would not have wanted to blog about it.

A Successful Approach: Query an ENS Library

Since the previous approach is not viable at this moment in time, I decided on putting an ENS library in between dnsdist and Go Ethereum to act as a proxy. After some superficial research I decided to use the Ethers JavaScript library on NodeJS, because of the clear and working examples.

Additionally I wanted to use a JSON-RPC library so the existing Lua code from the failed approach above can be used and also because the same code can query Go Ethereum. The http-json-server library was picked using the same criteria as above.

Dependencies

The specific versions used for developing this PoC are listed below. The assumption is that everything should still work if the versions are not too different. Patch releases are definitely supposed to work, other minor versions ought to work as well.

  • Go Ethereum v1.10.11
  • Lua v5.4.3
  • NodeJS v16.9.1
  • dnsdist v1.6.1

Running Go Ethereum

Running Go Ethereum is very simple, except you need to make sure you use a recent version, otherwise you cannot sync with the blockchain. The version that came with my Linux distribution (Void Linux) was too old.

Best to download it directly from the source:

Go Ethereum will be run as a light client. A light client does not download the whole blockchain but uses other peers to retrieve information. Unless you’re already running a synced Ethereum blockchain or if you have several days to sync up to it, I would advise to run as a light client.

There’s on more thing you can do to improve your sync1 speed as a light client and that is manually adding peers: https://medium.com/@rauljordan/a-primer-on-ethereum-blockchain-light-clients-f3cadde49137

Now run Go Ethereum: geth --syncmode "light"

The Geth Proxy

(see geth-proxy.js for the complete listing)

Install the Ethers and http-jsonrpc-server libraries:

  • npm install ethers
  • npm install http-jsonrpc-server

We make a wrapper around Ethers’ provider.resolveName function because we cannot directly feed it the incoming JSON-RPC request and also because it will be easier to add conversions and error checking to the incoming and outgoing values later:

async function resolveName (name) {
  console.log('Resolving ' + name + '...');
  address = await provider.resolveName(name[0]);
  console.log('  - ' + address);
  return address;
}

Provided geth is running with defaults the code connects to it as follows:

const provider = new ethers.providers.IpcProvider(os.homedir() + '/.ethereum/geth.ipc');

We add resolveName as a new JSON-RPC endpoint and start running the JSON-RPC server:

rpcServer.setMethod('resolveName', resolveName);

rpcServer.listen(9090, '127.0.0.1').then(() => {
  console.log('Geth Proxy is listening at http://127.0.0.1:9090/')
});

Run the Geth Proxy with: node geth-proxy.js

Lua ENS Functions for dnsdist

(see resolve-ens.lua for the complete listing)

Install the following libraries first because the Lua code depends on it:

  • luarocks install --local luasocket
  • luarocks install --local lua-cjson2

dnsdist has Lua as an extension language, so we need to call our new JSON-RPC endpoint from Lua. The Lua code has two functions:

  • lua_ens: called from dnsdist to handle all queries for domains ending in “.eth”,
  • lua_resolve_ens_name: called from lua_ens to call the Geth Proxy resolveName JSON-RPC endpoint.

lua_ens converts a dnsdist DNSQuestion.qname into an ENS name. This just involves chopping off the trailing dot from the qname. It then calls lua_resolve_ens_name using this new name.

If it gets a null response back from the Geth Proxy it returns a DNS NXDOMAIN (nonexistent domain) response, otherwise it will return the wallet address as a CNAME (domain alias).

There is an important issue with the code in that it also returns an NXDOMAIN if Go Ethereum is still syncing (while the domain might exist!). This could be fixed by first checking whether geth is still syncing.

function lua_ens (dq)
    local qname = tostring(dq.qname)
    local name = string.sub(qname, 0, string.len(qname) - 1)
    print('• request for *.eth domain: ' .. name)
    local ens_name = lua_resolve_ens_name(name)
    if ens_name == cjson.null then
        print('  - domain does not exist in ENS (or geth is still syncing)')
        return DNSAction.Nxdomain
    else
        print('  - address received from ENS: ' .. tostring(ens_name))
        return DNSAction.Spoof, ens_name
    end
end

lua_resolve_ens_name is unfortunate and I will not reproduce it here. I wanted to use a Lua HTTP library but ran into an issue with Lua 5.4.3 where HTTP calls did not work. So we just use raw sockets. Just try to ignore this function and pretend a proper HTTP library is used.

(This is all because I installed dnsdist with my distribution’s package manager and that dnsdist is using Lua 5.4.3.)

Configuring dnsdist

(see dnsdist.conf for the complete listing)

Besides some standard setup we also load the Lua ENS functions and add a new action to dnsdist.conf:

require('resolve-ens')

addAction({'eth.'}, LuaAction(lua_ens))

This means that for all *.eth domains the lua_ens function will be called to handle the query.

Run dnsdist: dnsdist --config dnsdist.conf

Querying dnsdist

We can now query dnsdist: dig @localhost -p 5200 vitalik.eth and we either get the wallet address back as a CNAME or we get an NXDOMAIN.

; <<>> DiG 9.16.22 <<>> @localhost -p 5200 vitalik.eth
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27276
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

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

;; ANSWER SECTION:
vitalik.eth.		60	IN	CNAME	0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045.

;; Query time: 307 msec
;; SERVER: 127.0.0.1#5200(127.0.0.1)
;; WHEN: Mon Nov 22 16:06:41 CET 2021
;; MSG SIZE  rcvd: 96

Necessary Improvement

The PoC is done: the concept has been proven.

Before we SSH these files over to production let us first look at a necessary change:

As the dig example above shows, the A type DNS query is the default. While this might be defendable for ENS names that look just like DNS domains the CNAME result we get back is already not directly usable since a dot is appended to it. Moreover, if we want to request other data from ENS this is unworkable.

An improvement would be to return TXT records so we can return arbitrary text as a result.

The dig request for an Ethereum address would become: dig @localhost -p 5200 -t txt vitalik.eth

And the response:

; <<>> DiG 9.16.22 <<>> @localhost -p 5200 -t txt vitalik.eth
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53767
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;vitalik.eth.			IN	TXT

;; ANSWER SECTION:
vitalik.eth.		60	IN	TXT	"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

;; Query time: 663 msec
;; SERVER: 127.0.0.1#5200(127.0.0.1)
;; WHEN: Tue Nov 23 14:04:49 CET 2021
;; MSG SIZE  rcvd: 84

By querying for virtual subdomains we can have dnsdist filter on them and get information from ENS like a BTC address, website, Twitter or GitHub URL. For example:

$ dig @localhost -p 5200 -t txt _ens_url.vitalik.eth

; <<>> DiG 9.16.22 <<>> @localhost -p 5200 -t txt _ens_url.vitalik.eth
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53767
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;_ens_url.vitalik.eth.			IN	TXT

;; ANSWER SECTION:
_ens_url.vitalik.eth.		60	IN	TXT	"https://vitalik.ca/"

;; Query time: 663 msec
;; SERVER: 127.0.0.1#5200(127.0.0.1)
;; WHEN: Tue Nov 23 14:04:49 CET 2021
;; MSG SIZE  rcvd: 84

Another option would be to just return all metadata the ENS has on a name, but these are all just unexplored ramblings.

Goodbye

I hope you had fun reading this and thanks for your time.

Erik Winkels (aerique.eth if you want to have a chat on XMTP)

Footnotes


  1. Even a light client has to sync some data before it can be operational. ↩︎