doll authoritative name server (not french)
  • Go 86.4%
  • Nix 11.3%
  • Just 2.3%
Find a file
2026-06-24 15:21:21 -07:00
cmd normalization of storage 2026-06-23 23:32:52 -07:00
dev normalization of storage 2026-06-23 23:32:52 -07:00
internal mtls test: tweaks 2026-06-24 15:21:21 -07:00
.air.toml init 2026-06-19 20:31:19 -07:00
.env TLSA auth yayyy 2026-06-21 22:49:07 -07:00
.envrc s3 service, go init 2026-06-19 23:35:00 -07:00
.gitignore auth yayy 2026-06-21 20:41:28 -07:00
compose.yaml TLSA auth yayyy 2026-06-21 22:49:07 -07:00
default.nix update with design doc 2026-06-20 01:27:57 -07:00
flake.lock init 2026-06-19 20:31:19 -07:00
flake.nix TLSA auth yayyy 2026-06-21 22:49:07 -07:00
go.mod normalization of storage 2026-06-23 23:32:52 -07:00
go.sum normalization of storage 2026-06-23 23:32:52 -07:00
justfile TLSA auth yayyy 2026-06-21 22:49:07 -07:00
README.md fix readme typo 2026-06-23 23:35:36 -07:00
result update with design doc 2026-06-20 01:27:57 -07:00

[d]oll [a]uthoritative [n]ame[se]erver

a dns server for combat dolls

developing

# start the S3 backend
docker compose up -d
# Garage WebUI: http://localhost:3909/

# generate the initial certs
just gen-certs

# start the API server
nix run . -- danse-coordinator

# start the DNS server
nix run . -- danse-dns

# start the DNS prober
nix run . -- danse-routine

# run authenticated curl requests
USER1_FP=$(CERT=user1 just mcurl /auth/identify | jq -r .fingerprints.certificate)
CERT=root just mcurl /zones/blood.pet -X PUT -F "rp=41666" -F "fp=$USER1_FP"

.env setup

Start the danse-coordinator service

just mcurl /auth/identify | jq -r .fingerprints.certificate
# set this in .env => ROOT_CERT_FINGERPRINT
# restart the coordinator, and the generated root key will be root.

goals

  • DNS server itself - "DD", danse-dns ({5711,d011}.tasty.blood.pet)

    • DNS over UDP
    • DNS over TCP
    • DNS over TLS
    • DNS over HTTP(S)
    • DNS over JSON API (a la cloudflare)
    • reads from datastore
    • DNSSEC / TSIG
    • nsupdate support (write to datastore)
    • AXFR/IXFR(?) (write to datastore)
  • DNS coordinator API - "DC", danse-coordinator (tasty.blood.pet)

    • mTLS-based auth
    • zone-depth-level permissions
    • JSON CRUD for zones
      • maybe: reimplement PowerDNS API for fun
    • zone editor web UI
    • ANAME + root flattening
    • latency & uptime based records
      • M1: switch a CNAME's value around
        • asks: hi.awa CNAME
        • zone file equivalent:
            ;; TTL here is 600. after 600 seconds, we probe again.
            _latency._danse.hi.awa. 600 IN SRV 1 100 443 us.srv.awa.
            _latency._danse.hi.awa. 600 IN SRV 2 100 443 eu.srv.awa.
            _latency._danse.hi.awa. 600 IN SRV 3 100 443 mx.srv.awa.
          
            ;; TTL here is 3600. we still serve stale TTLs above until this expires.
            hi.awa. 3600 IN CNAME _latency._danse.hi.awa.
          
            _failover._danse.hi.awa. 600 IN SRV 1 100 443 us.srv.awa.
            _failover._danse.hi.awa. 600 IN SRV 2 100 443 eu.srv.awa.
            _failover._danse.hi.awa. 600 IN SRV 3 100 443 mx.srv.awa.
          
            ;; dummy CNAME we can detect and switch handling for.
            ;; we don't want to replicate this record's local value at all.
            hi.awa. 3600 IN CNAME _failover._danse.hi.awa. 
          
        • danse-coordinator queries:
          • $SERIAL is probably a unix timestamp. its monotonic.
          • $LOC_ID is a unique ID for the locality. like maybe one rack or one datacenter or one country idk
            • primarily, danse-routine will write to $LOC_ID endpoints. so coordinator and dns need to know which matters
          • /zones/awa/hi/IN/CNAME => $TTL _latency._danse.hi.awa.
          • we match ^_latency._danse => switch to LatencyRecordStore
          • /zones/awa/hi/IN/CNAME/$LOC_ID => $SERIAL us.srv.awa.
          • if none exist, fall back to SRV _latency._danse where top record wins
        • danse-routine:
          • periodically, as well as on-demand/just-in-time, index the entire zone set for _latency or _failover type SRV records
          • /probes/awa/hi/$LOC_ID => $SERIAL - primarily looking at updatedAt fields in S3
          • dispatch into a queue
        • says: CNAME us.srv.awa.
      • M2: ANAME changes similar to above
        • only difference is ANAME flattening
  • DNS record probe - "DR", danse-routine

    • probes ANAME targets if needed
    • probes a _danse SRVs for latency and uptime
    • updates datastore

design notes

custom/purposed DNS records

  • ANAME - A/AAAA flattener
    • if its a zone we already know, directly reference
    • if its a zone we don't know, reference via DR probe
  • RP - owning party's id
  • TLSA - see auth

auth

we do everything via mTLS. client certificate fingerprints are auth tokens.

we're taking the IRC "certfp" approach to authentication.

a client generates a self-signed cert, and its public key fingerprint OR cert hash is validated.

this is technically DANE - RFC6698, but its not interested in the SRV-like approach, which may conflict. it may also be awesome? who knows

when we're checking auth, the current client cert fingerprint is found via recursive resolution of the DNS TLSA records, most specific first.

example zone

for any given zone level, including ., imagine this zone file

$ORIGIN blood.pet.
$TTL 3600 ;; 1 hour

; top-level public key (can edit everything in blood.pet)
blood.pet.              IN TLSA (
  3 0 1 ;; a sha256(.cer) fingerprint...
)

; subdomain editor key (can only edit servers.blood.pet)
servers.blood.pet.      IN TLSA (
  3 0 1 ;; a sha256(.cer) fingerprint...
);

; so this can't be edited by be second key, as it isn't below its
; dns hierarchy
hi.blood.pet.           IN AAAA fd41:d011:ffff::

; but this is editable by both
5711.servers.blood.pet. IN AAAA fd41:d011:5711::1

; and if there's a new key needing to be added, just.. add it.
; server will accept both keys for the same permission set.
servers.blood.pet.      IN TLSA (
  3 0 1 ;; another sha256(.cer) fingerprint...
);

so TLSA gives us a good RRset to base our auth on. we won't respond to the public with these, but this does simplify the API quite a lot.

to use pubkey vs cert, flip middle digit 3 0 1 (cert) to 3 1 1 (pubkey). pubkey useful for long-term auth, as cert itself will expire.

to use a sha512, bump the final digit 3 0 1 (sha256) to 3 0 2 (sha512)

we won't support any other patterns currently. this may be useful, though as we could validate on a CA's signature.

data

coordinator is source of truth, locally referenceable via UNIX socket for latency

coordinator stores data in an S3 bucket.

replication is handled by the S3 server, like garage. its pretty good at cross-geo.

structures

all record types are 1 object; \n delimited.

we store zones in /zones/ using reverse-order DNS hierarchy, lowercased, so noe.sh becomes /zones/sh/noe

to actually say "this is where the RRset begins", we mark the path with IN, and the next path segment is the record type, AAAA

  • /zones/pet/blood/IN/AAAA - blood.pet AAAA records
    • 3600 fd41:d011:dead::443
  • /zones/pet/blood/awawa/IN/CNAME - awawa.blood.pet CNAME
    • 3600 dead.dolly.sh
  • /zones/internal/danse/_keys/$keyfp/roles/IN/TXT - TXT roles.$keyfp._keys.danse.internal proves authz for a given mTLS public key.
    • zones:read, zones:modify, zones:provision, user:provision - like an admin
      • zones:read would always grant this read permission for all zones, like for listing everything.
      • zones:modify allows the user to modify any zone, useful for helping users update their certs, and updating this zone.
      • zones:provision allows a user to create new zones. normal users may get this too.
      • user:provision allows for creating new user certificate entries like this one.
    • - empty, like a normal user
    • zones:read - like a monitoring bot
    • note: if this is missing, we reject the auth.
    • this is not necessary for internal danse services with direct S3 access.
      • in fact, direct S3 access for modifying data is totally fine.