domains
Domains and DNS of the instance's teams in one panel, connected to the services that serve them. Host:
domains.<d>. Binary: domainsd. After stage 5 (apps need domains too; a second node carries a second
name server).
Providers
- Cloudflare first: a team connects an account with an API token (zone read and DNS edit); its zones
and records show up, and records are created and changed through the API.
- Own name servers later: an authoritative DNS server in Go (miekg/dns) on two nodes, for teams that
want no third party. Needs two addresses on different nodes and glue records at the registrar; the
panel says what to set.
Concepts
- Zone: a domain a team manages, with its provider and records (A, AAAA, CNAME, TXT, MX, CAA, SRV).
- Attachment: a host name pointed at a service: a pages alias, an app, a status page, a team's
share links, a tunnel.
Attaching creates the record (CNAME to the service host, or A/AAAA), waits until it resolves, and asks the service to
verify and serve it with a certificate. pages' custom domains and apps' domains use this instead of
telling people which records to set.
- Import: a team picks zones of a connected account; existing records stay and are shown read-only
until edited here.
Not here
Cloudflare's Workers, R2 buckets, page rules and the like aren't mirrored: R2 is reached as S3 (backups
destinations), the rest stays in Cloudflare's dashboard.
Events
domain.attached, domain.detached, record.changed, zone.unreachable (the provider's token stopped
working).
As built (stage 5b)
- Accounts: a team's admin connects a Cloudflare account with an API token (Zone:Read, DNS:Edit),
checked with Cloudflare first and sealed with
DATA_DIR/seal.key; it never comes back out. A token
Cloudflare later refuses marks the account and sends zone.unreachable once.
- Zones: the admin picks which of the account's zones are managed here (one team per domain on an
instance). Records are read live from Cloudflare and changed through it (A, AAAA, CNAME, TXT, MX, NS)
by the team's developers; records an attachment made are shown as such and change only through it.
- Attachments: a developer of the zone's team points a name at a pages alias or a web app of the
same team (deploy rights there too; admins for protected environments). domainsd asks the service
about the target with its own token, registers the name with it (pages: its custom domain, whose TXT
ownership record domainsd makes too; apps: a custom endpoint on the app; status: the page's host name),
creates the A record to the
instance (DNS only, so Caddy can issue the certificate) and checks until the name resolves through a
public resolver and answers over HTTPS:
domain.attached. Detaching takes the name off the service
and removes the records.
- Front: the manifest's
front makes domainsd Caddy's catch-all with on-demand TLS (xvii-node gives
a front precedence over a service that only asks). It approves certificates for attached names and
asks pages about the rest (pages' own custom domains keep working), and relays requests by name to
pages, apps or status on this server. Other panels open the Attach dialog filled in with
/names?new&attach=<kind>:<id>&team=<team>.
- Own name servers (provider
xvii): a team's admin adds a domain on xvii's name servers; its
records live in domains' database and each change moves the zone's serial on. Every name server is a
server running xvii-node dns https://domains.<d> TOKEN (the token from the Accounts page, superusers;
a new one stops the old): xvii-node pulls GET /api/v1/dns/zones every five seconds (ETag, so
unchanged zones cost a 304), keeps the last copy on disk and answers authoritatively on port 53 of the
server's public addresses (systemd-resolved keeps 127.0.0.53): SOA, NS, wildcards, CNAMEs inside the
zone, NXDOMAIN; anything else is refused, AXFR too. The instance's name servers are domains'
NAMESERVERS setting (for example ns1.example.com on the control plane, ns2.example.com on a
second node). A zone stays pending until its registrar points at them (checked every ten minutes,
zone.delegated). Records and attachments work the same as on Cloudflare.
- DNSSEC (own zones, the zone's DNSSEC button, admins;
PUT /api/v1/zones/{id}/dnssec {on},
zone.dnssec): domains makes the zone a key (ECDSA P-256, algorithm 13, one key in both roles; the
private half sealed) and hands it to the name servers with the zone (key, public_key). They sign as
they answer, for askers that set DO: an RRSIG for each RRset (a week long, made again with three days
left, kept in memory), the DNSKEY at the apex, and for an empty answer an NSEC at the name asked that
lists what it has; a name that doesn't exist answers as one with nothing (no NXDOMAIN, so the zone's
names can't be walked). The panel shows the DS record (key tag, algorithm, digest type 2, digest) and
the DNSKEY for the registrar; every ten minutes domains asks a public resolver for the zone's DS:
waiting until there is one, active when it names the key (zone.signed), mismatch when it names
another (zone.dnssec_mismatch, critical: validating resolvers refuse the domain). Turning it off is
refused while the registrar still publishes a DS.
- Names of apps on edge nodes: a name attached to an app points at the app's node when that node is
an edge that serves (apps'
GET /api/v1/apps/{id}/address), else at the control plane; domains checks
every two minutes, so a name follows an app that moves or a node that changes mode.
- Moving off Cloudflare: a Cloudflare domain's admin moves it onto xvii's name servers
(
POST /api/v1/zones/{id}/move): its records are copied (proxied ones become plain), the zone and its
attachments stay, and it is pending until the registrar points at xvii's name servers; Cloudflare answers
as before until then and keeps its copy for the owner to remove.
A domain in the same registry as the instance's own (Identity Digital runs both .network and .land)
takes ns1.<d> only once those are registered with their addresses (personal name servers / glue)
at the instance domain's registrar; other registries take them as they are.
- Cache and protection (a name's shield button,
GET|PATCH /api/v1/attachments/{host}/guard
{cache, rate, conns, attack, bans}, domain.guard): kit/httpcache keeps what the site says may be
shared (public, max-age, s-maxage, at most a day) and static files it says nothing about for an
hour, never a response with a cookie, private/no-store, a request with credentials or a Vary other
than encoding (X-Cache: HIT|MISS; POST …/purge drops a name's copies, domain.purge). kit/shield
limits requests a second from one address (a burst of five times as many) and requests at once, bans
addresses and networks, holds an address back for ten minutes after a minute over the rate
(DELETE …/held/{ip} lets it through), and under attack sends browsers through a proof of work
(/.xvii/check, a cookie for the address for a day). The front applies them to the names it serves;
for an app the guard goes to apps too (PUT /api/v1/apps/{id}/domains/{host}), and an edge node applies
it itself. Counters are per process and start again with it. For an app on an edge node the panel
reads the counters from the node that actually serves the name (POST /api/v1/apps/{id}/guard-state,
relayed to /_xvii/guard, domains only) and releases a held address there; the dialog says whether
numbers come from the front or the edge, and shows them as unavailable when the node can't be read.
- MCP tools
domains_list_zones, domains_list_records, domains_add_record, domains_list_attachments,
domains_attach, domains_detach; limits domains.zones, domains.attachments.
Next: key rollover for signed zones.
- Services with things of their own take names the same way: status pages, a team's share links,
shared analytics dashboards, webhook endpoints and reserved tunnel names (
<base>?team=,
<base>/<id>, <base>/<id>/domains), and mail's set-up hosts (hosts in mail-records).