Skip to content
strob3Public

About

DNS-over-HTTPS (DoH) proxy

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

null53

null53 is a lightweight, zero-dependency DNS-over-HTTPS (DoH) proxy written in Go. It intercepts local port 53 queries (UDP & TCP), tunnels them through encrypted HTTPS upstream resolvers, accelerates repeated requests with an in-memory LRU cache, and nullifies ad/tracking queries with an allowlist & blocklist txt files.

Local Client (Browser/OS)
        │
   [Port 53/1053]
        ▼
   ┌─────────┐
   │ null53  │
   └───┬─┬───┘
       │ │
       │ ├── Blocked Domain ──► Return NXDOMAIN (0ms, null route)
       │ │
       │ ├── Cache Hit ───────► Return cached wire bytes (TTL decremented)
       │ │
       │ └── Cache Miss ──────► HTTPS POST (RFC 8484) ──► Upstream DoH
       │                                                 (Cloudflare/Google)
       └─────────────────────────────────────────────────────────────

What it does

  • Port 53 Interceptor: Listens locally on port 53 (or unprivileged 1053 for testing) across both UDP and TCP.
  • Null Routing: Resolves ads, telemetry, and tracking domains directly to NXDOMAIN (null response) with zero WAN overhead.
  • Encrypted Egress: Upstream communication uses standard HTTPS (port 443), stopping ISP eavesdropping and DNS spoofing.
  • Failover Resilient: Automatically fails over between multiple DoH endpoints (Cloudflare, Google, Quad9) if an upstream fails.
  • Adaptive LRU Cache: Automatically decays record TTLs based on elapsed time and evicts least-recently-used records when full.

Usage

1. Build

go build -o null53 .

2. Run locally (unprivileged port 1053)

./null53 --config config.example.yaml --listen 127.0.0.1:1053

output:

level=INFO msg="blocklist loaded" domains=10 source=./blocklist.txt
level=INFO msg="allowlist loaded" domains=0 source=./allowlist.txt
level=INFO msg="null53 started" listen=127.0.0.1:1053 upstreams="[https://cloudflare-dns.com/dns-query https://dns.google/dns-query]"

3. Verify with dig

Normal resolution (encrypted DoH):

dig @127.0.0.1 -p 1053 example.com +short

Blocked domain (null / NXDOMAIN):

dig @127.0.0.1 -p 1053 doubleclick.net

returns status: NXDOMAIN in 0ms without hitting the internet.

TCP framing test:

dig +tcp @127.0.0.1 -p 1053 example.com +short

Cache hit verification: send two identical queries back-to-back:

# Query 1 (Cache miss):
msg=query proto=udp name=example.com. type=TypeA result=upstream latency=95ms
# Query 2 (Cache hit):
msg=query proto=udp name=example.com. type=TypeA result=cache-hit latency=15µs

^C to terminate, then, clean query metrics will be displayed:

msg="shutdown complete" total_queries=4 cache_hits=1 blocked=1 upstream=2 errors=0

Configuration (config.example.yaml)

listen: "127.0.0.1:1053"

upstream:
  urls:
    - "https://cloudflare-dns.com/dns-query"
    - "https://dns.google/dns-query"
  timeout: 5s

cache:
  max_entries: 10000

blocklist:
  path: "./blocklist.txt"     # local file or remote https url
  refresh_interval: 24h

allowlist:
  path: "./allowlist.txt"     # overrides blocklist

log:
  level: "info"               # debug, info, warn, error

Flags

Flag Description Example
--config Path to YAML config ./null53 --config config.yaml
--listen Listen address/port ./null53 --listen 127.0.0.1:53
--upstream Custom DoH upstream ./null53 --upstream https://dns.quad9.net/dns-query
--blocklist Local path or remote URL ./null53 --blocklist ./blocklist.txt
--allowlist Local path or remote URL ./null53 --allowlist ./allowlist.txt

Running on Port 53 (System-Wide DNS)

To run null53 without sudo, assign the CAP_NET_BIND_SERVICE capability to the binary:

sudo setcap cap_net_bind_service=+ep ./null53
./null53 --config config.example.yaml --listen 127.0.0.1:53

Then point your local resolver to 127.0.0.1 in /etc/resolv.conf.

About

DNS-over-HTTPS (DoH) proxy

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages