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
- Query a DNS server (dnsdist) with an ENS domain and get a wallet address back,
- Query a DNS server with a wallet address and get an ENS domain back,
- 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 loadingensutils.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 fromlua_ens
to call the Geth ProxyresolveName
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)
- code: https://gitlab.com/users/aerique/projects
- e-mail: aerique@xs4all.nl
- www: https://www.aerique.net/
Twitter: https://twitter.com/aerique
Footnotes
-
Even a light client has to sync some data before it can be operational. ↩︎