dnsflow — an advanced DNS forwarding plugin for CoreDNS focused on speed, reliability, and intelligent traffic routing. It extends the built-in forward plugin with CIDR-based dynamic rerouting, self-learning domain tracking, a high-performance cache, and Linux firewall integration.
Multi-protocol upstreams: Supports UDP, TCP, and DNS-over-TLS (DoT). Connections are reused across requests to minimise handshake overhead.
Intelligent routing with self-learning: The check directive inspects the IP addresses in each DNS response against one or more CIDR files. Domains whose resolved IPs fall outside (or inside) the expected range are automatically rerouted to a different upstream. Combined with track, newly discovered domains are appended to a target file in full:DOMAIN format, persisted for crash safety, and restored on restart — enabling hands-free, long-lived adaptive routing.
High-performance cache: A sharded, 2Q (Two-Queue) eviction cache shields hot entries from one-time scan evictions. Supports proactive background prefetching (prefetch), Stale-While-Revalidate serving (serve_stale), gzip-compressed persistence across restarts (persist), and top-hit warmup on startup (warmup).
Linux firewall integration: Resolved IP addresses can be automatically injected into ipset or nftables sets via an internal async worker, enabling per-domain transparent proxy steering without external scripts. Per-entry timeouts are supported.
Load balancing & health checking: Three upstream selection policies — random, round_robin, and sequential — with continuous health probing. A configurable spray fallback prevents SERVFAIL when all upstreams are simultaneously down.
Observability: Exports a comprehensive set of Prometheus metrics covering request latency (microsecond precision), cache hit/miss/stale rates, CIDR reroute events, domain tracker state, and firewall queue health.
In its most basic form, a simple DNS redirecter uses the following syntax:
dnsflow FROM... {
to TO...
}
-
FROM...is the file list which contains base domain/rule to match for the request to be redirected..(i.e. root zone) can be used solely to match all incoming requests as a fallback.Several formats are supported, including standard and v2fly compatible formats:
-
DOMAIN: matches the domain itself and all its subdomains. -
full:DOMAIN: matches the domain exactly (exact match). -
domain:DOMAIN: matches the domain itself and all its subdomains. -
keyword:KEYWORD: matches any domain that contains the keyword. -
regexp:PATTERN: matches domains using a regular expression. -
include:FILENAME: includes domain rules from another file (path relative to current file). -
server=/DOMAIN/...:dnsmasqformat, only theDOMAINpart will be used.Attributes can be appended to rules (e.g.,
domain:google.com @cn), although they currently serve as metadata. Text after#character will be treated as comment. Unparsable lines(including whitespace-only line) are therefore just ignored.
-
-
to TO...are the destination endpoints to redirected to. This is a mandatory option.The
tosyntax allows you to specify a protocol, a port, etc:[dns://]IP[:PORT]use protocol specified in incoming DNS requests, it mayUDPorTCP.[udp://]IP:[:PORT]useUDPprotocol for DNS query, even if request comes inTCP.[tcp://]IP:[:PORT]useTCPprotocol for DNS query, even if request comes inUDP.tls://IP[:PORT]@TLS_SERVER_NAMEortls://HOSTNAME[:PORT]for DNS over TLS. The@TLS_SERVER_NAMEsuffix supplies the SNI for the handshake; it is mandatory when the address is a literal IP. Hostname forms derive SNI from the hostname automatically.Example:
dns://1.1.1.1 8.8.8.8 tcp://9.9.9.9 udp://2606:4700:4700::1111 tls://1.1.1.1@one.one.one.one tls://8.8.8.8@dns.google tls://dns.quad9.net
An expanded syntax can be utilized to unleash of the power of dnsflow plugin:
dnsflow FROM... {
to TO...
except IGNORED_NAME...
ipset SETNAME FAMILY [timeout DURATION]
nfset add element TABLE SET [ip|ip6|auto] [INTERVAL] [TIMEOUT]
filter ipv4|ipv6
check OUTPUT_FILE [in|out] CIDR_FILE... [match any|all]
track [capacity N] [max_age DURATION] [compact_ratio FLOAT]
cache {
capacity SIZE
persist FILE_PATH
}
# Advanced options (rarely needed, see below)
fallback [spray|none]
policy random|round_robin|sequential
health_check DURATION
path_reload DURATION
metrics_lookup true|false
# Advanced cache options
cache {
warmup COUNT
prefetch HITS PERCENTAGE
serve_stale DURATION
min_ttl DURATION
max_ttl DURATION
}
}
Some of the options take a DURATION as argument, zero time(i.e. 0) duration to disable corresponding feature unless it's explicitly stated otherwise. Valid time duration examples: 0, 500ms, 3s, 1h, 2h15m, 7d, 1d12h, etc. Bare numbers are treated as seconds (e.g. 300 = 5m).
-
FROM...andto TO...as above. -
exceptis a space-separated list of domains to exclude from redirecting. Requests that match none of these names will be passed through.It usually not a good idea to embed too many
exceptdomains inCorefile, in which case you should try to delete them directly inFROMfiles. -
ipset(needs root user privilege) specifies resolved IP addresses fromFROM...will be added to ipsetSETNAME.-
FAMILY: The address family to use, e.g.,ipv4,ipv6,inet,inet4,inet6. -
timeout DURATION: Optional timeout for the ipset entry (e.g.,7d,24h,1h30m,604800s, or bare seconds604800).timeout 0means a permanent entry, not disabled.Note that this option is only effective on Linux.
-
-
nfset add element(needs root user privilege) adds resolved IP addresses to nftables set.TABLE: nftables table name.SET: nftables set name.[ip|ip6|auto]: Address family (default isauto).[INTERVAL]: Set totrueif the nftables set is an interval set.[TIMEOUT]: Optional duration for the set element timeout (e.g.,7d,24h,1h30m). A zero timeout means a permanent element, not disabled.
-
filterfilters the DNS responses.none: No filtering (default).ipv4: Filter IPv4 addresses (A records).ipv6: Filter IPv6 addresses (AAAA records).
-
checkevaluates DNS responses against CIDR ranges and automatically reroutes matching domains to the target upstream. IPv4-mapped IPv6 answers (::ffff:x.x.x.x) are normalized to IPv4 before matching, the same as firewall set dispatch.OUTPUT_FILE: The domain list file of the target upstream to reroute matched domains into (in v2flyfull:format). Must correspond to aFROMfile used by anotherdnsflowblock.[in|out]: Optional keyword to specify the matching direction:in: Match domains whose IPs ARE inside the CIDR range.out(default): Match domains whose IPs are NOT inside the CIDR range (e.g. foreign IPs falling outside a domestic CIDR list).
CIDR_FILE...: One or more files containing CIDR ranges (one prefix per line) to check against.[match any|all]: Optional match policy (defaultall):all(default): Reroute only when all IPs in the DNS response satisfy the condition. Recommended — avoids false positives for CDNs that return a mix of domestic and foreign addresses.any: Reroute as soon as any IP satisfies the condition. Use for strict pollution detection where even one foreign IP is enough reason to reroute.
checkcan be specified multiple times on the same upstream to check against different CIDR sets or target different outputs.Routing internals and dynamic tracking behavior are documented in docs/routing.md.
-
trackenables runtime domain lifecycle management on a target upstream. It must be placed on the upstream whoseFROMfile is referenced by acheck OUTPUT_FILEon another upstream.capacity N: Optional, maximum number of dynamically discovered domains to keep in memory (default8000). When full, the least recently queried domain is evicted. Set to0for unlimited.max_age DURATION: Optional, evict domains that have not been queried for longer than this duration (e.g.720h,30d). Default is disabled (0). On restart, entries older thanmax_ageare also discarded. Entries without a timestamp (from legacy files) are never age-evicted.compact_ratio FLOAT: Optional, fraction ofcapacityevictions that triggers a file compaction (default1.0, i.e. one full store turnover). Lower values compact more frequently at the cost of extra I/O. For unlimited-capacity trackers withmax_ageset, compaction triggers after every2000age evictions.
Tracked domains are appended to
OUTPUT_FILEimmediately for crash safety, and the file is compacted automatically when evictions accumulate. On restart, persisted entries are restored into memory. -
cacheenables a high-performance DNS cache with persistence and warmup support.-
capacity SIZE: Approximate maximum number of cached records. The cache is internally split into 128 shards, each capped atceil(SIZE / 128)entries; the true upper bound is thereforeceil(SIZE / 128) × 128, which can exceedSIZEby up to 127 entries (notable only for very smallSIZE). Set to0for unlimited. Default is10000. -
persist FILE_PATH: Persist hot cache entries to a gzip-compressed file. Supports atomic writes and versioned file headers. When multiple dnsflow blocks use the same path, files are automatically scoped by upstream name. Default is disabled.
Cache internals, persistence format, and refresh behavior are documented in docs/cache.md.
-
Firewall backend behavior, timeout precedence, and async deduplication are documented in docs/firewall.md.
The following options have sensible defaults and rarely need to be configured:
-
fallbackconfigures the failsafe policy when all upstreams intoare marked as unhealthy. The default isspray, which will randomly pick one upstream to send the traffic to as a last resort, ignoring health status. Usefallback noneto disable this behavior (requests will fail immediately with SERVFAIL when all upstreams are down). -
policyspecifies the policy to use for selecting upstream hosts. The default israndom.-
randomwill randomly select a healthy upstream host. -
round_robinwill select a healthy upstream host in round robin order. -
sequentialwill select a healthy upstream host in sequential order.
-
-
health_checkconfigure the behaviour of health checking of the upstream hosts:DURATIONspecifies health checking interval. Default is2s, minimal is1s.
-
path_reloadchanges the reload interval between each path inFROM.... Default is disabled (0), minimal is1s. -
metrics_lookup true|falseenables Prometheus timing for the domain lookup path (coredns_dnsflow_name_lookup_duration_us). Default isfalsebecause this metric adds timing/Observe overhead to every request; enable it only while profiling routing lookup cost.
-
warmup COUNT [RATE]: After restart, asynchronously pre-resolve the top N expired entries by hit count. Requirespersist. Set0to disable. Default count is200. OptionalRATE(1–200) controls queries per second during warmup (default10). -
prefetch HITS PERCENTAGE: Trigger background refresh when an entry has at leastHITSaccesses and remaining TTL is belowPERCENTAGE.HITSmay be0, which makes every cache hit eligible for the percentage check.PERCENTAGEmay be0to effectively disable near-expiry prefetch. Default is1 20%. -
serve_stale DURATION: Maximum time window to serve expired entries as stale data (RFC 8767).0disables stale serving. Default is1d. -
min_ttl DURATION: Minimum cache TTL to prevent excessive origin lookups.0disables the lower clamp. Default is5s. -
max_ttl DURATION: Maximum cache TTL. Must be greater than or equal tomin_ttl;0is only valid whenmin_ttlis also0, and disables caching time for newly written entries. Default is1h.
If monitoring is enabled (via the prometheus plugin) then the following metrics are exported:
-
coredns_dnsflow_name_lookup_duration_us{server, matched}- duration (in microseconds) per domain name trie lookup. This metric is emitted only whenmetrics_lookup trueis configured. -
coredns_dnsflow_request_duration_us{server, to}- end-to-end duration (in microseconds) per upstream interaction, including cache hits. -
coredns_dnsflow_request_count_total{server, to}- query count per upstream. -
coredns_dnsflow_response_rcode_count_total{server, to, rcode}- count of RCODEs per upstream. -
coredns_dnsflow_hc_failure_count_total{server, to}- number of failed health checks per upstream. -
coredns_dnsflow_hc_all_down_count_total{server, to}- counter of when all upstreams marked as down.
Where server is the Server Block address responsible for the request (and metric). matched is the match flag, "1" if in any name list, "0" otherwise.
-
coredns_dnsflow_cache_hits_total{server, type}- cache lookup counts by result type:hit,miss,stale,expired. -
coredns_dnsflow_cache_entries- current number of cached entries. -
coredns_dnsflow_cache_size_bytes{upstream}- approximate DNS wire-format size of cached messages by upstream. This is useful for comparing relative cache footprint, but it is not Go heap usage or full process RSS. -
coredns_dnsflow_cache_prefetch_total- number of background prefetch triggers. -
coredns_dnsflow_cache_stale_refresh_total- number of async refresh operations triggered for stale (expired but within serve-stale window) entries. -
coredns_dnsflow_cache_refresh_effective_total{type}- background refresh responses that actually updated the cache, bytype(prefetchorstale). Compare againstcache_prefetch_total/cache_stale_refresh_totalto measure how often a refresh wins the race against a concurrent direct query. Triggered ≠ applied. -
coredns_dnsflow_cache_refresh_skipped_fresher_total{type}- background refresh responses discarded because a fresher entry was written while the refresh was in flight. A steadily nonzero rate is healthy; a large ratio vscache_refresh_effective_totalsuggests the prefetch window is too wide. -
coredns_dnsflow_cache_drops_total- number of LRU evictions due to capacity limit. -
coredns_dnsflow_cache_prefetch_drops_total- number of prefetch requests dropped due to semaphore saturation (concurrent prefetch limit reached). -
coredns_dnsflow_firewall_queue_drops_total{backend}- number of firewall tasks dropped due to a full async queue, labeled by backend (ipsetornftables).
coredns_dnsflow_cidr_reroute_total{file, result}- CIDR-triggered domain reroute events per target file.resultisnew(first discovery),known(already tracked by a concurrent goroutine), orno_tracker(fallback path without a DomainTracker).
-
coredns_dnsflow_tracker_domain_count{file}- current number of dynamically tracked domains per domain list file. -
coredns_dnsflow_tracker_evictions_total{file, reason}- total evictions from the domain tracker.reasonindicates whether the eviction was triggered bycapacity(LRU) orage(TTL expiration). -
coredns_dnsflow_tracker_compact_total{file}- total file compaction operations triggered. -
coredns_dnsflow_tracker_restore_total{file}- total domains restored from disk on startup.
Sometimes you modified Corefile and yet Caddy server failed to reload the new config with the error "Error during parsing", dnsflow will do sanity check during parsing, if you misconfiged the Corefile, you're out of lock:
-
Argument count mismatch, out of range arguments, unrecognizable arguments, etc.
-
Missing mandatory property
to TO.... -
Used unsupported DNS transport type in
to TO.... -
exceptcontains domain names that conflict withFROMfiles. -
.(i.e. root zone) is matched in a configuration block.
Also note that some of the properties are cumulative (can be specified multiple times): except, to, ipset, nfset, check.
Rationale: Strict checking to ensure that user can detect errors ASAP, and make the Corefile less confusing.
If you think you found a bug in dnsflow, please issue a bug report. Enhancements are also welcomed.