Hosts and Local DNS Transports
Source: dns/transport/hosts/hosts.go, dns/transport/hosts/hosts_file.go, dns/transport/local/local.go, dns/transport/dhcp/dhcp.go
Hosts Transport
The hosts transport resolves domains against hosts file entries and predefined mappings.
Structure
type Transport struct {
dns.TransportAdapter
files []*File
predefined map[string][]netip.Addr
}Lookup Priority
- Predefined entries are checked first (in-config mappings)
- Hosts files are checked in order
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
domain := mDNS.CanonicalName(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
if addresses, ok := t.predefined[domain]; ok {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
for _, file := range t.files {
addresses := file.Lookup(domain)
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
}
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{Id: message.Id, Rcode: mDNS.RcodeNameError, Response: true},
Question: []mDNS.Question{question},
}, nil
}Only A and AAAA queries are handled. Unresolvable domains return NXDOMAIN. Non-address queries also return NXDOMAIN.
Construction
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string,
options option.HostsDNSServerOptions) (adapter.DNSTransport, error) {
if len(options.Path) == 0 {
files = append(files, NewFile(DefaultPath)) // /etc/hosts
} else {
for _, path := range options.Path {
files = append(files, NewFile(filemanager.BasePath(ctx, os.ExpandEnv(path))))
}
}
if options.Predefined != nil {
for _, entry := range options.Predefined.Entries() {
predefined[mDNS.CanonicalName(entry.Key)] = entry.Value
}
}
}Domain names are canonicalized (lowercased, FQDN with trailing dot) via mDNS.CanonicalName.
Hosts File Parsing
The File struct provides lazy parsing with caching:
type File struct {
path string
access sync.Mutex
modTime time.Time
modSize int64
entries map[string][]netip.Addr
lastCheck time.Time
}Cache invalidation: The file is re-parsed only when:
- More than 5 seconds have elapsed since the last check, AND
- The file's modification time or size has changed
func (f *File) Lookup(domain string) []netip.Addr {
f.access.Lock()
defer f.access.Unlock()
if time.Since(f.lastCheck) > 5*time.Second {
stat, err := os.Stat(f.path)
if stat.ModTime() != f.modTime || stat.Size() != f.modSize {
f.entries = parseHostsFile(f.path)
f.modTime = stat.ModTime()
f.modSize = stat.Size()
}
f.lastCheck = time.Now()
}
return f.entries[domain]
}Parsing rules:
- Lines starting with
#are comments - Each line:
<IP> <hostname1> [hostname2] ... - Hostnames are canonicalized (lowercased + trailing dot)
- IPv4 and IPv6 addresses are both supported
- Multiple entries for the same hostname are accumulated
Default Path
// Linux/macOS
var DefaultPath = "/etc/hosts"
// Windows
var DefaultPath = `C:\Windows\System32\drivers\etc\hosts`Local DNS Transport
The local transport resolves DNS queries using the system resolver.
Structure (non-Darwin)
type Transport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
hosts *hosts.File
dialer N.Dialer
preferGo bool
resolved ResolvedResolver
}Resolution Priority
- systemd-resolved (Linux only): If the system uses resolved, queries are sent via D-Bus
- Local hosts file: Checked before network resolution
- System resolver: Falls back to Go's
net.Resolver
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
// 1. Try systemd-resolved
if t.resolved != nil {
resolverObject := t.resolved.Object()
if resolverObject != nil {
return t.resolved.Exchange(resolverObject, ctx, message)
}
}
// 2. Try local hosts file
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
// 3. System resolver
return t.exchange(ctx, message, question.Name)
}systemd-resolved Detection
func (t *Transport) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
if !t.preferGo {
if isSystemdResolvedManaged() {
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
if err == nil {
err = resolvedResolver.Start()
if err == nil {
t.resolved = resolvedResolver
}
}
}
}
}
}If preferGo is true, the Go resolver is used directly, bypassing systemd-resolved.
Darwin (macOS) Variant
On macOS, the local transport uses DHCP-discovered DNS servers or the system resolver with special handling for .local domains (mDNS).
DHCP Transport
The DHCP transport discovers DNS servers dynamically via DHCPv4:
Discovery
The transport sends DHCPv4 Discover/Request on the specified network interface and extracts DNS server addresses from the DHCP Offer/Ack.
Interface Monitoring
DNS servers are cached per-interface and refreshed when:
- The interface state changes (link up/down)
- The interface address changes
- The cache expires
Server Caching
type Transport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
interfaceName string
autoInterface bool
// ...
transportAccess sync.Mutex
transports []adapter.DNSTransport
lastUpdate time.Time
}The DHCP transport creates child transports (typically UDP) for each discovered DNS server and delegates queries to them.
Configuration
Hosts
{
"dns": {
"servers": [
{
"tag": "hosts",
"type": "hosts",
"path": ["/etc/hosts", "/custom/hosts"],
"predefined": {
"myserver.local": ["192.168.1.100"]
}
}
]
}
}Local
{
"dns": {
"servers": [
{
"tag": "local",
"type": "local",
"prefer_go": false
}
]
}
}DHCP
{
"dns": {
"servers": [
{
"tag": "dhcp",
"type": "dhcp",
"interface": "eth0"
}
]
}
}