DNS Subsystem Overview
Source: dns/, dns/transport/, dns/transport/fakeip/, dns/transport/hosts/, dns/transport/local/, dns/transport/dhcp/
Architecture
sing-box's DNS subsystem consists of three core components:
+------------------+
| DNS Router | Rule matching, transport selection
+------------------+
|
+------------------+
| DNS Client | Caching, EDNS0, RDRC, TTL management
+------------------+
|
+-------------+-------------+
| | |
+---------+ +---------+ +---------+
| UDP | | HTTPS | | FakeIP | ... more transports
+---------+ +---------+ +---------+- DNS Router (
dns/router.go): Matches DNS queries against rules, selects the appropriate transport, handles domain strategy and reverse mapping - DNS Client (
dns/client.go): Performs the actual DNS exchange with caching (freelru), EDNS0 client subnet injection, response domain rejection cache (RDRC), and TTL adjustment - DNS Transports (
dns/transport/): Protocol-specific query execution (UDP, TCP, TLS, HTTPS, QUIC/HTTP3, FakeIP, Hosts, Local, DHCP)
Supporting Components
- Transport Registry (
dns/transport_registry.go): Generic type-safe registration of transport types - Transport Adapter (
dns/transport_adapter.go): Base struct with type/tag/dependencies/strategy/clientSubnet - Base Transport (
dns/transport/base.go): State machine (New/Started/Closing/Closed) with in-flight query tracking - Connector (
dns/transport/connector.go): Generic singleflight connection management
Query Flow
Exchange (raw DNS message)
- Router.Exchange receives a
*dns.Msg - Metadata extraction: query type, domain, IP version
- If no explicit transport, match against DNS rules:
RuleActionDNSRoute-- select transport with options (strategy, cache, TTL, client subnet)RuleActionDNSRouteOptions-- modify options without selecting transportRuleActionReject-- return REFUSED or dropRuleActionPredefined-- return pre-configured response
- Client.Exchange performs the actual query:
- Check cache (with deduplication via channel-based locking)
- Check RDRC for previously rejected responses
- Apply EDNS0 client subnet
- Execute transport.Exchange with timeout
- Validate response (address limit check)
- Normalize TTLs
- Store in cache
- Store reverse mapping (IP -> domain) if enabled
Lookup (domain to addresses)
- Router.Lookup receives a domain string
- Determines strategy (IPv4Only, IPv6Only, PreferIPv4, PreferIPv6, AsIS)
- Client.Lookup dispatches:
- IPv4Only: single A query
- IPv6Only: single AAAA query
- Otherwise: parallel A + AAAA queries via
task.Group
- Results sorted based on strategy preference
Rule Retry Loop
When a rule has address limits (e.g., geoip restrictions on response addresses), the router retries with subsequent matching rules if the response is rejected:
go
for {
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery, &dnsOptions)
responseCheck := addressLimitResponseCheck(rule, metadata)
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
if responseCheck != nil && rejected {
continue // Try next matching rule
}
break
}Key Design Decisions
Deduplication
The cache uses channel-based deduplication to prevent thundering herd:
go
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
<-cond // Wait for the in-flight query to complete
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond) // Signal waiters
}()
}Loop Detection
DNS query loops (e.g., transport A needs to resolve its server address via transport A) are detected via context:
go
contextTransport, loaded := transportTagFromContext(ctx)
if loaded && transport.Tag() == contextTransport {
return nil, E.New("DNS query loopback in transport[", contextTransport, "]")
}
ctx = contextWithTransportTag(ctx, transport.Tag())RDRC (Response Domain Rejection Cache)
When a response is rejected by an address limit check, the domain/qtype/transport combination is cached in the RDRC to skip future queries against the same transport:
go
if rejected {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
// On subsequent queries:
if c.rdrc.LoadRDRC(transport.Tag(), question.Name, question.Qtype) {
return nil, ErrResponseRejectedCached
}EDNS0 Client Subnet
Applied before exchange when configured:
go
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}