Skip to content

DNS & .test Domains

Open http://blog.test and something has to turn that name into an address your machine listens on. Yerd handles this with a small embedded DNS resolver that answers for one TLD (.test by default), plus a one-time change that points your OS at it.

The short version

  • The yerdd daemon runs an embedded DNS server. It answers *.test with 127.0.0.1 (A) and ::1 (AAAA).
  • You tell your OS to send .test lookups there, once: sudo yerd elevate resolver.
  • After that it's automatic. Park or link a project and <name>.test resolves to loopback, where Yerd's reverse proxy is waiting.

Why .test?

.test is reserved (RFC 6761) and can never exist on the public internet, so routing it to your machine can't shadow a real site. The TLD is configurable; see Configuration Reference.

The embedded resolver

The resolver lives in the yerd-dns crate, built on hickory-dns. It is authoritative for exactly one zone (your TLD) and nothing else: it never forwards, caches, or touches the network.

QueryWire response
app.test, ANOERROR, A 127.0.0.1
app.test, AAAANOERROR, AAAA ::1
app.test, any other qtype (MX, TXT, …)NOERROR, empty answer
test (bare TLD apex)NOERROR, empty answer
malformed in-zone label (.test, app..test)authoritative NXDOMAIN
outside the zone (app.com, testify)REFUSED, AA bit cleared

Worth knowing:

  • Any subdomain depth resolves. a.b.c.app.test maps to loopback just like app.test, and matching is case-insensitive.
  • Records carry a 60-second TTL (ANSWER_TTL_SECS), so resolvers don't cling to stale answers when your setup changes.
  • Out-of-zone queries return REFUSED with the AA bit cleared. If your OS ever misroutes a non-.test query here, the cleared bit tells it to ask elsewhere instead of trusting a bogus answer.
  • No SOA is emitted in the authority section of NXDOMAIN/NODATA replies.

Read-only by design

The DNS server holds no logic about your sites. It says "that's loopback" for the whole zone; routing to the right project and PHP version happens later in the reverse proxy. See Sites and The Daemon.

How the daemon binds it

The daemon binds the resolver on 127.0.0.1 at dns_port from yerd.toml. The default is 1053 (DEFAULT_DNS_PORT): not the mDNS-contended 5353, and not privileged 53, so the unprivileged daemon can bind it.

UDP and TCP both bind on the same address. Set dns_port = 0 and the kernel picks an ephemeral port, retrying until UDP and TCP match. A fixed port is better, though, since the resolver config your OS stores hard-codes 127.0.0.1:<port> and a stable port keeps that file valid across restarts.

Keep the port stable

If dns_port changes (or you used 0), the config written by yerd elevate resolver may point at a port nothing listens on. yerd doctor catches this by checking the config against the daemon's current DNS address. Fix it by re-running sudo yerd elevate resolver.

Pointing the OS at Yerd

Configuring the OS resolver needs root, so the auditable yerd-helper binary does it, once:

sh
sudo yerd elevate resolver

This is one slice of the one-time setup. The full command also does CA trust and port capability:

sh
sudo yerd elevate            # trust + resolver + ports, in one go

Daily use never touches root again. To undo it:

sh
sudo yerd unelevate resolver

On macOS, undo is a restore: if a previous resolver was backed up when Yerd took over (see below), unelevate resolver puts it back and clears the saved backups; with no backup it removes Yerd's file. On Linux it removes the systemd-resolved drop-in.

Install and uninstall are idempotent: re-installing an already-correct config changes nothing (and writes no backup), and uninstalling something absent succeeds quietly.

See Elevation & Privileges for the privilege model and the CLI Reference for every flag.

Per-platform mechanism

PlatformMechanismFile writtenReload
macOSresolver(5) per-TLD file/etc/resolver/<tld>None (read at next query)
Linux (systemd-resolved)drop-in config/etc/systemd/resolved.conf.d/yerd-<tld>.confsystemctl reload-or-restart systemd-resolved
Linux (no systemd-resolved)refused (see below)--
WindowsNRPT rule-planned

macOS

Yerd writes a file named after the TLD (e.g. /etc/resolver/test) with two directives:

text
nameserver 127.0.0.1
port 1053

macOS reads it at the next query, so there's nothing to reload.

If a file already exists there (a leftover from Valet, Herd, or older Yerd) and doesn't point at Yerd, the helper backs it up before overwriting. Backups land in /Library/Application Support/io.yerd.Yerd/resolver-backups/ as <tld>-<unix-seconds>.conf, and yerd doctor reports the most recent. An already-correct file is left alone.

sudo yerd unelevate resolver reverses this: it restores the most recent backup over /etc/resolver/<tld>, then deletes the saved backups. The restore is guarded - the helper only writes back a backup that is root-owned, not a symlink, and parses as a valid resolver file - so a tampered or junk backup is skipped in favour of a plain removal.

The port line is load-bearing

A bare nameserver 127.0.0.1 (what Valet/Herd leave) defaults to port 53, where nothing of Yerd's listens. The is_installed probe requires both nameserver and port to match the live daemon, so a stale file reads as "not installed" and gets rewritten on the next elevate.

Linux - systemd-resolved drop-in

On systemd-resolved systems Yerd writes /etc/systemd/resolved.conf.d/yerd-<tld>.conf:

ini
[Resolve]
DNS=127.0.0.1:1053
Domains=~test

The ~ prefix is systemd's routing-only marker: use this server only for names under test, without making Yerd the global resolver. The helper then runs:

sh
systemctl reload-or-restart systemd-resolved

Detection is conservative. Yerd treats systemd-resolved as in charge only if /run/systemd/resolve exists, or /etc/resolv.conf carries the systemd-resolved marker in its first few lines.

The Linux is_installed probe is shape-based: a well-formed drop-in for your TLD counts as wired up. Because resolved manages forwarding internally, the address inside the file isn't re-verified the way macOS does.

No safe automatic path without systemd-resolved

Without systemd-resolved, yerd elevate resolver refuses rather than edit /etc/resolv.conf directly. On many distros that file is rewritten by NetworkManager, resolvconf, or cloud-init, so a hand-edit would be clobbered. Either enable systemd-resolved, or reach sites through plain localhost without .test resolution at all - see Localhost Access (No Resolver).

Windows - NRPT (planned)

Windows is on the roadmap. The plan routes .test via the Name Resolution Policy Table (NRPT), Windows' native per-suffix mechanism, alongside named-pipe IPC and the system certificate store. Not implemented today; on unsupported platforms the resolver operations return an Unsupported error.

Verifying it works

yerd status shows the DNS address the daemon bound and whether the OS points at it:

sh
yerd status
# …
# dns       127.0.0.1:1053

When resolution breaks, yerd doctor flags it and names the fix (usually sudo yerd elevate resolver):

sh
yerd doctor
yerd doctor fix     # auto-repairs the safe ones

You can query the resolver directly. It's authoritative for the whole zone, so any .test name returns loopback:

sh
# Ask Yerd's resolver on its port:
dig @127.0.0.1 -p 1053 anything.test A
#   anything.test.   60   IN   A   127.0.0.1

dig @127.0.0.1 -p 1053 anything.test AAAA
#   anything.test.   60   IN   AAAA   ::1

To confirm the OS routes .test (not just Yerd's port): on macOS, dscacheutil -q host -a name app.test returns 127.0.0.1; on Linux with systemd-resolved, resolvectl query app.test shows the answer from 127.0.0.1:1053.

Source: github.com/forjedio/yerd.

A Forjed project. Released under the MIT License.