Security Engineering · Network Security
Detecting DNS Tunnels from Query Structure
How DNS becomes a bidirectional data channel, which structural properties of queries carry the signal, and why entropy alone is a weak discriminator against legitimate naming schemes.
DNS is the one protocol almost every network permits outbound, because name resolution is a prerequisite for everything else. It is also a request-response protocol with an arbitrary-length payload slot: a query name can carry data to the resolver, and a response can carry data back in a record of the attacker’s choosing. That combination makes it a general transport, and the transport requires no listening service on the client, no inbound connection, and no port other than 53. Every DNS tunnel detection problem follows from the fact that the channel is legitimate protocol usage.
The channel and its capacity
An attacker registers a domain and runs an authoritative server for it. A client inside the network encodes a chunk of data into a label of a query name and asks a recursive resolver to resolve it. The resolver, having no cached answer, forwards the query to the authoritative server, which decodes the label and replies. The reply can carry data in a TXT record, in a CNAME chain, or in the address fields of an A or AAAA record.
The capacity is bounded by the name format. RFC 1035 limits a label to 63 octets and a full domain name to 255 octets in its wire encoding. The client controls the portion left of the delegated zone name, so a short tunnel domain leaves roughly 200 octets of payload per query before encoding overhead. Base32 encoding costs 5 bits per character, so 8 bits of data occupy 1.6 characters; base64 costs 6 bits per character but is not case-preserving, which matters because DNS comparisons are case-insensitive by specification. Many implementations therefore use base32 or a custom alphabet with case-folding tolerance.
Per query the throughput is small, but queries are cheap and can be issued in parallel. A tunnel that sustains a few hundred queries per second achieves a data rate in the tens of kilobytes per second: sufficient for interactive command and control and for slow exfiltration, insufficient for bulk transfer. The design choice a tunnel author makes is therefore between speed and visibility: fewer, larger queries produce a distinctive size distribution; many small queries blend into the noise but require more time.
Responses matter as much as queries. A TXT record can hold a much larger payload than a name, subject to the record size limits and to fragmentation at the transport layer, and EDNS(0) advertises a larger UDP payload size than the 512-byte default of RFC 1035, which lets a single response carry more. RFC 6891 is what makes large responses routine, and a tunnel that wants downstream bandwidth uses it.
Structural properties that carry the signal
Detection works on the shape of the query rather than its content, since the content is encrypted or encoded by design. Four properties are measurable from a single query, and a fifth requires aggregating many.
Label length distribution. Human-authored names have short labels: a hostname, a service prefix, a zone. Encoded payload produces labels that cluster near the maximum, because the encoder fills each label before moving to the next. A distribution of label lengths that is concentrated at 60 to 63 octets is a strong structural signal, and it is one that legitimate schemes rarely produce.
Character composition. Base32 output uses a 32-character alphabet, so digits and letters appear with roughly uniform frequency and no English digraph structure appears. A measure of entropy over the label alphabet distinguishes this from words, but only weakly, because many legitimate naming schemes are also high-entropy: content delivery hostnames, cloud-generated instance names, and hash-based shard names all look similar. Entropy is a useful filter and a poor classifier.
Unique subdomain count. A tunnel generates a new label for each chunk, so the number of distinct names under one parent zone grows without bound. A legitimate service resolves a bounded set of names. Counting distinct labels per parent zone over a window is therefore the single most discriminative aggregate feature, because it does not depend on what the labels look like.
Query type mix. Most legitimate client traffic is A and AAAA, with some PTR for reverse lookups, MX for mail, and SRV for service discovery. A high proportion of TXT queries to one zone, or a high proportion of NULL-type queries, a type historically used by tunnel implementations because its handling is inconsistent across resolvers, is anomalous. So is a query for a name under a zone that the network has no business resolving.
Response size relative to query. A tunnel response carries data; a legitimate negative or address response is small. The ratio of response bytes to query bytes, aggregated per zone, separates downstream-heavy tunnels from ordinary resolution.
A detector that uses the cheap signals first
Ordering matters, because the volume of DNS traffic makes an expensive test per query unaffordable. A practical detector applies cheap structural filters, then aggregates.
from collections import defaultdict, Counter
from math import log2
from statistics import median
def label_entropy(label):
counts = Counter(label)
return -sum((c / len(label)) * log2(c / len(label)) for c in counts.values())
def score_query(name, qtype):
"""Per-query structural signals. Cheap enough to run on every query."""
payload = name.split('.')[0]
score = 0
if len(payload) >= 50: # encoders fill each label before moving on
score += 2
if qtype in ('TXT', 'NULL'):
score += 1
if label_entropy(payload) > 4.0: # a 32-symbol alphabet tops out at 5 bits
score += 1
return score
def per_zone_window(events, window=300):
"""Aggregates decide; single queries only nominate candidates."""
zones = defaultdict(list)
for ts, name, qtype in events:
parent = '.'.join(name.split('.')[-2:])
zones[(parent, ts // window)].append(name)
for (parent, _), names in zones.items():
distinct = len(set(names))
if distinct > 500 and median(len(n.split('.')[0]) for n in names) > 40:
yield parent, distinct
The thresholds in that sketch are illustrative, not calibrated; the point is the structure of the test. Per-query scoring produces candidates, and the decision is taken on aggregates per parent zone per time window, because a single long label is unremarkable while ten thousand distinct long labels under one zone in five minutes is not.
The entropy test is the weak link, and it is worth being explicit about why. A threshold on letter statistics fails against names that are legitimately random, and against any tunnel that encodes in a way that mimics natural language. The reliable version of the question is not “does this label look random” but “is this label drawn from the set of labels this client has used before”, which is stateful and requires per-client history.
Where detection breaks
Resolution happens off-path. If clients use an external resolver over an encrypted protocol, the internal sensor sees neither the query nor the response. DNS over TLS and DNS over HTTPS move the channel to port 853 or to an HTTPS endpoint, and a network that permits those has removed its own visibility. The detection question becomes a policy question: which resolvers may clients use, and is that enforced rather than configured.
Legitimate high-entropy names. Anti-abuse and tracking domains encode identifiers into subdomains, and some update and telemetry schemes use per-request unique labels. These produce exactly the aggregate signature of a tunnel. The distinguishing feature is not the structure but the destination: a known vendor zone with a bounded label alphabet is different from an unclassified zone receiving uniformly random labels, and zone reputation is doing the work that structure cannot.
Low-and-slow tunnels. A tunnel that issues a few queries per minute stays under any count-based threshold. Catching it requires the periodic-structure analysis described in Detecting Periodic Beacons in Flow Telemetry, applied to query timing rather than to connection timing, plus the retention to compare against a baseline from weeks earlier.
Encrypted payloads inside the encoding. The label content may itself be ciphertext, so no amount of content analysis helps; the structural features survive because they describe the encoding, not the plaintext. That is the reason structural detection is the right frame and content inspection is not.
What a positive result means
A zone flagged by these tests is a zone whose query pattern is consistent with a data channel. It is not proof that a host is compromised, because the same pattern appears whenever a service encodes identifiers into names. The productive follow-up is to identify which clients queried the zone, whether those clients have any business resolving it, and whether the timing correlates with anything else in the environment. The zone’s registration age and its authoritative infrastructure are useful and are external facts, not derivable from the query log.
The channel also interacts with the segmentation design. A tunnel succeeds because the client can reach a resolver and the resolver can reach the internet, and the second half is often the unexamined permission. Constraining which resolvers may recurse to external authorities is the policy question that VLAN Segmentation and the Limits of Layer 2 Isolation frames at the segment level, and it removes the channel for every client that is not supposed to have it, which is a stronger control than any detector.