A DNS proxy that gives you real control over your home network.
Note: The full_reference_config.yaml is always more up to date than this README. When in doubt, check there.
DNS is the phonebook of the internet - every device on your network looks up a name before it connects anywhere.
sdproxy sits in the middle of that, on your router, and lets you decide what happens next: cache it, block it, route it to a different resolver, or apply time limits per child. No cloud subscription, no monthly fee, no external service that goes down.
It's written in Go, compiles to a single binary, and is lean enough to run on cheap home routers (OpenWrt on a TP-Link or Netgear, pfSense, OPNsense, or just a plain Linux box).
Plain old UDP/TCP (port 53), encrypted DNS-over-TLS (DoT), DNS-over-HTTPS (DoH), DNS-over-QUIC (DoQ), and the HTTP/3 variant. You pick what you want to listen on and what you want to forward to.
Answered something recently? Serve it from cache. That means faster browsing and fewer queries leaving your network. If a record expires while it's still popular, sdproxy refreshes it silently in the background so your devices never notice a miss. Stale records are served instantly while the refresh is in flight.
Point it at your DHCP lease files and /etc/hosts and it will answer local name lookups (your NAS, your printer, your Pi) without bothering an upstream resolver. Works with dnsmasq, ISC DHCP, Kea, and odhcpd - whatever your router uses.
Map a device's MAC address to a different upstream resolver group. The kid's tablet goes through a filtered resolver, your work laptop goes somewhere else, guests go through a basic default.
You can also route by domain suffix - queries for .lan stay local, queries for your company VPN domains go to your VPN's resolver.
Set up a profile per child, assign their devices by MAC address, and configure:
- A schedule - internet only between 07:00 and 21:00, for example. Different hours for school days vs weekends.
- A daily time budget - 2 hours total, with sub-limits per category (1 hour games, 30 minutes social media).
- Category blocking - social media completely blocked for one child, educational sites always allowed regardless of budget.
Categories (games, streaming, social media, etc.) are loaded from public domain lists. Only the categories you actually use are loaded - if you don't define a budget for "gambling", that list is never touched.
Time is tracked via DNS heartbeat: categorised domains get a short TTL so devices keep checking in. Accuracy is roughly plus or minus 5 minutes per session - more than good enough for home use.
Usage is snapshotted to disk so a router reboot doesn't wipe the day's counters.
Optional browser-based UI (enable it with a password in the config). No restart needed to flip a group into a different mode:
| Mode | What it does |
|---|---|
| DEFAULT | Normal - schedule and budget apply as configured. |
| ALLOW | Gate wide open - no schedule, no budget, no blocks. For "I need internet right now" moments. |
| FREE | Suspend the schedule and budget, but keep everything else. Good for supervised homework time. |
| BLOCK | Cut internet entirely for that group. |
Override state is intentionally in-memory only - it resets to DEFAULT on restart. These are meant to be quick manual interventions, not permanent changes.
Queries going upstream are stripped of client subnet info (EDNS ECS) and padded to standard sizes so they don't leak which domains you're querying by size alone. Cookies are stripped too.
With DDR (RFC 9462) enabled, any client that supports it (iOS, Android, modern Windows/macOS) will automatically discover and switch to encrypted DNS. No need to configure each device individually.
Built-in adaptive admission control monitors memory pressure, goroutine counts, and cache miss rate. If the router is getting hammered it backs off gracefully instead of falling over. Zero configuration needed.
git clone https://github.com/cbuijs/sdproxy
cd sdproxy
go build -o sdproxy .
./sdproxy -config config.yamlRequires Go 1.25+. No CGo, no external libraries.
server:
listen_udp: ["0.0.0.0:53"]
cache:
enabled: true
size: 1024
min_ttl: 60
upstreams:
default:
- "udp://1.1.1.1:53"
- "udp://9.9.9.9:53"That's enough to get a caching DNS proxy running. Add sections as you need them.
# OpenWrt MIPS (TP-Link, Netgear, etc.)
GOOS=linux GOARCH=mipsle GOMIPS=softfloat go build -ldflags="-s -w" -o sdproxy .
# OpenWrt ARM (Linksys, Asus, etc.)
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -o sdproxy .
# OpenWrt x86_64
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o sdproxy .The -s -w flags strip debug symbols - saves 30-40% binary size, which matters on small flash storage.
Everything lives in one YAML file. The full_reference_config.yaml in this repo documents every single option inline - that file is the manual. Copy it, strip what you don't need, adjust the rest.
2026/03/16 14:23:01 [DNS] [UDP] 192.168.1.42 (alice-iphone) -> google.com A | ROUTE: kids | UPSTREAM: doh://cloudflare-dns.com | OK
2026/03/16 14:23:02 [DNS] [UDP] 192.168.1.42 (alice-iphone) -> google.com A | ROUTE: kids | CACHE HIT
2026/03/16 14:23:05 [DNS] [UDP] 192.168.1.10 -> nas.lan A | LOCAL IDENTITY
2026/03/16 14:23:10 [DNS] [UDP] 192.168.1.42 (alice-iphone) -> youtube.com A | PARENTAL BLOCK
Disable with logging.log_queries: false. Strip timestamps for systemd/procd with logging.strip_time: true.
Note: The full_reference_config.yaml is always more up to date than this README. When in doubt, check there.