Detection and response
DNS sinkhole
A resolver that answers known-bad domains with a harmless address or NXDOMAIN, blocking them for every client.
Because a company's machines all resolve through one resolver, it is the natural place to enforce a blocklist. A sinkhole makes the resolver hand back a controlled address for a bad domain — often a web server that logs each arrival, giving a second list of infected hosts — or refuse the name with NXDOMAIN, a harder stop that keeps no arrival log. Either way the resolver's query log already names which internal host asked, even in logging-only mode.
How a DNS sinkhole works, step by step
In the DNS sinkhole lab two PCs, infectedPc (10.0.1.10) and cleanPc
(10.0.1.11), resolve through one company resolver (10.0.1.12, unbound). The malware on infectedPc calls home
to c2.evil, served by badNs (10.0.2.11). The sinkhole web server is 10.0.2.13.
- Every lookup passes the resolver. DHCP or static configuration puts
nameserver 10.0.1.12in each machine's/etc/resolv.conf, and the firewall should stop machines from asking anyone else on port 53. - The resolver logs each question with the client address, name, type and, with replies logged, the response code. In logging-only mode nothing is blocked yet, but the log already names the infected machine.
- A policy takes over a domain. The resolver gets a local zone for
evil.. With theredirecttype, every name underevil(c2.evil,panel.evil, any name the malware generates) gets the answer you set:evil. A 10.0.2.13. The question never reaches the internet. - Old answers are flushed. A cached
c2.evil A 10.0.2.11would live on until its TTL ran out, so the policy is followed by a flush of the zone. - The malware connects to the sinkhole. Its beacon, an HTTP request to
c2.evil, now lands on your own server, which answers a harmless page and logs the source address: a second list of infected machines, confirmed by traffic. - Or the name is refused. The
always_nxdomaintype answers "no such name". The beacon cannot even start, and the arrival log is lost.
Production resolvers load these rules in bulk from a response policy zone, fed by threat intelligence lists, rather than one command at a time. Public filtering resolvers do the same for everyone who uses them: see filtering resolver.
What it looks like on a Linux machine
unbound writes one line per query and one per reply to /var/log/unbound.log. The client address
comes first:
$ grep evil /var/log/unbound.log | tail -2
Oct 10 18:19:55 unbound[111:0] info: 10.0.1.10 c2.evil. A IN
Oct 10 18:19:55 unbound[111:0] info: 10.0.1.10 c2.evil. A IN NOERROR 0.000453 0 52
The reply line adds the response code, the time taken, whether it came from the cache (1) or not (0), and the
size. The sinkhole is three commands, applied while unbound runs:
$ unbound-control local_zone evil. redirect
ok
$ unbound-control local_data "evil. A 10.0.2.13"
ok
$ unbound-control flush_zone evil.
ok removed 1 rrsets, 1 messages and 0 key entries
On the infected PC both names now resolve to the sinkhole, and the beacon gets the blocked page:
$ dig +short c2.evil; dig +short panel.evil
10.0.2.13
10.0.2.13
$ curl -s -m3 http://c2.evil/
blocked by security policy
The hard block instead:
$ unbound-control local_zone evil. always_nxdomain
ok
$ dig c2.evil | grep -E 'status|ANSWER:'
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 60070
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
$ curl -s -m3 http://c2.evil/; echo rc=$?
rc=6
Exit code 6 is curl's "could not resolve host". The NXDOMAIN carries no SOA record, which tells a local block from a
name that really does not exist. Rules added with unbound-control vanish on restart; permanent ones go in
unbound.conf as local-zone: and local-data: lines.
What goes wrong, and how you see it
The block does not take. The policy is in, but dig +short c2.evil still prints 10.0.2.11: the old
answer is cached. unbound-control flush_zone evil. clears it.
Clients go around the resolver. Malware that asks 8.8.8.8 directly, or uses DNS over HTTPS, never meets the
policy, and the query log goes quiet while the beacon still works. Block outbound port 53 except from the resolver,
and watch for DoH endpoints.
The rule is too wide. A local zone covers every name under it, so blocking a shared parent domain cuts off
legitimate sites too. dig +short www.site from a clean PC is the check that only the bad domain is gone.
The log names the resolver, not the PC. If another server forwards to yours, every query arrives from the forwarder's address. Read the log on the server the clients ask.
Questions people ask
Sinkhole or NXDOMAIN? A sinkhole address keeps a record of every machine that still tries to connect, and can show users a notice. NXDOMAIN is simpler and stops the connection earlier, but leaves only the query log.
Is a DNS sinkhole the same as Pi-hole? Pi-hole is a DNS sinkhole for ads and trackers on a home network: it
answers blocked names with 0.0.0.0 or NXDOMAIN. The mechanism is the same; the list is different.
Can a sinkhole stop data theft over DNS? Only for domains it knows. A tunnel needs its queries to reach the attacker's name server; a policy on that domain, or a drop rule for that server as in the DNS exfiltration lab, ends it. See DNS tunneling.