Skip to content
hollow

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 pointVerbs, flags, output
EngineServing and resolving
StateCaching, filtering, counting
CodecOctets in, octets out

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.

2,177 code2,072 test

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