| name | dnsdist |
| description | DNS load balancer with DoS protection, multi-protocol support (DoH, DoT, DoQ), and dynamic traffic management |
| version | 1.0.0 |
| tags | ["dns","load-balancer","powerdns","doh","dot","doq","security","lua","performance"] |
dnsdist
High-performance DNS load balancer from PowerDNS that distributes queries across backend servers while providing DoS protection, caching, and support for encrypted DNS protocols (DoH, DoT, DoQ, DNSCrypt).
Overview
dnsdist is a DNS-aware, DoS-aware load balancer that receives DNS queries and intelligently routes them to backend servers based on configurable policies. It operates at the DNS protocol level, enabling sophisticated traffic management, abuse prevention, and protocol translation.
Key Capabilities
- Load Balancing: Distribute queries across multiple DNS backends
- DoS Protection: Dynamic blocking, rate limiting, eBPF filtering
- Multi-Protocol: Do53, DNS-over-TLS, DNS-over-HTTPS, DNS-over-QUIC, DNSCrypt
- Response Caching: Built-in packet cache with stale serving
- Dynamic Configuration: Runtime changes via Lua console
- Monitoring: Prometheus metrics, REST API, Carbon export
Installation
Package Managers
Debian/Ubuntu:
apt-get install -y dnsdist
RHEL/CentOS:
yum install -y epel-release
yum install -y dnsdist
FreeBSD:
pkg install dns/dnsdist
From Source
wget https://downloads.powerdns.com/releases/dnsdist-X.Y.Z.tar.bz2
tar xf dnsdist-X.Y.Z.tar.bz2
cd dnsdist-X.Y.Z
./configure
make
sudo make install
git clone https://github.com/PowerDNS/pdns.git
cd pdns/pdns/dnsdistdist
autoreconf -i
./configure
make
Verify Installation
dnsdist --version
Quick Start
Basic Command Line
dnsdist -l 127.0.0.1:5300 9.9.9.9 2620:fe::fe 2620:fe::9
Minimal Configuration
Create dnsdist.conf:
setLocal("0.0.0.0:53")
newServer({address="9.9.9.9", name="quad9-primary"})
newServer({address="1.1.1.1", name="cloudflare"})
setServerPolicy(firstAvailable)
setACL({"0.0.0.0/0", "::/0"})
Run with configuration:
dnsdist -C dnsdist.conf
Backend Server Configuration
Adding Servers
newServer("192.168.1.10")
newServer({
address = "192.168.1.10:53",
name = "dns-primary",
qps = 100,
order = 1,
weight = 100,
retries = 2,
tcpConnectTimeout = 5,
tcpSendTimeout = 30,
tcpRecvTimeout = 30,
checkInterval = 1,
checkTimeout = 1000,
maxCheckFailures = 3,
mustResolve = true,
checkName = "health.example.com",
rise = 2,
useClientSubnet = true,
pool = "primary"
})
newServer({
address = "1.1.1.1:853",
tls = "openssl",
subjectName = "cloudflare-dns.com",
validateCertificates = true
})
newServer({
address = ,
tls = ,
dohPath = ,
subjectName =
})
Server Pools
newServer({address="192.168.1.10", pool="recursive"})
newServer({address="192.168.1.11", pool="recursive"})
newServer({address="10.0.0.50", pool="authoritative"})
addAction(QNameSuffixRule("internal.example.com"), PoolAction("authoritative"))
addAction(AllRule(), PoolAction("recursive"))
Server Management
showServers()
getServer(0):setUp()
getServer(0):setDown()
getServer(0):setQPS(50)
getServer(0):addPool("backup")
rmServer(0)
Load Balancing Policies
Built-in Policies
setServerPolicy(firstAvailable)
setServerPolicy(roundrobin)
setServerPolicy(leastOutstanding)
setServerPolicy(wrandom)
setServerPolicy(whashed)
setServerPolicy(random)
setServerPolicy(chashed)
Custom Policy
function myPolicy(servers, dq)
if dq.qname:isPartOf(newDNSName("internal.")) then
return servers[1]
end
return leastOutstanding.policy(servers, dq)
end
setServerPolicyLua("myPolicy", myPolicy)
Per-Pool Policies
getPool("recursive"):setPolicy(leastOutstanding)
getPool("authoritative"):setPolicy(roundrobin)
Rules and Actions
Rule Selectors
QNameRule("example.com")
QNameSuffixRule("example.com")
QNameSetRule(newSuffixMatchNode())
RegexRule(".*\\.example\\.com$")
QTypeRule(DNSQType.ANY)
QTypeRule(DNSQType.AXFR)
NetmaskGroupRule(nmg)
MaxQPSIPRule(10, 32, 48)
MaxQPSRule(1000)
TCPRule(true)
DNSSECRule()
RCodeRule(DNSRCode.NXDOMAIN)
RCodeRule(DNSRCode.SERVFAIL)
AndRule({QTypeRule(DNSQType.ANY), MaxQPSIPRule(1)})
OrRule({QNameRule("blocked1.com"), QNameRule("blocked2.com")})
NotRule(NetmaskGroupRule(allowedNmg))
Actions
PoolAction("poolname")
newServer({...})
DropAction()
RefusedAction()
NXDomainAction()
SpoofAction("192.168.1.1")
SpoofCNAMEAction("cname.example.com")
SetNoRecurseAction()
DelayAction(100)
TCAction()
LogAction("/var/log/queries.log")
DelayAction(500)
SetEDNSOptionAction(8, "value")
Adding Rules
addAction(MaxQPSIPRule(10), DelayAction(200))
addAction(QTypeRule(DNSQType.ANY), DropAction())
addAction(QNameSuffixRule("blocked.com"), RefusedAction())
addAction(MaxQPSIPRule(50), LogAction("/var/log/ratelimit.log", false, true))
addAction(QNameRule("priority.com"), PoolAction("fast"), 0)
addResponseAction(RCodeRule(DNSRCode.SERVFAIL), LogAction())
addCacheHitResponseAction(...)
Rule Management
showRules()
rmRule(0)
mvRule(0, 5)
clearRules()
topRules()
DNS-over-TLS (DoT)
Incoming DoT
addTLSLocal("0.0.0.0:853",
"/etc/ssl/certs/dns.example.com.pem",
"/etc/ssl/private/dns.example.com.key"
)
addTLSLocal("0.0.0.0:853",
"/etc/ssl/certs/dns.pem",
"/etc/ssl/private/dns.key",
{
minTLSVersion = "tls1.2",
ciphers = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384",
numberOfTicketsKeys = 5,
ticketKeyFile = "/etc/dnsdist/ticket.key",
sessionTickets = true,
numberOfStoredSessions = 20000
}
)
addTLSLocal("0.0.0.0:853",
{"/etc/ssl/certs/dns.ecdsa.pem", "/etc/ssl/certs/dns.rsa.pem"},
{"/etc/ssl/private/dns.ecdsa.key", "/etc/ssl/private/dns.rsa.key"}
)
Outgoing DoT
newServer({
address = "1.1.1.1:853",
tls = "openssl",
subjectName = "cloudflare-dns.com",
validateCertificates = true
})
newServer({
address = "9.9.9.9:853",
tls = "openssl",
subjectName = "dns.quad9.net",
validateCertificates = true,
caStore = "/etc/ssl/certs/quad9.pem"
})
DNS-over-HTTPS (DoH)
Incoming DoH
addDOHLocal("0.0.0.0:443",
"/etc/ssl/certs/dns.pem",
"/etc/ssl/private/dns.key",
"/dns-query"
)
addDOHLocal("0.0.0.0:443",
"/etc/ssl/certs/dns.pem",
"/etc/ssl/private/dns.key",
"/dns-query",
{
minTLSVersion = "tls1.2",
customResponseHeaders = {["X-Custom"] = "value"},
serverTokens = "dnsdist",
trustForwardedForHeader = false
}
)
addDOHLocal("127.0.0.1:8053")
Outgoing DoH
newServer({
address = "1.1.1.1:443",
tls = "openssl",
dohPath = "/dns-query",
subjectName = "cloudflare-dns.com",
validateCertificates = true
})
DNS-over-QUIC (DoQ)
addDOQLocal("0.0.0.0:853",
"/etc/ssl/certs/dns.pem",
"/etc/ssl/private/dns.key"
)
addDOH3Local("0.0.0.0:443",
"/etc/ssl/certs/dns.pem",
"/etc/ssl/private/dns.key",
"/dns-query"
)
Response Caching
Basic Cache Setup
pc = newPacketCache(100000, {
maxTTL = 86400,
minTTL = 60,
temporaryFailureTTL = 60,
staleTTL = 3600,
dontAge = false,
numberOfShards = 1,
deferrableInsertLock = true,
maxNegativeTTL = 3600,
parseECS = true
})
getPool(""):setCache(pc)
recursiveCache = newPacketCache(50000)
getPool("recursive"):setCache(recursiveCache)
Cache Management
getPool(""):getCache():printStats()
getPool(""):getCache():purgeExpired(0)
getPool(""):getCache():expunge(1000)
getPool(""):getCache():expungeByName(newDNSName("example.com"))
getPool(""):getCache():expungeByName(newDNSName("example.com"), DNSQType.A)
getPool(""):unsetCache()
DoS Protection
Dynamic Blocking Rules
local dbr = dynBlockRulesGroup()
dbr:setQueryRate(100, 10, "Query rate exceeded", 60)
dbr:setRCodeRate(DNSRCode.NXDOMAIN, 50, 10, "NXDOMAIN rate exceeded", 300)
dbr:setRCodeRate(DNSRCode.SERVFAIL, 20, 10, "SERVFAIL rate exceeded", 300)
dbr:setQTypeRate(DNSQType.ANY, 5, 10, "ANY query rate exceeded", 60)
dbr:setResponseByteRate(10000000, 10, "Bandwidth exceeded", 60)
dbr:excludeRange(newNMG({"10.0.0.0/8", "192.168.0.0/16"}))
function maintenance()
dbr:apply()
end
Manual Blocking
addDynamicBlock(newCA("192.0.2.1"), "Manual block", 3600)
addDynamicBlock(newCA("192.0.2.0/24"), "Subnet block", 3600, DNSAction.Refused)
showDynBlocks()
clearDynBlocks()
rmDynBlocks(newCA("192.0.2.1"))
eBPF Filtering (High Performance)
bpf = newBPFFilter({
ipv4MaxItems = 100000,
ipv6MaxItems = 100000,
qnamesMaxItems = 10000,
mapDir = "/sys/fs/bpf/dnsdist"
})
bpf:attachToAllBinds()
dynbpf = newDynBPFFilter(bpf)
dbr:setQueryRate(100, 10, "Rate exceeded", 60, DNSAction.Drop, 0, 0, dynbpf)
bpf:block(newCA("192.0.2.1"))
bpf:blockQName(newDNSName("blocked.com"), DNSQType.ANY)
bpf:unblock(newCA("192.0.2.1"))
bpf:getStats()
Access Control
setACL({"10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"})
addACL("203.0.113.0/24")
showACL()
setProxyProtocolACL({"10.0.0.1/32", "10.0.0.2/32"})
Webserver and API
Enable Webserver
webserver("127.0.0.1:8083")
webserver("127.0.0.1:8083")
setWebserverConfig({
password = hashPassword("supersecret"),
apiKey = hashPassword("apikey123"),
acl = "127.0.0.1/8, ::1/128",
dashboardRequiresAuthentication = true,
statsRequireAuthentication = false
})
setAPIWritable(true, "/etc/dnsdist/acl-updates/")
API Endpoints
| Endpoint | Method | Description |
|---|
/ | GET | Dashboard HTML |
/jsonstat?command=stats | GET | Statistics JSON |
/jsonstat?command=dynblocklist | GET | Dynamic blocks |
/metrics | GET | Prometheus metrics |
/api/v1/servers/localhost | GET | Server overview |
/api/v1/servers/localhost/config/allow-from | PUT | Update ACL |
/api/v1/cache | DELETE | Purge cache entries |
Example API Usage
curl -H "X-API-Key: apikey123" http://127.0.0.1:8083/jsonstat?command=stats
curl http://127.0.0.1:8083/metrics
curl -X DELETE -H "X-API-Key: apikey123" \
"http://127.0.0.1:8083/api/v1/cache?pool=&name=example.com"
Console Management
Enable Console
controlSocket("127.0.0.1:5199")
setKey("base64encodedkey==")
Connect to Console
dnsdist -c 127.0.0.1:5199
dnsdist -c 127.0.0.1:5199 -k "base64encodedkey=="
Console Commands
showServers()
showPools()
getServer(0):isUp()
showRules()
showResponseRules()
topRules()
showBinds()
showTCPStats()
showTLSErrorCounters()
showDOHFrontends()
showDOHResponseCodes()
getPool(""):getCache():printStats()
grepq("example.com")
topQueries(20)
topResponses(20, DNSRCode.NXDOMAIN)
topClients(20)
showDynBlocks()
clearDynBlocks()
dumpStats()
delta()
quit() / exit()
Logging and Metrics
Query Logging
addAction(AllRule(), LogAction("/var/log/dnsdist/queries.log", false, true))
addAction(MaxQPSIPRule(10), LogAction("/var/log/ratelimit.log"))
dnstapLog = newFrameStreamUnixLogger("/var/run/dnstap.sock")
addAction(AllRule(), DnstapLogAction("dnsdist", dnstapLog))
addResponseAction(AllRule(), DnstapLogResponseAction("dnsdist", dnstapLog))
Carbon/Graphite Export
carbonServer("graphite.example.com:2003", "dnsdist.node1", 10)
Prometheus Metrics
Access at http://server:8083/metrics:
# HELP dnsdist_queries Number of queries
# TYPE dnsdist_queries counter
dnsdist_queries 12345678
# HELP dnsdist_responses Number of responses
# TYPE dnsdist_responses counter
dnsdist_responses 12345670
# HELP dnsdist_servfail_responses ServFail responses
# TYPE dnsdist_servfail_responses counter
dnsdist_servfail_responses 100
# HELP dnsdist_latency_avg Average latency (usec)
# TYPE dnsdist_latency_avg gauge
dnsdist_latency_avg 1234.5
Configuration Examples
High-Availability DNS Load Balancer
setLocal("0.0.0.0:53")
addTLSLocal("0.0.0.0:853", "/etc/ssl/dns.pem", "/etc/ssl/dns.key")
addDOHLocal("0.0.0.0:443", "/etc/ssl/dns.pem", "/etc/ssl/dns.key", "/dns-query")
newServer({
address = "192.168.1.10:53",
name = "dns1",
checkName = "health.internal.",
checkInterval = 1,
maxCheckFailures = 3,
rise = 2,
pool = "recursive"
})
newServer({
address = "192.168.1.11:53",
name = "dns2",
checkName = "health.internal.",
checkInterval = 1,
maxCheckFailures = 3,
rise = 2,
pool = "recursive"
})
setServerPolicy(leastOutstanding)
pc = newPacketCache(500000, {
maxTTL = 86400,
minTTL = 60,
staleTTL = 3600
})
getPool("recursive"):setCache(pc)
local dbr = dynBlockRulesGroup()
dbr:setQueryRate(100, 10, "Query flood", 60)
dbr:setRCodeRate(DNSRCode.NXDOMAIN, 50, 10, "NXDOMAIN flood", 300)
function maintenance()
dbr:apply()
setACL({, })
webserver()
setWebserverConfig({password = hashPassword()})
controlSocket()
Split-Horizon DNS
newServer({address = "10.0.0.53", pool = "internal"})
newServer({address = "10.0.0.54", pool = "internal"})
newServer({address = "9.9.9.9", pool = "external"})
newServer({address = "1.1.1.1", pool = "external"})
addAction(QNameSuffixRule("internal.example.com"), PoolAction("internal"))
addAction(QNameSuffixRule("corp.example.com"), PoolAction("internal"))
addAction(AllRule(), PoolAction("external"))
getPool("internal"):setPolicy(roundrobin)
getPool("external"):setPolicy(leastOutstanding)
DNS Firewall with Blocklist
blockedDomains = newSuffixMatchNode()
for line in io.lines("/etc/dnsdist/blocklist.txt") do
blockedDomains:add(newDNSName(line))
end
addAction(SuffixMatchNodeRule(blockedDomains), SetTagAction("blocked", "malware"))
addAction(TagRule("blocked"), NXDomainAction())
addAction(QTypeRule(DNSQType.ANY), DropAction())
addAction(QTypeRule(DNSQType.AXFR), RefusedAction())
addAction(TagRule("blocked"), LogAction("/var/log/blocked.log"))
Troubleshooting
Debug Commands
setVerbose(true)
setVerboseHealthChecks(true)
showServers()
getServer(0):getDrops()
getServer(0):getLatencyAvg()
getPool(""):getCache():printStats()
showTLSErrorCounters()
grepq("problem.domain.com", 100)
topQueries(20)
topSlowResponses(20)
Common Issues
| Issue | Diagnosis | Solution |
|---|
| Server marked down | showServers() - check healthcheck | Verify backend reachable, check checkName |
| High latency | getServer(N):getLatencyAvg() | Add more backends, enable caching |
| Cache misses | getCache():printStats() | Increase cache size, check minTTL |
| DoH not working | showTLSErrorCounters() | Verify certificates, check SNI |
| Rate limiting | showDynBlocks() | Adjust thresholds, add exclusions |
Resources