- Go 86.4%
- Nix 11.3%
- Just 2.3%
| cmd | ||
| dev | ||
| internal | ||
| .air.toml | ||
| .env | ||
| .envrc | ||
| .gitignore | ||
| compose.yaml | ||
| default.nix | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| justfile | ||
| README.md | ||
| result | ||
[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
- reverse-ish DANE-like? (https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities)
- 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 toLatencyRecordStore /zones/awa/hi/IN/CNAME/$LOC_ID=>$SERIAL us.srv.awa.- if none exist, fall back to
SRV _latency._dansewhere top record wins
- danse-routine:
- periodically, as well as on-demand/just-in-time, index the entire zone set for
_latencyor_failovertype SRV records /probes/awa/hi/$LOC_ID=>$SERIAL- primarily looking at updatedAt fields in S3- dispatch into a queue
- periodically, as well as on-demand/just-in-time, index the entire zone set for
- says:
CNAME us.srv.awa.
- M2: ANAME changes similar to above
- only difference is ANAME flattening
- M1: switch a CNAME's value around
-
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/AAAAflattener- 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 idTLSA- 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 records3600 fd41:d011:dead::443
/zones/pet/blood/awawa/IN/CNAME- awawa.blood.pet CNAME3600 dead.dolly.sh
/zones/internal/danse/_keys/$keyfp/roles/IN/TXT-TXT roles.$keyfp._keys.danse.internalproves authz for a given mTLS public key.zones:read, zones:modify, zones:provision, user:provision- like an adminzones:readwould always grant thisreadpermission for all zones, like for listing everything.zones:modifyallows the user to modify any zone, useful for helping users update their certs, and updating this zone.zones:provisionallows a user to create new zones. normal users may get this too.user:provisionallows for creating new user certificate entries like this one.
- empty, like a normal userzones: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.