filterdns — Why I Wrote My Own DNS Filter
A museum network is not one network. There are staff machines that need full internet, exhibition installations that need almost none, public Wi-Fi that needs ads and trackers blocked, kiosk PCs that should only ever talk to one upstream, and the occasional artwork that needs an unblockable allowlist of three specific domains. One DNS server, many policies.
The off-the-shelf options I tried — Pi-hole, AdGuard Home, NextDNS — all assume one policy per server. You can run several instances and route traffic between them, but at that point you’re maintaining a fleet, not a service. So I wrote filterdns.
What it actually does
Multi-protocol on the front, profile-aware on the back.
- DoH (DNS-over-HTTPS, port 443) — profile selected via the Host header subdomain (
work.dns.example,kiosk.dns.example, …) - DoT (DNS-over-TLS, port 853) — profile via the TLS SNI
- Legacy DNS (port 53, UDP and TCP) — profile via a source-IP lookup in the devices table
Per profile you get the standard things: blocklist subscriptions (Hagezi, StevenBlack, OISD), allowlists, query logs, stats. Pause filtering for 5/15/30/60 minutes when something legitimately wants to load. Maintenance mode that blocks everything except a tiny manual allowlist — useful for kiosk installations during opening hours.
The bit I’m happiest about is self-service: profiles can be created by users without admin access, gated by per-profile bcrypt passwords. The IT person doesn’t need to be in the loop for someone to spin up a “kids’ tablet, no social media” profile.
Tech choices
- Python 3.14 + Poetry — recent enough for the async ergonomics improvements
- Async stack: Quart on Hypercorn for HTTP/2 and concurrency
- DNS:
dnspythonfor the protocol handling - DB: PostgreSQL with
asyncpg, migrations via Alembic - Frontend: SvelteKit
- Cache: two-tier — a curated allowlist that survives restarts, plus an on-demand cache for everything else
- Deploy: Docker Compose
The whole thing fits in one repo and one stack. There’s no Redis dependency for the cache, no separate worker process, no message queue — every request goes async, the cache lives in-process, and PostgreSQL is the source of truth.
Why not just $existing-thing
Pi-hole is great if you have one network. AdGuard Home is great if you don’t need legacy DNS. NextDNS is great if you trust a SaaS with the query log. None of them do per-profile-on-the-same-box across DoH/DoT/53 with self-service profile creation.
Also — and this is the museum-engineer answer — if it breaks at 9am on a Saturday and the exhibition opens at 10, I want to be reading my own code, not someone else’s plugin system.
Open source
github.com/zkmkarlsruhe/filterdns. Currently running our network and several of my home segments.
If you’re a small institution that has variant policies but doesn’t want a fleet, give it a try. If you’re at a larger institution with the same problem, I’d love to hear what your shape looks like.