MCPcopy Index your code
hub / github.com/bschaatsbergen/dnsdialer

github.com/bschaatsbergen/dnsdialer @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
119 symbols 494 edges 20 files 44 documented · 37% 1 cross-repo links
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

dnsdialer

Go Reference Go Report Card

This package allows you to take control of DNS resolution behavior through configurable multi-resolver strategies.

Why you'd want multiple resolvers: Redundancy (primary resolver failure doesn't cascade into total DNS outage). Performance (concurrent queries across resolvers, returning fastest response). Security (consensus validation across independent resolvers mitigates poisoning and MITM attacks). Integrity (cross-resolver validation detects poisoning, cache corruption, and configuration drift before propagation).

Most OS-level DNS stacks already support multiple resolvers, but they don't use them in parallel, they typically try the first, then fail over in sequence (which can be slow if the first resolver hangs). In high-throughput systems where single-digit millisecond DNS latency affects tail latencies and resolver failures propagate into cascading outages, you need deterministic multi-resolver behavior.

While OS-level DNS caching (mDNSResponder on macOS, systemd-resolved on Linux) provides sub-millisecond lookups, this package bypasses it by default to ensure fresh results for redundancy and consensus validation. Optional LRU caching with TTL-aware expiration is available via WithCache() to reduce latency on repeated lookups while maintaining explicit control over cache size and TTL bounds.

This package provides a DialContext implementation that plugs directly into HTTP transports, gRPC clients, or any custom connection pools expecting net.Dialer.

How it works

Built on miekg/dns, dnsdialer implements the same DialContext signature as net.Dialer, making it a drop-in replacement for any Go code that accepts a custom dialer (HTTP clients, gRPC, etc.).

The only difference: instead of using your system DNS resolver, it queries multiple DNS resolvers using your chosen strategy.

Performance

The standard library's net.Dialer relies on OS-level DNS caching (mDNSResponder on macOS, systemd-resolved on Linux), which provides sub-millisecond lookups once cached. dnsdialer has its own in-process LRU cache to avoid shared global state and maintain explicit control over TTL bounds. By caching parsed net.IP slices instead of raw DNS strings, you get similar dial latency with reduced per-lookup allocations.

go test -bench='^BenchmarkStdLib_DialContext$|^BenchmarkDNSDialer_DialContext_Cache_Single_Race$' -run=^$ -benchtime=5s -benchmem
goos: darwin
goarch: arm64
pkg: github.com/bschaatsbergen/dnsdialer
cpu: Apple M4
BenchmarkStdLib_DialContext-10                               360          16735385 ns/op            3549 B/op         57 allocs/op
BenchmarkDNSDialer_DialContext_Cache_Single_Race-10          354          16519391 ns/op             936 B/op         21 allocs/op
PASS
ok      github.com/bschaatsbergen/dnsdialer     12.114s

The standard library's DNS resolver implementation varies by CGO status: with CGO enabled (default), it uses getaddrinfo() which requires a system call and inter-process communication to the OS DNS cache (mDNSResponder on macOS, systemd-resolved on Linux) for every lookup. With CGO disabled, it uses a pure Go DNS implementation that sends queries directly to DNS resolvers for every lookup. dnsdialer maintains deterministic in-memory caching regardless of build configuration, providing much faster lookups by eliminating external communication overhead (system calls, inter-process communication, or network round-trips):

CGO_ENABLED=0 go test -bench=CGO -benchtime=5s -run=^ -benchmem
goos: darwin
goarch: arm64
pkg: github.com/bschaatsbergen/dnsdialer
cpu: Apple M4
BenchmarkCGO_StdLib_LookupHost-10                  26244            226596 ns/op             348 B/op         11 allocs/op
BenchmarkCGO_DNSDialer_LookupHost-10            58951063            103.6 ns/op              128 B/op          3 allocs/op
PASS
ok      github.com/bschaatsbergen/dnsdialer     12.401s

Usage

HTTP Client

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("8.8.8.8:53", "1.1.1.1:53"),
    dnsdialer.WithStrategy(dnsdialer.Race{}),
    dnsdialer.WithCache(1000, 1*time.Second, 5*time.Minute),
)

client := &http.Client{
    Transport: &http.Transport{
        DialContext: dialer.DialContext,
    },
}

resp, err := client.Get("https://api.github.com")

gRPC

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("8.8.8.8:53", "1.1.1.1:53"),
    dnsdialer.WithStrategy(dnsdialer.Race{}),
    dnsdialer.WithCache(1000, 1*time.Second, 5*time.Minute),
)

conn, err := grpc.Dial(
    "api.example.com:443",
    grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
        return dialer.DialContext(ctx, "tcp", addr)
    }),
)

Strategies

Race

Queries all servers simultaneously and returns the first successful response. Minimizes latency by leveraging the fastest available server.

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("8.8.8.8:53", "1.1.1.1:53"),
    dnsdialer.WithStrategy(dnsdialer.Race{}),
)

Fallback

Queries servers sequentially in order, providing reliability through ordered failover.

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("primary.dns:53", "backup.dns:53"),
    dnsdialer.WithStrategy(dnsdialer.Fallback{}),
)

Consensus

Requires a minimum number of servers to agree on the response. Improves security by detecting inconsistencies or DNS poisoning.

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("8.8.8.8:53", "1.1.1.1:53", "9.9.9.9:53"),
    dnsdialer.WithStrategy(dnsdialer.Consensus{
        MinAgreement: 2,    // Require 2 servers to agree
        IgnoreTTL:    true, // Ignore TTL differences when comparing
    }),
)

Compare

Queries all servers and detects discrepancies, calling a user-provided callback when differences are found. Useful for monitoring DNS resolver integrity.

dialer := dnsdialer.New(
    dnsdialer.WithResolvers("8.8.8.8:53", "1.1.1.1:53"),
    dnsdialer.WithStrategy(dnsdialer.Compare{
        OnDiscrepancy: func(host string, qtype dnsdialer.RecordType, results map[string][]dnsdialer.Record) {
            fmt.Printf("Discrepancy detected for %s (%s)\n", host, qtype)
        },
        IgnoreTTL: true,
    }),
)

Extension points exported contracts — how you extend this code

Strategy (Interface)
Strategy determines how to coordinate DNS queries across multiple resolvers. All strategies implement this interface, a [6 …
strategy.go
Logger (Interface)
Logger provides structured logging throughout the resolution process. [2 implementers]
resolver.go
Option (FuncType)
Option is a function that configures a Dialer. This package uses the functional options pattern, which provides: 1. Sen
options.go

Core symbols most depended-on inside this repo

Close
called by 27
connpool.go
WithResolvers
called by 22
options.go
WithStrategy
called by 22
options.go
New
called by 22
resolver.go
String
called by 12
recordtype.go
recordsEqual
called by 12
util.go
WithCache
called by 11
options.go
lookupIPs
called by 11
resolver.go

Shape

Function 64
Method 32
Struct 18
Interface 3
FuncType 1
TypeAlias 1

Languages

Go100%

Modules by API surface

benchmark_test.go24 symbols
strategy_test.go21 symbols
resolver.go18 symbols
util_test.go10 symbols
options.go7 symbols
strategy.go6 symbols
cache.go6 symbols
connpool.go5 symbols
udp.go4 symbols
recordtype.go4 symbols
resolver_test.go3 symbols
cgo_benchmark_test.go3 symbols

Used by 1 indexed graphs manifest dependencies, hub-wide

For agents

$ claude mcp add dnsdialer \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact