zeronsd: Unicast DNS resolution for ZeroTier, now in alpha

Erik Hollensbe
May 6, 2021

TLDR: zeronsd is a new, standalone unicast DNS server that integrates with ZeroTier Central. You can find it here. It is currently in alpha testing; please read on if this is interesting!

Some of you know that ZeroTier has a good layer 2 story already, allowing for things like Multicast DNS to function transparently on your network without any changes. For those of you who don’t want to click through, this is what resolves networks on your Macs and Windows operating systems to DNS names on the .local network. Linux/BSD users can use Avahi to achieve the same effect.

There are a lot of things that Multicast DNS doesn’t do well: reliability and the notion of the record only existing when the machine is online are two easily-found fruits we can pick up off the floor, as they are an immediate, present problem for any production environment. If your DNS record no longer exists because the machine rebooted/is off/caught fire, the resolving host is going to respond to it very differently (and much slower) than if the record exists and the machine is offline. Likewise, if the link is slow or unresponsive (as can be the case with hundreds of computers behaving like a LAN across the globe), the DNS record will cease to exist, temporarily. That’s bad, yo.

So, while layer 2 LLMNR and mDNS definitely fill a void in your use of ZeroTier, they do not solve a lot of problems with larger networks, leaving people to resort to those pesky multi-octet beasts we call IP addresses.

A while back, I discovered the trust-dns toolkit in Rust; after having made my own DNS service in Golang for my home network, I found the trust-dns API to be quite refreshing and easy to use, as well as quite performant; there are numbers late in the article.

zeronsd provides unicast resolution for ZeroTier networks

zeronsd was the product of that experimentation, and provides a number of features you may appreciate as a ZeroTier user:

Features

  • One network, one service. No magic multi-network beast to manage.

  • Easy command-line usage.

  • Auto-detection of listening IP.

  • Auto-configuration of DNS resolver in ZeroTier Central.

  • Provides a TLD. You can map subdomains just by partitioning them with a dot.

  •  Optionally parses additional /etc/hosts formatted names and addresses into this TLD.

  • Forwards all other queries to /etc/resolv.conf resolvers, allow you to look up your TLD and google.com without needing two DNS servers.

  • Portable to many operating systems and architectures by way of rust-lang.

  • Auto-detection of members list and population of:

zeronsd video

How it works

So, before I go into any of this; a recent development by our Central team was the porting of all API specs to OpenAPI 3, including a translation of the zerotier-one service API! You can’t give me tools like this, and expect me to not use them! 😀

So, what zeronsd more or less does, is poll the Central API periodically and populate records in what trust-dns calls a “Catalog” and then proceeds to let trust-dns do most of the work on its own. The code footprint is quite small, and once initialized there is only one supervisory routine running (the record refresher) that does not belong to trust-dns’s crate.

To configure the listening information, we talk directly to ZeroTierOne over the privileged API, which is at localhost:9993 and ask it to tell us its IP addresses for that network. Easy sauce.

Usage

First, to get maximum benefit, ensure your users of the networks have the Manage DNS flag turned on; this will be important as your DNS server comes live.

Generate an API token. You will need to put this in a file, or in the environment, so keep it handy.

Create a network. Make note of the Network ID: the long hexadecimal number at the top of the network’s page. You will need this to pass to zeronsd.

Finally, obtain a rust environment, so you can build & install zeronsd.

Installation

The latest release can be found this way:

cargo install zeronsd

If you want to run the bleeding edge, try:

cargo install --git https://github.com/zerotier/zeronsd --branch main

Running

NOTE: If you are on OS X, Windows, or Linux your zerotier-one credentials will be automatically detected. Otherwise, (e.g., FreeBSD), you will need to pass a special flag to point at the authtoken.secret file in your ZeroTier installation.

Finally, you must run zeronsd as root. While I would like it differently, plenty of resolvers will simply not work over anything but port 53, making root pretty much a requirement on any POSIX system.

Put your API token in a file named zerotier-central-token and run this:

sudo zeronsd start -t zerotier-central-token

If you prefer, you can also pass ZEROTIER_CENTRAL_TOKEN in the environment. One of these, however, is required for the daemon to start. After that, you should see something like this:

Welcome to ZeroNS! Your IP for this network: 172.27.207.100

Your IP will of course be different. 🙂 It should correspond to the IP listed in zerotier-cli listnetworks for the network you’ve provided. Your DNS settings on the Central API will be updated, and all clients will start resolving ASAP to your DNS server.

Querying

In my case, I have the following members configured:

This allows for the following records:

% host zt-09ea1c84bd.domain 172.27.207.100
zt-09ea1c84bd.domain has address 172.27.67.27

% host test4.domain 172.27.207.100
test4.domain has address 172.27.67.27

% host 172.27.67.27 172.27.207.100
27.67.27.172.in-addr.arpa domain name pointer test4.domain.

Options

There are a few options you can use to modify the behavior a bit. Notably:

-d lets you adjust the TLD, which defaults to domain. Set it to any dotted combination you can think of.

**-f **lets you add a hosts file that appends static records to the TLD. It uses /etc/hosts format, e.g.:

# <address> <host1> <host2> ...
10.0.0.1 home-router netgear-thingy
10.0.0.2 super-secret-home-server

This will result in home-router.domain pointing to 10.0.0.1. No PTRs are generated (yet).

Things to Improve

Well, nothing’s perfect.

DoT/DoH support

Does not exist yet. trust-dns supports this, so this is earmarked but we are thinking through ways to distribute certificate authorities, which is critical for these features. Stay tuned!

IPv6, 6plane, RFC4193 support

If you are a heavy IPv6 user, we could really use your help diagnosing quality-of-life issues with these features. Notably, PTR records do not work for any of these situations.

Performance

As mentioned prior, trust-dns performs quite well, and it really does most of the work once the service is configured. We can see this in performance numbers.

The benchmarking environment is slipshod; it is mostly here to say “this will perform better than mDNS”, not “OMFG THIS IS THE FASTEST DNS SERVER ON EARTH”. That said, I think we should all be comforted that this will not be your problem with this service, is my best guess.

I forked a copy of dnsbench because that was simpler than downgrading golang. Try it out!

This is running on two local nodes, so no intermediary services, just two copies of ZeroTier having a conversation. The requesting node is a 6 core, 12 thread i7-8700K mostly unloaded, running 100 threads for a million queries in total. The serving machine is a Ryzen 2800X with 8 cores and 16 threads, running a moderate workload. zeronsd RSS usage never went beyond 10MB at the time of this benchmark.

As I think we can all see here, you’re gonna be fine.

$ export HOSTS="seafile.zerotier\ngoogle.com\n"
$ export SERVER=10.147.19.234
$ dnsbench run remote -c 100 -n 1000000 -f <(echo "$HOSTS") "$SERVER"
Reading names from /proc/self/fd/11
Benchmarking 10.147.19.234...

# requests errors min  [ p50  p95  p99  p999] max  qps
  76424       0   1.02    [6.02 9.72 12.41 23.77] 23.77  15284.80
  75669       0   1.49    [6.18 9.99 13.74 32.37] 32.37  15133.80
  76180       0   1.48    [6.19 9.85 13.03 22.94] 22.94  15236.00
  76347       0   1.33    [6.14 9.64 12.59 34.90] 34.90  15269.40
  72827       0   1.46    [6.33 10.78 16.74 35.49] 35.49  14565.40
  77493       0   1.52    [6.06 9.66 13.27 28.49] 28.49  15498.60
  76266       0   1.60    [6.04 9.95 13.66 36.54] 36.54  15253.20
  76729       0   1.44    [6.07 10.11 13.16 37.58] 37.58  15345.80
  76611       0   1.10    [6.13 9.89 13.07 27.33] 27.33  15322.20
  74124       0   1.34    [6.26 10.26 13.71 46.96] 46.96  14824.80
  76535       0   1.29    [6.11 9.67 14.08 38.14] 38.14  15307.00
  78195       0   1.32    [5.91 9.66 12.57 39.22] 39.22  15639.00
  80786       0   1.13    [5.85 9.40 11.44 30.08] 30.08  16157.20

Finished 1000000 requests

# latency summary
1000000       0   1.02    [6.09 9.87 13.26 46.96] 46.96  15266.87

Concurrency level: 100
Time taken for tests: 65.50 seconds
Completed Requests: 1000000
Failed Requests: 0
Requests per second: 15266.87 [#/sec] (mean)
Time per request: 6.37 [ms] (mean)
Fastest request: 1.02 [ms]
Slowest request: 46.96 [ms]

Thanks!

This was a lot of writing and thanks for getting this far! If you’re more interested in the project, feedback and patches are always great ways to contribute. Until next time!