Internal design
Every reply is untrusted input
hollow reads octets that arrived from the network before anything has authenticated them, and the zones it walks are controlled by whoever owns the name. So the structure is a strict downward graph with no cycles, work is bounded at every level, and the parts that decide what to believe are the ones with the most tests behind them.
Package graph
Imports only ever point downward
Eleven packages in four tiers, with no cycles and one interface in the whole tree. Select any package to read the decision it turns on.
Entry point
cmd/hollow + internal/cli
Six verbs, their flags, and every line of output.
Presentation only: no protocol decision is made here. flag.ContinueOnError with an explicit output means a bad flag returns an exit code instead of calling os.Exit, which is what makes each verb testable.
internal/resolver
The walk, and what it refuses
Resolution starts at the compiled-in root hints and follows referrals down. Glue is bailiwick-checked against the zone that sent it by comparing decoded labels, not string suffixes — a dot inside a label is escaped, so evil\.com ends in the bytes com. without being anywhere near it. A referral must descend strictly below the zone just asked and must contain the name being resolved.
- Delegations
- 16 max
- Queries per resolution
- 64 max
- CNAME links
- 8 max
- Recursion desired
- cleared on every hop
internal/server
Two transports, two concurrency models
UDP has one reader feeding a bounded worker pool, because a datagram carries no connection to account against and a goroutine per packet is memory amplification anyone can aim at you. TCP gets a goroutine per connection, capped, because a connection is something a client had to establish. A partial bind is fatal: a server holding only TCP reads to a user as DNS being broken.
- Default listener
- 127.0.0.1:15353
- UDP workers
- 64
- Queued packets
- 1024, then dropped
- EDNS0 payload
- 1232 octets
internal/cache · internal/single
Two depths of memory, and one walk per herd
Answers are the obvious half. Delegations are the half that distinguishes names: caching only answers still sends every unseen host under a zone through root and com, while a remembered zone cut starts the second one at the zone. Concurrent identical questions collapse into a single in-flight resolution, which is the case a cache cannot help with.
- Shards
- 256, random maphash seed
- Negative caching
- RFC 2308, capped at 3h
- Serve-stale
- RFC 8767, opt-in
- TTLs
- rewritten on the way out
internal/rrl · resolver/case0x20.go
Forgery and amplification
Off-path forgery is answered with entropy: a crypto/rand transaction ID, a source port the kernel picks on a connected socket, and the case of every letter in the name randomised as a nonce the reply must echo. Amplification is answered by limiting responses rather than queries, because what matters is what leaves the machine.
- Bits to guess
- 34 to 51
- Client networks
- /24 and /56
- Over the limit
- dropped, not refused
- Slip
- every 2nd answered truncated