Razvan Dimescu 7327a96e82 refactor to async tokio with modular architecture
- Replace synchronous std::net::UdpSocket with tokio async runtime
- Spawn concurrent task per incoming DNS query via tokio::spawn
- Extract monolithic main.rs into modules: buffer, header, question,
  record, packet, config, cache, forward, stats
- Share state across tasks via Arc<ServerCtx> with scoped Mutex locks
- Add TOML config loading, TTL-aware cache, structured logging, stats
- Add CLAUDE.md, README, dns_fun.toml config, and design docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 04:50:16 +02:00
2020-12-29 12:29:09 +02:00

dns_fun

A DNS forwarding/caching proxy written from scratch in Rust. Parses and serializes DNS wire protocol (RFC 1035), serves local zone records from TOML config, caches upstream responses with TTL-aware expiration, and logs every query with structured output.

No async runtime, no DNS libraries — just std::net::UdpSocket and manual packet parsing.

Record Types

A, NS, CNAME, MX, AAAA

Usage

# Run with default config (dns_fun.toml)
sudo cargo run

# Run with custom config path
sudo cargo run -- path/to/config.toml

# Test
dig @127.0.0.1 google.com
dig @127.0.0.1 mysite.local

Requires root/sudo for binding to port 53.

Configuration

Edit dns_fun.toml:

[server]
bind_addr = "0.0.0.0:53"

[upstream]
address = "8.8.8.8"
port = 53
timeout_ms = 3000

[cache]
max_entries = 10000
min_ttl = 60        # floor: cache at least 60s
max_ttl = 86400     # ceiling: never cache longer than 24h

[[zones]]
domain = "mysite.local"
record_type = "A"
value = "127.0.0.1"
ttl = 60

[[zones]]
domain = "other.local"
record_type = "AAAA"
value = "::1"
ttl = 120

All sections are optional — sensible defaults are used if the config file is missing.

Request Pipeline

Query -> Parse -> Local Zones -> Cache -> Upstream Forward -> Respond
  1. Local zones — match against records defined in [[zones]], respond immediately
  2. Cache — return TTL-adjusted cached response if available
  3. Forward — send query to upstream resolver, cache the response
  4. SERVFAIL — returned to client on upstream failure

Caching

  • TTL derived from minimum TTL across answer records
  • Clamped to configured min_ttl/max_ttl bounds
  • TTLs in cached responses decrease over time (adjusted on serve)
  • Lazy eviction on capacity overflow + periodic sweep every 1000 queries

Logging

Controlled via RUST_LOG environment variable:

RUST_LOG=info sudo cargo run    # default — one line per query
RUST_LOG=debug sudo cargo run   # includes response details
RUST_LOG=warn sudo cargo run    # errors only

Log output:

2026-03-10T14:23:01.123Z INFO  192.168.1.5:41234 | A google.com | FORWARD | NOERROR | 12ms
2026-03-10T14:23:01.456Z INFO  192.168.1.5:41235 | A mysite.local | LOCAL | NOERROR | 0ms
2026-03-10T14:23:02.789Z INFO  192.168.1.5:41236 | A google.com | CACHED | NOERROR | 0ms

Stats summary (total, forwarded, cached, local, blocked, errors) logged every 1000 queries.

Project Structure

src/
  main.rs       # startup, config load, UDP listen loop, request pipeline
  lib.rs        # module declarations, Error/Result type aliases
  buffer.rs     # BytePacketBuffer — 512-byte DNS wire format read/write
  header.rs     # DnsHeader, ResultCode
  question.rs   # DnsQuestion, QueryType
  record.rs     # DnsRecord (A, NS, CNAME, MX, AAAA, UNKNOWN)
  packet.rs     # DnsPacket — full DNS message parse/serialize
  config.rs     # TOML config loading, zone map builder
  cache.rs      # TTL-aware DNS response cache with lazy eviction
  forward.rs    # upstream forwarding, SERVFAIL builder
  stats.rs      # query counters and periodic summary

Dependencies

toml = "0.8"
serde = { version = "1", features = ["derive"] }
log = "0.4"
env_logger = "0.11"
Description
Portable DNS resolver in Rust — .numa local domains, ad blocking, developer overrides
Readme MIT 4.1 MiB
v0.14.2 Latest
2026-04-23 04:57:37 +08:00
Languages
Rust 76%
HTML 12.1%
Shell 11.4%
Python 0.2%
Makefile 0.1%