Dgaard
A colleague in cybersecurity got me looking at DNS blocking lists — filtering ads, malware, telemetry — at the network level rather than per-device. The first problem I wanted to solve was randomly generated domains: malware uses algorithmically generated hostnames (C2 channels, botnets) that rotate so fast that static blocklists can never keep up. You add a domain to the list, the malware generates a thousand new ones overnight.
That conversation with an AI while designing the tool opened up a broader picture. Entropy math and N-Gram models can detect those generated domains in real time, before they ever appear on any public list. DNS protocol news pointed to other gaps: homograph attacks (look-alike domains using Unicode characters), DNS rebinding, data exfiltration over TXT records, CNAME cloaking. Each became a filter stage. Then, as a parent, I needed something that could also keep children from stumbling onto harmful content — without the maintenance overhead of subscribing to and updating category databases that are always one step behind.
The result is Dgaard: a DNS filtering proxy written in Rust that runs at the network edge (on a router, a Raspberry Pi, or a small VPS) and combines static blocklists with a heuristic engine that catches threats no list has seen yet.
How it works
Every DNS query passes through a stratified pipeline. Each stage can block the query or let it through to the next:
- Fast-drop gatekeeper — rejects malformed or non-ASCII domains immediately.
- Whitelist — trusted domains skip all remaining checks via a zero-copy hash lookup.
- Smart-IDN blocker — decodes Punycode and flags homograph attacks (e.g.
аррlе.comusing Cyrillic characters). - Tiered blocklist — exact matches and wildcard rules stored in Bloom filters and rkyv zero-copy archives; millions of entries, sub-millisecond lookups, minimal RAM.
- Heuristic engine — calculates Shannon Entropy and N-Gram scores on the domain name itself. High-randomness strings like
ajh12-v9z.topare characteristic of algorithmically generated domains and get blocked here without any prior knowledge. - Behavioral analytics — tracks per-client NXDOMAIN rates (botnet indicators), monitors TXT record entropy and CNAME chains for data exfiltration patterns, and limits domain label depth to detect DNS-over-DNS tunneling tools (iodine, dnscat2).
- GeoIP scoring — after upstream resolution, checks returned IPs against a local MaxMind-format database. Responses from high-risk jurisdictions add weighted points to the domain’s cumulative threat score, amplifying other signals without outright blocking by geography alone.
- Custom threat-intelligence flags — up to 16 user-defined domain lists mapped to named bitflags, each with its own suspicion weight; useful for sector-specific feeds (finance, healthcare) or AI-generated classification datasets.
The pipeline is designed to short-circuit early: most clean queries are answered from the LRU cache or whitelist before the heuristic engine ever runs.
Parental control
Rather than subscribing to large category databases that require constant updates, Dgaard uses label-aware keyword matching powered by an Aho-Corasick automaton. A small list of keywords (e.g. casino, porno, bet) is compiled into an automaton that occupies kilobytes, not megabytes. A keyword only triggers a block if it forms a complete domain label or is separated by a hyphen — so casino.com is blocked but casinon-les-bains.fr is not. This avoids the classic Scunthorpe problem where substring matching blocks legitimate sites. New domains in blocked categories are caught the moment they are registered, with no list update required.
Components
The suite has four independently installable tools:
dgaard is the DNS proxy itself. It runs on port 5353 (or 53 directly), listens for UDP/TCP queries, and forwards clean ones to an upstream resolver over DNS-over-TLS. Binary size under 5 MB; designed for OpenWrt and embedded routers.
dgaard-engine is the filtering core extracted as a standalone Rust library with no async runtime. Any application — a mail server, an HTTP proxy, a custom script — can embed it and call resolve_with_score() to score a domain against the full pipeline.
dgaard-daemon is a Unix socket daemon that wraps dgaard-engine. It evaluates domain strings against the full filter pipeline and returns a JSON suspicion score — without performing DNS resolution. Designed as a sidecar for MTAs (Postfix, Rspamd) or any local process that can write to a Unix socket. Supports SIGHUP for atomic blocklist reloads without restart.
dgaard-rest is an HTTP REST API that wraps dgaard-engine. It exposes domain scoring over HTTP/JSON for dashboards, web services, or any tool that speaks HTTP — without performing DNS resolution. Endpoints: GET /api/v1/health (liveness probe), GET /api/v1/blocklists (list metadata and async reload), and POST /api/v1/check (score a domain). Supports SIGHUP for atomic blocklist reloads.
dgaard-monitor is a terminal dashboard that connects to dgaard’s Unix socket and renders live DNS activity: per-client traffic, timeline charts, top blocked domains, and block statistics. Linux only.
adblockptimize ingests standard adblock lists (files or URLs) and splits them into a deduplicated network-level output (for dgaard, Pi-hole, dnsmasq) and a browser-level output (cosmetic CSS/JS rules for uBlock Origin). This removes browser-specific noise from the DNS blocklist and shrinks it considerably.
Alternatives
Self-hosted
Pi-hole is a DNS sinkhole that intercepts queries and returns NXDOMAIN for blocked domains. It is well-established, has a web interface, and supports a large ecosystem of community blocklists. Filtering is entirely list-based: a domain must appear on a list before it is blocked. No heuristics, no DGA detection. Memory usage scales with list size. Designed for Raspberry Pi but requires more resources than a typical OpenWrt router can offer.
AdGuard Home is architecturally similar to Pi-hole but adds DNS-over-HTTPS/TLS upstream support, a parental control mode backed by AdGuard’s cloud category database, and a client-side filtering rule editor. Still purely list-based for blocking decisions. Parental control requires sending queries to AdGuard’s Safe Browsing API, which is a cloud call. Heavier than Pi-hole in terms of resource use.
Both Pi-hole and AdGuard Home share the same fundamental gap: they are blind to newly registered and algorithmically generated domains until a human curates a list entry.
Cloud DNS
CleanBrowsing Family Filter, OpenDNS FamilyShield, AdGuard Family DNS, and Cloudflare 1.1.1.3 all work the same way: you point your router’s DNS at their servers, and they filter responses according to their own categories. Setup takes two minutes. Parental filtering is updated continuously by their teams.
The trade-off is structural. Every DNS query your network makes — every website visited, every app’s check-in, every connected device — is resolved through their infrastructure. That data is logged, at minimum for abuse and service purposes. For a home network with children’s devices, medical apps, and work machines on the same subnet, that is a significant data footprint handed to a third party.
Latency is another factor. Cloud DNS adds a round-trip to a remote resolver for every uncached query. On a well-connected connection this is usually negligible, but it is a hard dependency: if the upstream is unreachable or rate-limits you, DNS resolution stalls for your whole network.
These services also have no customization below the category level. You cannot tune scoring, add your own threat feeds, whitelist specific patterns, or inspect what was blocked and why.
Comparison
| Pi-hole | AdGuard Home | Cloud DNS (all four) | Dgaard | |
|---|---|---|---|---|
| Privacy | Local | Local (parental via cloud API) | All queries sent to provider | Fully local |
| DGA detection | No | No | Varies, opaque | Yes (entropy + N-Gram) |
| Newly registered domains | Reactive (list) | Reactive (list) | Varies, opaque | Proactive (NRD feeds + heuristics) |
| Parental control | Via blocklists | Via AdGuard cloud API | Yes, category-based | Yes, local keyword engine |
| RAM usage | Moderate–high | High | None (cloud) | Low (Bloom filters + rkyv) |
| OpenWrt / embedded | Difficult | No | No (just DNS config) | Yes (binary under 5 MB) |
| Monitoring | Web UI | Web UI | Provider dashboard only | Terminal TUI, Unix socket |
| Customization | Blocklists, regex | Blocklists, rules | None below category | Per-stage config, custom TI flags, GeoIP weights |
| DNS tunneling blocking | No | No | Varies, opaque | Yes (label depth limit) |
| Internet dependency | No | Partial (parental) | Yes | No |
Source code: codeberg.org/slundi/dgaard