Detecting DNS Tunnelling and Suspicious DNS Query Patterns
For developers and platform engineers who need to spot data exfiltration and command-and-control hiding in DNS traffic. This guide shows where to instrument DNS, what query patterns actually matter, and how to turn logs into practical detections without drowning in false positives.
TL;DR — DNS tunnelling usually shows up as lots of high-entropy subdomains, unusually long labels, odd record types like TXT, and repeated NXDOMAINs to one domain. The single most useful first step is to log recursive DNS queries centrally and alert on per-client patterns: long labels, high unique-subdomain counts, high NXDOMAIN ratio, and uncommon qtypes. Reading time: ~7 min
What it is and where it sits
DNS tunnelling is not a DNS-server feature; it is an abuse pattern in the part of your network that resolves names. An endpoint, container, or compromised workload encodes data into DNS queries or responses because DNS is often allowed out even when other egress is blocked. From a network-segmentation angle, the important point is this: the tunnel usually crosses your recursive resolver or egress DNS control point, not your authoritative DNS unless you host the attacker-controlled zone.
In a typical enterprise or cloud setup, the actors are:
- client workload: laptop, VM, pod, container
- recursive resolver: BIND, Unbound, dnsmasq, Active Directory DNS, cloud-provided resolver
- upstream/Internet authoritative servers
- your logging pipeline/SIEM
- optionally an egress firewall that should force all DNS through approved resolvers
What it replaces: often, nothing legitimate. A tunnel is usually substituting for blocked HTTP(S), SSH, or direct TCP exfiltration. In malware cases it may replace normal command-and-control transport. In some environments, legitimate tools also generate weird DNS: service discovery systems, antivirus reputation lookups, DKIM/SPF checks, ACME challenges, and some CDN or telemetry clients.
View as text
[Workload] ---- 1. query abc123.data.attacker.tld ----> [Resolver] [Workload] <---- 2. response TXT/NULL/CNAME ---- [Resolver] [Resolver] ---- 3. recurse to attacker auth ----> [Internet DNS] [Resolver] ---- 4. query logs ----> [SIEM] [Firewall] ---- 5. allow only DNS to resolver ----> [Resolver]
If you do not control the recursive path, detection gets much weaker. Packet capture on one host can help for incident response, but durable detection usually lives on the recursive resolvers and egress controls.
How it actually works
A realistic exfiltration flow looks like this:
- Malware on a workstation reads a secret, say a cloud credential file.
- It base32-encodes chunks of that data into DNS-safe labels.
- It sends queries like
mfrggzdfmztwq2lk...chunk01.exfil.attacker.tld. - Your recursive resolver forwards them to the authoritative nameserver for
attacker.tld. - The attacker reconstructs the chunks from query names. Responses may be irrelevant, or they may carry commands back in TXT records.
What you can observe:
- very long left-most labels, often near the 63-byte DNS label limit
- many unique subdomains under one parent domain
- high entropy strings:
x9q2m1a8k...instead ofapi,cdn,mail - bursts from one source host
- unusual qtypes: TXT is common for command return traffic; NULL is rare but some tunnel tools use it; CNAME and MX can also be abused
- high NXDOMAIN or SERVFAIL rates when the tool is sloppy or probing
End-to-end example
Assume a Linux host 10.20.4.15 uses your Unbound resolver at 10.20.0.53. You suspect DNS tunnelling.
First, look at live query logs on the resolver. In Unbound, with query logging enabled, the shape is typically like this:
sudo journalctl -u unbound -f
Oct 30 10:14:02 dns01 unbound[812]: [812:0] info: 10.20.4.15 m4xw6ytboi======.a1.exfil-example.com. TXT IN
Oct 30 10:14:02 dns01 unbound[812]: [812:0] info: 10.20.4.15 orsxg5a=.a2.exfil-example.com. TXT IN
Oct 30 10:14:03 dns01 unbound[812]: [812:0] info: 10.20.4.15 on2he2lom4======.a3.exfil-example.com. TXT IN
Oct 30 10:14:03 dns01 unbound[812]: [812:0] info: 10.20.4.15 p5xgk3tdn5wq====.a4.exfil-example.com. TXT IN
That pattern is suspicious because:
- same client IP
- same parent domain
- every query is unique
- labels look encoded, not human-named
- qtype TXT is repeated
Now compare with a normal application domain. A healthy service often repeats a small set of names and gets cache hits. Tunnel traffic tends to defeat caching by making every query unique.
You can validate from a packet capture if needed:
sudo tcpdump -ni any 'udp port 53 or tcp port 53' -vv
Typical output shape:
10:14:02.118941 IP 10.20.4.15.53842 > 10.20.0.53.53: 41231+ [1au] TXT? m4xw6ytboi======.a1.exfil-example.com. (73)
10:14:02.121203 IP 10.20.0.53.53 > 10.20.4.15.53842: 41231 1/0/1 TXT "ok" (89)
If you want to test your detector against a known suspicious shape without deploying a tunnel tool, generate a long random label:
python3 - <<'PY'
import os, base64
s = base64.b32encode(os.urandom(30)).decode().rstrip('=').lower()
print(f"{s}.test.example.org")
PY
Then query it:
dig @10.20.0.53 -t TXT q5r6x7y8z9abcdefghijklmnopqrstuv.test.example.org +short
dig exit behavior matters when you automate checks: it often exits 0 even for NXDOMAIN; you must parse the status line, not just the exit code.
dig @10.20.0.53 suspicious.example.invalid
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 48822
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
For response-side tunnelling, inspect TXT answer sizes and frequency. A single large TXT response is not enough; SPF, DKIM, and verification systems use TXT legitimately. The suspicious signal is repeated back-and-forth TXT traffic with encoded-looking names from one client.
When to use it (and when not to)
You should treat DNS tunnel detection as an egress-control and anomaly-detection layer, not as your only malware defense.
| Scenario | Recommendation |
|---|---|
| You run central recursive resolvers for office, VPN, servers, or Kubernetes nodes | Do it. This is the highest-value place to detect and block suspicious DNS patterns. |
| You already force all DNS through approved resolvers with firewall rules | Do it now; your visibility is good and false negatives are lower. |
Endpoints can query 8.8.8.8, DoH endpoints, or arbitrary outbound 53/853/443 | Fix egress first. Detection on your resolver will miss bypass traffic. |
| Small internal app with no sensitive data and no central DNS logs | You probably don't need a full detection program yet; start by forcing DNS through one resolver and retaining logs for incident response. |
| You only host authoritative DNS for your public domains | Limited value for internal exfil detection. You can detect abuse against your own zones, but not general outbound tunnelling. |
| You have lots of service discovery, mesh, or telemetry systems creating random-ish names | Use detection, but baseline first and tune aggressively to avoid false positives. |
You probably do not need this as a separate project if your bigger gap is simpler: laptops and workloads can still make direct outbound HTTPS to anywhere, or DNS-over-HTTPS is unrestricted. In that case, segmentation and egress policy will reduce risk more than fancy DNS analytics.
Trade-offs
Every useful DNS detection control comes with cost.
- Central DNS logging gives visibility.
- Cost: storage, privacy review, and operational overhead. Query logs can expose internal hostnames and user behavior.
- Blocking long/high-entropy domains can stop tunnels quickly.
- Cost: false positives from CDNs, security products, tracking domains, and some SaaS verification flows.
- Alerting on uncommon qtypes like TXT/NULL catches many tunnel tools.
- Cost: TXT is heavily used legitimately for SPF, DKIM, DMARC, domain verification, and ACME-related workflows.
- Forcing all DNS to approved resolvers improves coverage.
- Cost: more firewall policy, breakage for software expecting direct resolver access, and more pain in hybrid/cloud networks.
- Deep packet inspection of DNS payloads improves fidelity.
- Cost: CPU, complexity, and less value if clients bypass with DoH/DoT over 443/853.
- Managed DNS security services reduce build effort.
- Cost: vendor lock-in and less control over raw detection logic.
The practical balance in 2026 is still: centralize recursive DNS, log queries, block direct outbound DNS except to approved resolvers, and add a small set of high-signal detections before trying ML or heavy analytics.
In practice
Example 1: Unbound query logging for detection input
⚠️ Enabling full query logging on a busy resolver can generate a lot of I/O and may expose sensitive internal names. Roll it out on one resolver first and check disk usage before enabling fleet-wide.
# /etc/unbound/unbound.conf.d/logging.conf
server:
verbosity: 1
log-queries: yes
log-replies: no
logfile: "/var/log/unbound/unbound.log"
use-syslog: no
Apply it:
sudo unbound-checkconf && sudo systemctl reload unbound
sudo tail -f /var/log/unbound/unbound.log
This turns on per-query logging so you can count unique subdomains, qtypes, and NXDOMAIN-heavy clients. Gotcha: log-replies: yes is tempting, but it increases volume sharply; start with queries only.
Example 2: Quick suspicious-domain hunt from resolver logs
awk '{print $(NF-1), $NF}' /var/log/unbound/unbound.log \
| sed 's/\.$//' \
| awk '
{
qtype=$1; name=$2;
n=split(name, a, ".");
if (n >= 3) {
parent=a[n-1] "." a[n];
left=a[1];
if (length(left) > 40 || qtype == "TXT" || qtype == "NULL") {
print qtype, name;
}
}
}' \
| sort | uniq -c | sort -nr | head -50
This surfaces domains with long first labels and uncommon qtypes, which catches a lot of low-to-mid sophistication tunnelling. Gotcha: it is intentionally crude; domain-verification and email-related TXT records will appear, so review by client IP and frequency before blocking.
Example 3: Force DNS through approved resolvers with nftables
⚠️ This can break workloads immediately if they rely on direct access to public resolvers or DNS-over-TLS. Test from a non-critical subnet first.
sudo nft add table inet filter
sudo nft 'add chain inet filter output { type filter hook output priority 0; policy accept; }'
sudo nft add rule inet filter output udp dport 53 ip daddr != 10.20.0.53 drop
sudo nft add rule inet filter output tcp dport 53 ip daddr != 10.20.0.53 drop
Verify with dig:
dig @8.8.8.8 example.com
Typical failure shape after the rule:
;; communications error to 8.8.8.8#53: timed out
;; no servers could be reached
This enforces the architecture assumption behind DNS-tunnel detection: all classic DNS must traverse your resolver. Gotcha: this does nothing against DoH over port 443; you need separate egress controls or proxy policy for that.
Further reading
- RFC 1035
- Unbound documentation, the logging and local-zone sections
- BIND 9 Administrator Reference Manual, query logging
- SANS DFIR, DNS Tunneling articles
- Zeek documentation, the DNS protocol analyzer
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI