filterlist is a CoreDNS plugin that filters DNS queries against an ultra fast hybrid matching engine built from allowlist and denylist filter lists. It is designed for DNS-layer blocking of domain-based rules with predictable lookup latency and live reload support.
The plugin loads host-oriented rules from supported filter list formats and, by default, splits them into literal domain patterns (stored in a hash-based suffix map) and wildcard patterns (compiled into a minimized DFA). If you prefer a single fully compiled automaton, matcher_mode dfa compiles every rule into one DFA instead. Queries are evaluated in this order:
- Normalize the queried name.
- Check the allowlist matcher first.
- Check the denylist matcher second.
- Forward or block based on the configured action.
That makes whitelist precedence explicit and keeps per-query matching on the hot path inexpensive.
filterlist enforces high-performance DNS policy and security controls—blocking unwanted or malicious domains, protecting internal services, and ensuring naming compliance. Designed for predictable per-query overhead and hot-reloadable rules for safe live updates.
- Filter list support: Parses AdGuard, EasyList, ABP, and hosts-style filter lists
- Selectable matcher mode: default hybrid mode uses a suffix map for literals plus a DFA for wildcards;
matcher_mode dfacompiles all rules into one DFA - Ultra fast: about 200ns (0.0002ms) latency per query, less than 5s for full compilation/DFA construction for standard AdGuard DNS filter list (.5s compilation time for hybrid mode)
- Hot reload: Watches filter list directories and recompiles matchers on changes
- Allowlist precedence: Domains in the allowlist are always allowed, even if blacklisted
- Multiple block actions: NXDOMAIN, REFUSE, or null IP responses
- RFC / IDNA name validation: Blocks queries whose names violate RFC rules (can be disabled)
- Deny-non-allowlisted mode: Optionally blocks every query not present in the allowlist (default: off)
- Observability: Prometheus metrics and structured logging
- Filter files are read from dedicated allowlist and denylist directories.
- Supported rules are split into literal domains (suffix map) and wildcards (DFA).
- The active matchers are swapped atomically after a successful recompilation.
- Filesystem changes trigger debounced recompilation, so updates can be applied without restarting CoreDNS.
This project intentionally focuses on DNS-relevant host matching. Browser-only rule semantics such as cosmetic filtering, request-type modifiers, or path-based matching are out of scope.
To use filterlist in production, build a custom CoreDNS binary that includes this plugin.
- Clone the CoreDNS repository and enter it.
- Add the
filterlistmodule as a dependency. - Register
filterlistinplugin.cfgbeforeforwardso it is inserted earlier in the CoreDNS plugin chain. - Regenerate the generated plugin glue.
- Build CoreDNS.
Example:
git clone https://github.com/coredns/coredns.git
cd coredns
go get github.com/TomTonic/filterlist@latestThen edit plugin.cfg and add this line before forward:
filterlist:github.com/TomTonic/filterlistThe important order is the CoreDNS plugin chain generated from plugin.cfg, not the order in which Go downloads modules and not the stanza order in the Corefile. go get only makes the module available to the build. filterlist must be listed before forward in plugin.cfg so the generated handler chain reaches filterlist before forward answers the query.
After updating plugin.cfg, regenerate and build:
go generate
go buildThis produces a coredns binary that includes the filterlist plugin.
go build -o build/filterlist-check ./cmd/filterlist-checkThis produces the helper CLI at ./build/filterlist-check. The CLI is optional and focused on validating list directories outside CoreDNS.
. {
prometheus :9153
filterlist {
allowlist_dir /etc/coredns/allowlist.d
denylist_dir /etc/coredns/denylist.d
action nxdomain
debounce 300ms
max_states 200000
}
forward . 8.8.8.8
}
In that configuration:
prometheusexposes the metrics described below.filterlistevaluates queries before they are forwarded upstream.allowlist_dirtakes precedence overdenylist_dirwhen the same domain matches both sets.
# Validate filter lists
./build/filterlist-check validate --list testdata/filterlists/allowlist --list testdata/filterlists/denylistThe CLI is useful for validating large lists before deploying them into CoreDNS and verifying that parsing plus matcher compilation succeed.
| Syntax | Example | Description |
|---|---|---|
| Domain filter | ||example.com^ |
Block domain and all subdomains |
| Exception | @@||example.com^ |
Allow rule (used for allowlist entries; excluded from denylist) |
| Wildcard | ||*.ads.example.com^ |
Block subdomain pattern (compiled into DFA) |
| Hosts entry | 0.0.0.0 example.com |
Block via hosts format |
The supported subset is intentionally conservative. If a rule cannot be reduced to a domain-level decision at DNS time, it is skipped rather than partially interpreted.
This project intentionally implements a strict, DNS-oriented subset of Adblock Plus, EasyList, and AdGuard syntax. The parser is designed to extract host-based network rules that can be matched at the DNS layer. It does not attempt full browser-side filter semantics.
| Rule family | Accepted examples | Behavior in filterlist |
|---|---|---|
| Basic host-based blocking rules | ||example.com^, ||sub.example.com^ |
Parsed into blocking domain patterns |
| Exception rules | @@||example.com^, @@example.com |
Parsed into allow rules; allowlist wins over denylist |
| Host wildcards | ||*.ads.example.com^, ||ads*.example.com^ |
Preserved as wildcard domain patterns |
| Hosts file entries | 0.0.0.0 example.com, 127.0.0.1 example.com |
Parsed as blocking rules |
| Selected no-op modifiers | ||example.com^$important, ||example.com^$document, ||example.com^$all, ||example.com^$third-party |
Domain part is kept; modifier semantics are ignored |
| Rule family | Real-world examples | Why it is unsupported |
|---|---|---|
| Cosmetic rules | ##.banner, example.com#@#.sponsor, example.com#?#div:has(.ad) |
These are browser DOM rules, not DNS host rules |
| Scriptlet and JS rules | #%#//scriptlet('abort-on-property-read', 'alert') |
Requires browser runtime behavior |
| HTML filtering rules | $$script[tag-content="banner"] |
Operates on HTML bodies, not DNS names |
| Path and URL rules | ||example.com/path^, /ads/banner |
Cannot be reduced to a pure domain decision |
| Semantics-changing network modifiers | ||example.com^$script, ||example.com^$domain=foo.com, @@||example.com^$xmlhttprequest |
Request context is unavailable in DNS matching |
| Genericblock and generichide exceptions | @@||example.com^$genericblock, @@||example.com^$generichide |
Browser filter-engine concepts, not DNS policy |
The repository includes stricter regression tests against:
testdata/filterlists/Adguard_filter_example.txttestdata/filterlists/easylistgermany_example.txt
Those tests assert that:
- supported host-based network rules are parsed from large real-world lists;
- unsupported ABP and EasyList rule families are logged as unsupported instead of being treated as comments;
- real exception rules that fit the supported subset are recognized as allow rules;
- rules that depend on browser request context remain intentionally excluded.
- Non-network rules (
##,#@#,#?#,#$#,#%#,$$) - Advanced modifiers with browser request semantics (
$script,$domain=,$xmlhttprequest) - Path-only rules without hostnames
| Directive | Default | Description |
|---|---|---|
allowlist_dir |
(none) | Directory containing allowlist filter files |
denylist_dir |
(none) | Directory containing denylist filter files |
action |
nxdomain |
Block action: nxdomain, nullip, refuse |
nullip |
0.0.0.0 |
IPv4 address for nullip action |
nullip6 |
:: |
IPv6 address for nullip action |
debounce |
300ms |
Debounce duration for file change events |
max_states |
200000 |
Maximum wildcard DFA states (limits memory); set 0 to disable this cap |
compile_timeout |
30s |
Maximum compile duration |
ttl |
3600 |
TTL for blocked responses (nullip) |
log_queries |
false |
Log per-query outcome (list matched, name, rule source, pattern) at INFO level |
debug |
— | Deprecated alias for log_queries; prefer log_queries in new Corefiles |
invert_allowlist |
false |
Use ||domain^ instead of @@||domain^ for allowlist entries |
deny_non_allowlisted |
false |
Block every query that is not matched by the allowlist (deny-by-default mode) |
disable_RFC_checks |
false |
Disable the RFC 1035 / IDNA query-name validation precheck (default: checks are active) |
matcher_mode |
hybrid |
Runtime matcher representation: hybrid (suffix map + DFA) or dfa (fully compiled DFA) |
- At least one of
allowlist_dirordenylist_dirmust be configured. - Allowlist and denylist files use the same filter syntax. The
@@exception prefix controls which rules are compiled for each directory:- Denylist directories always exclude
@@-prefixed rules. Downloaded AdGuard and EasyList files work without conversion — exception rules embedded in those lists are automatically skipped. - Allowlist directories by default compile only
@@-prefixed rules (AdGuard semantics:@@= allow). Write@@||safe.example.com^to allowlist a domain. - With
invert_allowlist, allowlist directories compile non-@@rules instead, so you can write||safe.example.com^to allowlist a domain.
- Denylist directories always exclude
- Startup stays fail-open if configured directories are unreadable, empty, or contain only unsupported rules.
- Every initial load and hot-reload writes a detailed compile summary to the CoreDNS log, including directory, outcome, rule count, state count, duration, and any error.
action nxdomainreturns NXDOMAIN for blocked queries.action refusereturns REFUSED for blocked queries.action nullipreturns syntheticAandAAAAanswers for address lookups, and falls back to NXDOMAIN for other query types.nullipconfigures the IPv4 sinkhole address.nullip6configures the IPv6 sinkhole address.ttlis only relevant fornullipanswers.debounce,max_states, andcompile_timeoutare operational safeguards for large or volatile filter sets.max_states 0disables DFA state capping for wildcard compilation. The plugin logs a warning at startup when uncapped mode is configured.- List parser safety limits are enforced per file: maximum physical line length is
8192bytes and maximum line count is200000. Files exceeding those limits are rejected and logged. log_queriesenables per-query log lines showing the outcome (allowlisted, forwarded, blocked), the matching list, the queried name, the source file and line number, and the original rule pattern. Useful for monitoring which queries are blocked and tuning custom allow/deny lists. The output appears at the[INFO]level in the CoreDNS log. Internal events such as raw filesystem notifications and DFA compile progress are logged at[DEBUG]level and only appear when CoreDNS is started with--debug.debugis a deprecated alias forlog_queriesand is accepted for backwards compatibility. Preferlog_queriesin new Corefiles.deny_non_allowlisted onenables deny-by-default mode: every query that is not explicitly matched by the allowlist is blocked in the denylist phase, before the denylist matcher is consulted. Requires at least one configured allowlist to be useful. Default isoff.disable_RFC_checkscontrols the RFC 1035 + IDNA Lookup-profile query-name precheck. Whenoff(the default), queries whose names violate LDH syntax, label-length limits, or IDNA encoding are blocked immediately after thedeny_non_allowlistedcheck and before the denylist matcher. The implementation uses a tight scan with per-label and total-length counters on the ASCII fast path and only calls IDNA conversion when it sees an ACE-prefix label (xn--). Set it toonto skip this check for environments that host non-standard names (for example, names with underscores used by some services).matcher_mode hybridis the default and keeps startup and reload compile times much lower by storing literal rules in the suffix map and compiling only wildcard rules into the DFA.matcher_mode dfacompiles every rule into one DFA (also the literal ones). In theBenchmarkSequenceMapVsDFAbenchmark with the bundled realistic denylist samples and Cloudflare top-domain input, that is allocation free and reduced lookup cost about 5%, but increased compile time from about0.5 sto about5 s. Use it when request-path latency matters more than reload speed.
- Normalize query name (lowercase, remove trailing dot)
- Check allowlist matcher → if match, allow (forward to next plugin)
deny_non_allowlisted— if enabled, block every allowlist miss- RFC / IDNA precheck — if not disabled, block names that violate RFC 1035 or the IDNA Lookup profile
- Check denylist matcher → if match, block according to action
- No match → forward to next plugin
All metrics are exported with the coredns_filterlist_ prefix through the CoreDNS Prometheus endpoint.
| Metric | Type | Description |
|---|---|---|
coredns_filterlist_queries_total |
Counter | Total queries handled, labeled by result (see below) |
The coredns_filterlist_queries_total counter records exactly one increment per
query. Summing it over all result values yields the total query volume, and
the individual series tell you why a query was forwarded or blocked:
result label |
Meaning |
|---|---|
allowlisted |
The query matched the allowlist and was forwarded |
forwarded |
No rule matched and the query was forwarded unchanged |
blocked_denylist |
The query matched the denylist and was blocked |
blocked_rfc |
The query name failed the RFC 1035 / IDNA check and was blocked |
blocked_unlisted |
The query was blocked by the deny_non_allowlisted policy |
| Metric | Type | Description |
|---|---|---|
coredns_filterlist_compile_errors_total |
Counter | Number of failed filter load or compile runs |
coredns_filterlist_allowlist_rules |
Gauge | Current number of compiled allowlist rules in the active snapshot |
coredns_filterlist_denylist_rules |
Gauge | Current number of compiled denylist rules in the active snapshot |
coredns_filterlist_allowlist_states |
Gauge | Number of states in the compiled allowlist matcher (memory/complexity proxy) |
coredns_filterlist_denylist_states |
Gauge | Number of states in the compiled denylist matcher (memory/complexity proxy) |
coredns_filterlist_last_compile_timestamp_seconds |
Gauge | Unix timestamp of the most recent successful compilation |
coredns_filterlist_last_compile_duration_seconds |
Gauge | Duration in seconds of the most recent successful compilation |
| Metric | Type | Description |
|---|---|---|
coredns_filterlist_compile_duration_seconds |
Histogram | Distribution of matcher compilation durations across reloads |
coredns_filterlist_match_duration_seconds |
Histogram | Per-query matching latency, labeled by result (forwarded / blocked) |
- Use the
resultbreakdown ofqueries_totalto track total volume, block rate, and the reason for each block (denylist vs. RFC violation vs. unlisted). - Use
compile_duration_secondsandlast_compile_duration_secondsto spot slow reloads. - Use
last_compile_timestamp_secondsto alert when file changes stop being picked up (e.g.time() - ...last_compile_timestamp_seconds > 3600). - Use
compile_errors_totalto alert on failing reloads while the plugin keeps serving the previous ruleset. - Use
allowlist_states/denylist_statesto watch matcher memory and complexity grow as lists change. - Use
match_duration_secondsto watch lookup overhead on the request path. - The
allowlist_rulesanddenylist_rulesgauges reflect the currently active parsed rule counts after reload, which is more useful operationally than just counting raw source lines.
Typical Prometheus queries look like this:
# Overall block rate
sum(rate(coredns_filterlist_queries_total{result=~"blocked_.*"}[5m]))
/
sum(rate(coredns_filterlist_queries_total[5m]))
# Blocked queries broken down by reason
sum by (result) (rate(coredns_filterlist_queries_total{result=~"blocked_.*"}[5m]))
# Run tests
go test ./... -count=1
# Run tests with race detector
go test ./... -race -count=1
# Run linter
golangci-lint run ./...
# Generate coverage report
go test ./... -race -coverprofile=coverage.out -covermode=atomic
go tool cover -html=coverage.out -o coverage.html
# Run benchmarks
go test -bench=. -benchmem ./pkg/automaton ./pkg/listparserSee DESIGN.md for detailed architecture documentation.
BSD 3-Clause License. See LICENSE.