Malware Detection¶
Ferrous DNS is the first self-hosted DNS server with built-in malware detection. It identifies and blocks DNS-based attacks in real time — no external service, no cloud dependency, no subscription required.
Five detection engines run independently:
- DNS Tunneling Detection — catches malware using DNS as a covert data channel for C2 communication or data exfiltration
- DGA Detection — identifies Domain Generation Algorithm malware by analyzing the statistical properties of second-level domains (entropy, consonant ratio, bigram score)
- DNS Rebinding Protection — blocks attacks that trick browsers into accessing internal network resources
- NXDomain Hijack Detection — detects and neutralizes ISP interception of NXDOMAIN responses, restoring correct DNS behavior
- Response IP Filtering — downloads C2 IP threat feeds and blocks DNS responses that resolve to known command-and-control server IPs
No other self-hosted DNS server (Pi-hole, AdGuard Home, Blocky, Technitium, CoreDNS) offers these capabilities natively.
DNS Tunneling Detection¶
What Is DNS Tunneling?¶
DNS tunneling encodes arbitrary data inside DNS queries and responses, turning your DNS server into an unwitting proxy. Malware uses this technique because DNS traffic is almost never blocked by firewalls — port 53 is open on virtually every network.
How it works: the malware encodes stolen data (passwords, files, keystrokes) into subdomain labels of a domain controlled by the attacker. The attacker's authoritative DNS server decodes the data from the query and sends commands back in the response.
Normal DNS query:
www.google.com → A 142.250.80.46
Tunneling query (data exfiltration):
aGVsbG8gd29ybGQ.x3j9k.evil.com → TXT "cmd:sleep 30"
╰── base64 data ──╯ ╰─ C2 ─╯
Real-World Examples¶
Cobalt Strike / Sliver C2¶
Cobalt Strike and Sliver are the most widely used C2 frameworks in real-world attacks. Both support DNS as a transport channel, generating queries like:
aabb0011.cdn-check.attacker.com A
cc22dd33.cdn-check.attacker.com A
ee44ff55.cdn-check.attacker.com TXT
post.aabb0011cc22dd33.cdn-check.attacker.com A
Each query carries a fragment of the C2 payload. Ferrous DNS detects this pattern through a combination of high query rate, high unique subdomain count, and elevated entropy in the subdomain labels.
iodine / dnscat2¶
Open-source tunneling tools like iodine and dnscat2 create a full IP-over-DNS tunnel. They generate NULL record queries with very long, high-entropy labels:
t3RoaXMgaXMgYSBzZWNyZXQgbWVzc2FnZQ.t.example.com NULL
dAABAAABAAAAAAAACnR1bm5lbGRhdGE.t.example.com NULL
zLy8vYmFzZTY0ZW5jb2RlZGRhdGFmb3.t.example.com NULL
Ferrous DNS catches these immediately: NULL record type is blocked on the hot path, labels exceed 50 characters, and the FQDN exceeds 120 bytes.
Data Exfiltration via TXT Records¶
Malware can exfiltrate sensitive data by encoding it in subdomain labels and reading the TXT response:
Y3JlZGVudGlhbHM6YWRtaW46cGFzc3dvcmQxMjM.exfil.bad.com TXT
c2Vuc2l0aXZlLWRhdGEtaGVyZQ.exfil.bad.com TXT
ZmlsZS1jb250ZW50cy1oZXJl.exfil.bad.com TXT
The background analysis engine detects this pattern: high TXT query proportion (>5% of traffic to one apex), many unique subdomains, and high Shannon entropy in the labels.
DGA Malware (Conficker, Mirai, Emotet)¶
Domain Generation Algorithm malware generates thousands of random domain names per day, connecting to whichever one the attacker has registered:
xkjf8sdf.com A → NXDOMAIN
qwer7hjk.net A → NXDOMAIN
mn3kl9sd.org A → NXDOMAIN
p8fj2ksl.com A → resolved (C2 server)
Ferrous DNS detects this through a high NXDOMAIN ratio (>20%) and elevated query rate for random apex domains from a single client subnet.
How Detection Works¶
Ferrous DNS uses a two-phase architecture that adds zero overhead to normal DNS traffic:
DNS Query arrives
│
▼
┌─────────────────────────────────┐
│ Phase 1 — Hot Path (O(1)) │
│ │
│ ✓ FQDN length > 120 bytes? │ ~20-50ns
│ ✓ Label length > 50 bytes? │
│ ✓ NULL record type? │
│ ✓ Domain flagged by Phase 2? │ ~100-200ns (DashMap lookup)
│ │
│ Block immediately if detected │
└────────────┬────────────────────┘
│ pass
▼
Normal resolution
│
▼
┌─────────────────────────────────┐
│ Phase 2 — Background Analysis │
│ │
│ ◆ Shannon entropy of labels │
│ ◆ Query rate per apex domain │
│ ◆ Unique subdomain count │
│ ◆ TXT query proportion │
│ ◆ NXDOMAIN ratio │
│ │
│ Confidence score > 0.7? │
│ → Flag domain for Phase 1 │
└─────────────────────────────────┘
Phase 1 runs inline on every query with zero heap allocations. It checks structural anomalies (oversized labels, NULL records) and whether the domain was previously flagged by the background analysis.
Phase 2 runs in a separate async task, consuming events from the hot path via a bounded channel. It maintains per-client/apex statistics using lock-free atomic counters and a DashMap, computes a weighted confidence score, and flags domains that exceed the threshold.
Confidence Scoring¶
Each signal contributes a weight to the overall confidence score:
| Signal | Weight | Threshold | What It Catches |
|---|---|---|---|
| Shannon entropy | 0.30 | > 3.8 bits/char | Base64/hex encoded data in labels |
| Query rate | 0.25 | > 50/min per apex | Rapid-fire C2 beaconing |
| Unique subdomains | 0.25 | > 30/min per apex | Data exfiltration (each query = new subdomain) |
| TXT proportion | 0.10 | > 5% of traffic | TXT-based tunneling (iodine, dnscat2) |
| NXDOMAIN ratio | 0.10 | > 20% for an apex | DGA malware probing random domains |
When the combined score exceeds the confidence_threshold (default 0.7), the apex domain is flagged. All subsequent queries to that domain are blocked on the hot path.
Explainable alerts
Every detection includes the signal that triggered it, the measured value, and the configured threshold. This means you can see why a domain was flagged — not just a generic "blocked" message.
Configuration¶
DNS tunneling detection is enabled by default with sensible thresholds that catch real attacks without false positives on normal traffic.
[dns.tunneling_detection]
enabled = true # Master switch
action = "block" # "alert" | "block" | "throttle"
# Phase 1 — hot path checks (O(1), ~50ns)
max_fqdn_length = 120 # Block FQDNs longer than 120 bytes
max_label_length = 50 # Block labels longer than 50 bytes
block_null_queries = true # Block NULL (type 10) record queries
# Phase 2 — background statistical analysis
entropy_threshold = 3.8 # Shannon entropy (bits/char)
query_rate_per_apex = 50 # Queries/min per client+apex pair
unique_subdomain_threshold = 30 # Unique subdomains/min per client+apex pair
txt_proportion_threshold = 0.05 # TXT > 5% of client traffic
nxdomain_ratio_threshold = 0.20 # NXDOMAIN > 20% for a client+apex pair
confidence_threshold = 0.7 # Minimum score to flag (0.0–1.0)
stale_entry_ttl_secs = 300 # Eviction of idle tracking entries
# Exemptions
domain_whitelist = [] # Domains exempt from detection
client_whitelist = [] # Client CIDRs exempt from detection
| Option | Default | Description |
|---|---|---|
enabled | true | Master switch for tunneling detection |
action | block | Action when tunneling is detected: block (REFUSED), alert (log only), or throttle (planned) |
max_fqdn_length | 120 | Maximum FQDN length in bytes before triggering |
max_label_length | 50 | Maximum single label length in bytes before triggering |
block_null_queries | true | Block NULL record type queries (used by iodine, dnscat2) |
entropy_threshold | 3.8 | Shannon entropy threshold in bits/char for subdomain labels |
query_rate_per_apex | 50 | Maximum queries per minute per client subnet + apex domain |
unique_subdomain_threshold | 30 | Maximum unique subdomains per minute per client subnet + apex domain |
txt_proportion_threshold | 0.05 | Maximum proportion of TXT queries (5% default) |
nxdomain_ratio_threshold | 0.20 | Maximum NXDOMAIN ratio for a client + apex pair (20% default) |
confidence_threshold | 0.7 | Minimum combined score to flag a domain (0.0–1.0) |
stale_entry_ttl_secs | 300 | Seconds before idle tracking entries are evicted from memory |
domain_whitelist | [] | Domains that bypass tunneling detection entirely |
client_whitelist | [] | Client CIDRs (e.g. 10.0.0.0/8) that bypass detection |
Actions¶
| Action | Behavior |
|---|---|
block | Query is refused (REFUSED response code) and logged with block_source = dns_tunneling |
alert | Query is resolved normally but the detection event is logged for review |
throttle | Planned for a future release — will add a delay to suspicious responses |
Start with alert mode
If you are unsure about your network's traffic patterns, set action = "alert" for a few days. Check the query log for false positives by filtering for "Malware Detection" in the queries page, then switch to action = "block" once validated.
Whitelisting¶
Some legitimate services generate DNS patterns that resemble tunneling — CDNs with long random subdomains, monitoring tools with high query rates, or IoT devices with encoded labels.
Domain whitelist — exempt specific domains from all detection:
[dns.tunneling_detection]
domain_whitelist = [
"cdn.jsdelivr.net",
"tiles.services.mozilla.com",
"push.services.mozilla.com",
]
Client whitelist — exempt specific client subnets (e.g. your monitoring server):
[dns.tunneling_detection]
client_whitelist = [
"10.0.0.50/32", # monitoring server
"192.168.10.0/24", # lab network
]
Performance Impact¶
| Operation | Latency | Where |
|---|---|---|
| Phase 1 checks (length, NULL) | ~20-50ns | Hot path |
| Flagged domain lookup (DashMap) | ~100-200ns | Hot path |
| Event emission (mpsc try_send) | ~10-20ns | Hot path |
| Shannon entropy calculation | ~500ns-1us | Background task |
| UDP fast path | 0ns | Completely untouched |
| Total worst-case (hot path) | ~270ns | Within P99 < 35us budget |
Zero impact on cache hits
The UDP fast path (try_cache_direct) is completely untouched by tunneling detection. Only queries that go through the full execute() pipeline are checked — and even those add less than 300ns.
DGA Detection¶
What Is a Domain Generation Algorithm?¶
Domain Generation Algorithm (DGA) malware generates hundreds or thousands of pseudo-random domain names per day. The malware and the attacker's C2 infrastructure share the same seed and algorithm, so both sides know which domains will be "active" on any given day. The attacker registers only a handful of these domains, while infected machines spend most of their time hitting NXDOMAIN for the rest.
Day 1 — infected host tries 500 domains:
xkjf8sdf.com → NXDOMAIN
qwer7hjk.net → NXDOMAIN
mn3kl9sd.org → NXDOMAIN
p8fj2ksl.com → A 185.220.101.42 ← C2 server (attacker registered this one)
Day 2 — new seed, 500 new domains:
tz9plmcx.net → NXDOMAIN
...
This makes DGA malware resilient: taking down one domain forces the attacker to register a new one, but infected machines find the new domain automatically. Traditional blocklists fail here because the domain name changes daily and there are too many variants to enumerate.
DGA detection in Ferrous DNS analyzes the second-level domain (SLD) — the core label like xkjf8sdf in xkjf8sdf.com — rather than subdomains. This is the key difference from tunneling detection, which looks at long subdomain labels.
Two-Phase Detection Architecture¶
Ferrous DNS uses the same two-phase architecture as tunneling detection, but with signals tuned for SLD analysis:
DNS Query arrives (A/AAAA for apex domain)
│
▼
┌─────────────────────────────────────────┐
│ Phase 1 — Hot Path (O(1)) │
│ │
│ Weighted mini-scoring: │ ~30-60ns
│ ✓ SLD entropy (weight 0.30) │
│ ✓ Consonant ratio (weight 0.25) │
│ ✓ SLD length (weight 0.25) │
│ ✓ Digit ratio (weight 0.20) │
│ │
│ Combined score ≥ hot_path_confidence? │
│ → Block / Alert immediately │
│ │
│ ✓ SLD flagged by Phase 2? │ ~100-200ns (DashMap lookup)
│ → Block / Alert immediately │
└────────────┬────────────────────────────┘
│ pass
▼
Normal resolution
│
▼
┌─────────────────────────────────────────┐
│ Phase 2 — Background Analysis │
│ │
│ ◆ N-gram bigram scoring (character │
│ pair frequency vs. English corpus) │
│ ◆ Per-client DGA rate tracking │
│ (NXDOMAIN ratio per client subnet) │
│ │
│ Confidence score > 0.65? │
│ → Flag SLD in hot-path DashMap │
└─────────────────────────────────────────┘
Phase 1 runs inline on every A/AAAA query with no heap allocations. It evaluates four O(1) signals on the SLD and accumulates their weights into a mini-score. A single suspicious signal alone is not enough to trigger — the combined mini-score must exceed hot_path_confidence_threshold (default 0.40), which typically requires two or more signals to fire simultaneously. This prevents false positives on legitimate domains that appear suspicious in only one dimension (e.g., CDN domains with high entropy but normal consonant ratios). Phase 1 also performs a DashMap lookup to check whether Phase 2 has previously flagged this SLD.
Phase 2 runs in a background async task, consuming events from the hot path via a bounded channel. It evaluates n-gram bigram scoring against a compiled English character-pair frequency table and tracks per-client NXDOMAIN rates over a sliding window. When the combined confidence score exceeds the threshold, the SLD is inserted into the hot-path DashMap so Phase 1 can block it immediately on the next query.
Confidence Scoring¶
Detection uses two independent scoring stages, each with its own threshold:
Phase 1 — Hot-path mini-scoring (O(1), inline on every query):
Four signals are evaluated and their weights accumulated. If the combined mini-score exceeds hot_path_confidence_threshold (default 0.40), the domain is blocked immediately. With default weights, this typically requires two or more signals to fire simultaneously — a single suspicious dimension alone is not enough.
| Signal | Weight | Threshold | What It Catches |
|---|---|---|---|
| SLD Shannon entropy | 0.30 | > 3.5 bits/char | Character randomness (DGA names look like random strings) |
| Consonant ratio | 0.25 | > 0.75 | Too many consonants in sequence (human-readable names mix consonants and vowels) |
| SLD length | 0.25 | > 24 chars | Unusually long second-level domains |
| Digit ratio | 0.20 | > 0.30 | Excessive digits — DGA algorithms often embed numbers |
Phase 2 — Background weighted scoring (async task):
Six signals combine into a full confidence score. If the score exceeds confidence_threshold (default 0.65), the SLD is flagged in the hot-path DashMap for immediate blocking on subsequent queries.
| Signal | Weight | Threshold | What It Catches |
|---|---|---|---|
| SLD Shannon entropy | 0.25 | > 3.5 bits/char | Character randomness |
| N-gram bigram score | 0.25 | > 0.6 | Low bigram frequency — real words have predictable character pairs |
| Consonant ratio | 0.15 | > 0.75 | Too many consonants |
| Digit ratio | 0.15 | > 0.30 | Excessive digits |
| SLD length | 0.10 | > 24 chars | Unusually long SLDs |
| Per-client DGA rate | 0.10 | > 10/min | High DGA domain count from a single client subnet indicates active scanning |
Why two thresholds?
The hot_path_confidence_threshold (Phase 1) is intentionally lower than confidence_threshold (Phase 2) because Phase 1 has fewer signals. Phase 1 catches obvious multi-signal DGA domains immediately; Phase 2 catches subtler cases using n-gram analysis and behavioral signals that require more computation.
Explainable detections
Every DGA detection includes the triggering signals, measured values, and configured thresholds. You can see exactly why xkjf8sdf.com was flagged — for example: entropy 3.9 (threshold 3.5), consonant ratio 0.82 (threshold 0.70), bigram score 0.11 (threshold 0.25).
Known DGA Malware Families¶
Ferrous DNS DGA detection has been validated against the SLD patterns of the following malware families:
| Family | Algorithm Type | Typical SLD Length | Key Signals |
|---|---|---|---|
| Conficker | Arithmetic DGA with date seed | 8–11 chars | High entropy, low bigram score, many consonants |
| Mirai | Word-concatenation variants, some random | 6–14 chars | Digit ratio, entropy |
| Emotet | Dictionary + arithmetic hybrid | 10–18 chars | Low bigram score, high consonant ratio |
| CryptoLocker | Pseudo-random with hardcoded seed | 12–20 chars | High entropy, extreme consonant ratio |
| GameOver Zeus (GOZ) | RC4-based DGA | 8–14 chars | Entropy + per-client NXDOMAIN rate |
No external feed or threat intelligence subscription is required. Detection is entirely self-contained based on statistical properties of the SLD.
Configuration¶
DGA detection is enabled by default with conservative thresholds that produce very few false positives on normal traffic. Because the analysis is self-contained (no external feeds), it activates immediately on startup.
[dns.dga_detection]
enabled = true # Master switch
action = "block" # "alert" | "block"
# Phase 1 — hot path weighted mini-scoring (O(1), ~30-60ns)
hot_path_confidence_threshold = 0.40 # Mini-score threshold (typically 2+ signals)
sld_entropy_threshold = 3.5 # Shannon entropy of the SLD (bits/char)
sld_max_length = 24 # Maximum SLD length before triggering
consonant_ratio_threshold = 0.75 # Fraction of consonants in the SLD
digit_ratio_threshold = 0.30 # Fraction of digits in the SLD
# Phase 2 — background analysis
ngram_score_threshold = 0.6 # Bigram deviation score above this triggers detection
dga_rate_per_client = 10 # DGA domains per minute per client subnet
confidence_threshold = 0.65 # Minimum combined score to flag an SLD (0.0–1.0)
stale_entry_ttl_secs = 300 # Eviction of idle tracking entries
# Exemptions
domain_whitelist = [] # Domains exempt from DGA detection
client_whitelist = [] # Client CIDRs exempt from DGA detection
| Option | Default | Description |
|---|---|---|
enabled | true | Master switch for DGA detection |
action | block | Action when a DGA domain is detected: block (REFUSED) or alert (log only) |
hot_path_confidence_threshold | 0.40 | Minimum weighted mini-score for Phase 1 to trigger (0.0–1.0). With default weights, 0.40 typically requires two or more signals to fire simultaneously, preventing false positives on legitimate domains with a single suspicious characteristic |
sld_entropy_threshold | 3.5 | Shannon entropy of the SLD in bits/char. Values above this indicate random-looking names |
sld_max_length | 24 | Maximum SLD length — longer names trigger the length signal |
consonant_ratio_threshold | 0.75 | Fraction of consonant characters in the SLD. DGA names often lack vowels |
digit_ratio_threshold | 0.30 | Fraction of digit characters in the SLD. DGA algorithms frequently embed numbers |
ngram_score_threshold | 0.6 | Bigram deviation score above this indicates non-human-readable character sequences |
dga_rate_per_client | 10 | Maximum DGA-like domains per minute per client subnet before triggering the rate signal |
confidence_threshold | 0.65 | Minimum combined weighted score for Phase 2 to flag an SLD (0.0–1.0) |
stale_entry_ttl_secs | 300 | Seconds before idle tracking entries are evicted from memory |
domain_whitelist | [] | Domains that bypass DGA detection entirely |
client_whitelist | [] | Client CIDRs (e.g. 10.0.0.0/8) that bypass DGA detection |
Actions¶
| Action | Behavior |
|---|---|
block | Query is refused (REFUSED response code) and logged with block_source = dga_detection |
alert | Query is resolved normally but the detection event is logged for review |
Start with alert mode
If you have many IoT devices or embedded systems on your network, their domain names may score higher than typical. Set action = "alert" for a few days, review the query log filtering by block_source = dga_detection, then tune the thresholds or add entries to domain_whitelist before switching to action = "block".
Relationship to DNS Tunneling Detection¶
DGA detection and tunneling detection are independent engines analyzing different signals:
| Aspect | DNS Tunneling Detection | DGA Detection |
|---|---|---|
| Analyzed label | Subdomain labels (long, encoded data) | SLD (second-level domain) |
| Primary signal | Subdomain entropy + query rate + NXDOMAIN ratio | SLD entropy + consonant/digit ratios + bigram score |
| Malware pattern | C2 beaconing, data exfiltration via DNS | Random domain generation for C2 rendezvous |
| Hot-path threshold | Single structural check (length, NULL record) | Mini-scoring: 0.40 (typically 2+ signals required) |
| Background threshold | 0.7 | 0.65 |
| Enabled by default | Yes | Yes |
| External feeds required | No | No |
Both engines run simultaneously. A Mirai-like host that both generates DGA domains and uses DNS tunneling for exfiltration will be caught by both.
DNS Rebinding Protection¶
What Is DNS Rebinding?¶
DNS rebinding attacks exploit the way browsers enforce the same-origin policy. An attacker registers a domain (e.g. evil.com) and configures its DNS to first return the attacker's public IP, then switch to returning a private IP (e.g. 192.168.1.1). The browser, believing it is still talking to evil.com, makes requests to your router, NAS, or other internal devices.
Step 1 — Browser visits evil.com:
evil.com → A 203.0.113.50 (attacker's server)
Browser loads malicious JavaScript
Step 2 — DNS rebinding (short TTL):
evil.com → A 192.168.1.1 (your router!)
JavaScript makes requests to "evil.com" = your router
Browser allows it (same origin)
Result: attacker's script can access your router's admin panel,
change settings, exfiltrate data — all from your browser.
Real-World Targets¶
DNS rebinding has been demonstrated against:
- Home routers — change DNS settings, enable remote management, extract Wi-Fi passwords
- IoT devices — smart cameras, thermostats, smart plugs with unauthenticated HTTP APIs
- NAS devices (Synology, QNAP) — access files, create admin accounts
- Development servers — localhost services (Docker, databases, admin panels) exposed via
127.0.0.1 - Cloud metadata — AWS
169.254.169.254metadata service for credential theft
How Protection Works¶
Ferrous DNS inspects every DNS response before delivering it to clients. If a public domain resolves to a private IP address, the response is blocked.
Protected ranges:
| Range | Description |
|---|---|
10.0.0.0/8 | Private network (Class A) |
172.16.0.0/12 | Private network (Class B) |
192.168.0.0/16 | Private network (Class C) |
169.254.0.0/16 | Link-local |
127.0.0.0/8 | Loopback |
No configuration required
DNS rebinding protection is built-in and always active when blocking is enabled. There is no TOML option to configure — it engages automatically.
NXDomain Hijack Detection¶
What Is NXDomain Hijacking?¶
When you type a non-existent domain (e.g. thisdomaindoesnotexist.com), your DNS server should return NXDOMAIN — "this domain does not exist" (RFC 1035). Many ISPs intercept this response and replace it with the IP address of their own advertising/search page, a practice known as NXDomain hijacking or DNS redirect.
Expected behavior (RFC 1035):
thisdomaindoesnotexist.com → NXDOMAIN (no such domain)
ISP hijacking:
thisdomaindoesnotexist.com → A 198.105.244.23 (ISP ad server)
Browser shows ISP search page with ads
This violates the DNS standard and has cascading effects on DNS-based security mechanisms:
- Rate limiting corruption — NXDOMAIN responses have a separate, stricter rate budget. When the ISP converts them to A records, the NXDOMAIN budget is never consumed, rendering it ineffective against DGA malware floods
- Tunneling detection corruption — the NXDOMAIN ratio signal (10% weight in confidence scoring) becomes useless because DGA-style probes no longer produce NXDOMAIN responses
- DGA detection corruption — future Domain Generation Algorithm detection relies on high NXDOMAIN ratios as a primary indicator; hijacking makes every DGA probe look "successful"
- Application breakage — software that checks for domain existence via NXDOMAIN (email validation, certificate issuance, service discovery) receives incorrect positive results
- Privacy violation — every mistyped URL is redirected to the ISP's tracking infrastructure
Real-World Examples¶
Comcast / Xfinity "Domain Helper"¶
Comcast's "Domain Helper" service intercepts NXDOMAIN responses and redirects users to a Comcast-branded search page filled with sponsored results:
$ dig randomnonexistent12345.com @comcast-dns
;; ANSWER SECTION:
randomnonexistent12345.com. 300 IN A 198.105.244.23
randomnonexistent12345.com. 300 IN A 198.105.245.23
The IPs 198.105.244.x and 198.105.245.x belong to Comcast's ad server infrastructure. Every non-existent domain query returns these IPs instead of NXDOMAIN.
Virgin Media "Web Safe" (UK)¶
Virgin Media's "Web Safe" feature intercepts NXDOMAIN and redirects to their branded search/blocking page:
$ dig nonexistentdomain98765.com @virgin-media-dns
;; ANSWER SECTION:
nonexistentdomain98765.com. 60 IN A 81.200.64.50
Rogers / Bell / Telus (Canada)¶
Canadian ISPs commonly redirect NXDOMAIN to advertising pages. Rogers uses their "Rogers Assure" service, while Bell and Telus have similar programs.
Verizon / AT&T / CenturyLink (US)¶
Multiple US ISPs have deployed NXDOMAIN interception at various times, often with opt-out mechanisms buried in account settings that reset periodically.
Hotel / Airport / Public Wi-Fi¶
Captive portals often intercept NXDOMAIN responses to redirect users to a login page. While the intent is different (authentication vs. advertising), the DNS-level effect is identical — NXDOMAIN responses are replaced with A records pointing to the portal server.
Corporate DNS Resolvers¶
Some corporate networks use DNS interception for security monitoring, brand protection, or content filtering. Internal resolvers may redirect non-existent domains to a corporate intranet page.
How Detection Works¶
Ferrous DNS uses a background probe mechanism that tests each upstream DNS server with domains that must return NXDOMAIN per RFC 6761. If an upstream returns A/AAAA records instead, the returned IPs are recorded as "hijack IPs" and all future responses containing those IPs are converted back to proper NXDOMAIN.
Background Probe Loop (every 5 min)
│
▼
┌─────────────────────────────────────────────┐
│ For each upstream DNS server: │
│ │
│ Send 3 queries for random .invalid domains │
│ e.g. a1b2c3d4e5f67890.probe.invalid │
│ │
│ RFC 6761: .invalid MUST return NXDOMAIN │
│ │
│ ┌─ Response is NXDOMAIN? → Upstream OK │
│ │ │
│ └─ Response has A/AAAA? → HIJACKING! │
│ Record IPs in DashSet (O(1) lookups) │
│ Log warning with upstream + IP details │
└─────────────────────────────────────────────┘
Hot Path (every DNS query)
│
▼
┌─────────────────────────────────────────────┐
│ DNS response arrives from upstream │
│ │
│ Check each IP in response against DashSet │
│ (1-4 IPs, O(1) per lookup, ~10-40ns each) │
│ │
│ ┌─ No match? → Deliver response normally │
│ │ │
│ └─ Match! → NXDomain hijack detected │
│ Block: return NXDOMAIN to client │
│ Alert: log and deliver original response│
└─────────────────────────────────────────────┘
Key design decisions:
.invalidTLD (RFC 6761) — this TLD is guaranteed to never exist by IANA. Any DNS server returning A records for.invaliddomains is definitively hijacking.- Multiple probes per round — 3 random domains per cycle to confirm hijacking (not a transient error). Random hex ensures no caching effects.
- DashSet for O(1) hot-path lookups — the hijack IP set is lock-free and typically contains 0-5 IPs, adding negligible overhead to the DNS hot path.
- TTL-based eviction — hijack IPs expire after
hijack_ip_ttl_secs(default 1 hour) and must be re-confirmed by the next probe cycle. This handles ISPs that rotate their redirect IPs. - Per-upstream tracking — each upstream is probed independently. If only one of your upstreams is hijacking, only responses from that upstream's IPs are affected.
Configuration¶
NXDomain hijack detection is enabled by default. If your upstreams are not hijacking (e.g. Cloudflare 1.1.1.1, Google 8.8.8.8, Quad9 9.9.9.9), the probe loop runs silently with zero impact — no IPs are recorded, no responses are modified.
[dns.nxdomain_hijack]
enabled = true # Master switch
action = "block" # "alert" | "block"
probe_interval_secs = 300 # Probe every 5 minutes
probe_timeout_ms = 5000 # Timeout per probe query
probes_per_round = 3 # Random .invalid queries per upstream per cycle
hijack_ip_ttl_secs = 3600 # Evict unconfirmed hijack IPs after 1 hour
| Option | Default | Description |
|---|---|---|
enabled | true | Master switch for NXDomain hijack detection |
action | block | Action when hijack is detected: block (convert to NXDOMAIN) or alert (log only) |
probe_interval_secs | 300 | Seconds between probe cycles (5 min default) |
probe_timeout_ms | 5000 | Timeout in milliseconds for each probe query |
probes_per_round | 3 | Number of random .invalid domain queries per upstream per cycle |
hijack_ip_ttl_secs | 3600 | Seconds before an unconfirmed hijack IP is evicted from the set |
Actions¶
| Action | Behavior |
|---|---|
block | Response is converted to NXDOMAIN and logged with block_source = nxdomain_hijack. The client receives the correct NXDOMAIN response as if the ISP were not hijacking. |
alert | Response is delivered as-is (with the hijack IP) but the detection event is logged for review. Useful for monitoring before enforcing. |
No false positives on clean upstreams
If your upstream DNS servers are not hijacking (Cloudflare, Google, Quad9, NextDNS, etc.), this feature has zero false positives — the .invalid TLD is defined by IANA as "always NXDOMAIN". Only a genuinely hijacking upstream will trigger detection.
Start with alert mode
If you are unsure whether your ISP hijacks NXDOMAIN, set action = "alert" and check the query log after one probe cycle (5 minutes). If no entries appear, your upstreams are clean and detection runs silently.
Edge Cases¶
| Scenario | Behavior |
|---|---|
| Multiple upstreams, only some hijacking | Each upstream is probed independently; only IPs from offending upstreams are recorded |
| ISP rotates hijack IPs | IPs are re-confirmed every probe cycle; stale IPs are evicted after hijack_ip_ttl_secs |
| All upstreams hijack | All hijack IPs are recorded; legitimate domains resolve to different IPs and are unaffected |
Corporate DNS intercepts .invalid | Rare but possible; set enabled = false for these environments |
| Hotel/airport captive portal | Hijack IPs are detected and filtered; set action = "alert" if you want to see the portal page |
Performance Impact¶
| Operation | Latency | Where |
|---|---|---|
| Hijack IP check (DashSet lookup) | ~10-40ns per IP | Hot path |
| Typical response (1-4 IPs) | ~40-160ns total | Hot path |
| Probe query (background) | ~5-50ms per upstream | Background task |
| Normal traffic (no hijack IPs) | ~10ns | Empty DashSet check |
Negligible overhead
When no hijack IPs are recorded (clean upstreams), the hot-path check is a single DashSet contains() call on an empty set — effectively free. The background probe adds 3 DNS queries per upstream every 5 minutes.
Response IP Filtering¶
What Is Response IP Filtering?¶
Even with comprehensive domain blocklists, malware can evade detection by rapidly rotating domains (domain flux). However, the infrastructure IPs behind C2 servers change far less frequently. Response IP filtering checks the IP addresses in DNS responses against known-bad IP threat feeds, blocking connections at the DNS layer before the client ever contacts the C2 server.
Normal DNS query:
safe-website.com → A 93.184.216.34 ✓ Allowed
Malware C2 domain (not yet in blocklists):
cdn-update-7x9k.com → A 185.220.101.42 ✗ Blocked (known C2 IP)
Why It Matters¶
Domain blocklists are reactive — they can only block domains that have already been identified. Attackers generate thousands of new domains daily using Domain Generation Algorithms (DGA), domain fronting, and fast-flux DNS. By the time a domain appears on a blocklist, the malware has already established C2 communication.
C2 infrastructure, however, is expensive to change. A bulletproof hosting provider, a compromised server, or a rented VPS has a fixed IP that persists across many domain rotations. Threat intelligence feeds like abuse.ch, Feodo Tracker, and SSL Blacklist track these IPs.
Response IP filtering closes the gap between domain-based blocking and IP-based threat intelligence.
Real-World Examples¶
Feodo Tracker (Emotet, Dridex, TrickBot)¶
The Feodo Tracker maintains a blocklist of IPs associated with the Emotet, Dridex, and TrickBot banking trojans. These malware families use DNS to locate their C2 servers:
newly-registered-domain-xyz.com → A 185.220.101.42 (Emotet C2)
another-random-domain.net → A 194.135.33.88 (Dridex C2)
Both domains are new and not yet on any blocklist, but the IPs have been tracked for weeks by Feodo Tracker. Response IP filtering blocks the resolution immediately.
SSL Blacklist (Cobalt Strike, Sliver, Metasploit)¶
The SSL Blacklist tracks IPs of servers with SSL certificates associated with C2 frameworks:
The domain is clean, but the IP matches a known Cobalt Strike team server.
How It Works¶
Background Fetch Loop (every 24h)
│
▼
┌─────────────────────────────────────────────┐
│ For each configured feed URL: │
│ │
│ HTTP GET → parse IPs (one per line) │
│ Insert into DashSet (O(1) lookups) │
│ Update TTL timestamp for eviction │
│ │
│ Typical feeds: 5K-50K IPs │
│ Memory: ~200KB-2MB │
└─────────────────────────────────────────────┘
Hot Path (every DNS query)
│
▼
┌─────────────────────────────────────────────┐
│ DNS response arrives from upstream │
│ │
│ Check each IP in response against DashSet │
│ (1-4 IPs, O(1) per lookup, ~10-40ns each) │
│ │
│ ┌─ No match? → Deliver response normally │
│ │ │
│ └─ Match! → C2 IP detected │
│ Block: return REFUSED to client │
│ Alert: log and deliver original response│
└─────────────────────────────────────────────┘
Key design decisions:
- Fetch immediately on startup — protection is active from the first DNS query, not after the first refresh interval.
- DashSet for O(1) hot-path lookups — the blocked IP set is lock-free and uses FxHash for minimal overhead. Typical feeds contain 5K-50K IPs using 200KB-2MB of memory.
- TTL-based eviction — IPs not re-confirmed by a feed refresh are evicted after
ip_ttl_secs(default 7 days). This handles feeds that drop recovered IPs. - Graceful feed failure — if a feed URL fails to download, existing IPs are retained. Only a successful fetch updates the confirmation timestamp.
- Standard IP list format — one IP per line,
#comments, blank lines ignored. Compatible with abuse.ch, Feodo Tracker, and most threat intelligence feeds.
Configuration¶
Response IP filtering is disabled by default because it requires configuring external feed URLs.
[dns.response_ip_filter]
enabled = false # Master switch (opt-in)
action = "block" # "alert" | "block"
ip_list_urls = [] # C2 IP feed URLs
refresh_interval_secs = 86400 # Re-download feeds every 24 hours
ip_ttl_secs = 604800 # Evict IPs not re-confirmed within 7 days
Example with real feeds:
[dns.response_ip_filter]
enabled = true
action = "block"
ip_list_urls = [
"https://feodotracker.abuse.ch/downloads/ipblocklist.txt",
"https://sslbl.abuse.ch/blacklist/sslipblacklist.txt",
]
refresh_interval_secs = 86400
ip_ttl_secs = 604800
| Option | Default | Description |
|---|---|---|
enabled | false | Master switch — requires user to configure feed URLs |
action | block | Action when a C2 IP is detected: block (REFUSED) or alert (log only) |
ip_list_urls | [] | URLs of IP threat feeds (one IP per line, # comments) |
refresh_interval_secs | 86400 | Seconds between feed re-downloads (24 hours) |
ip_ttl_secs | 604800 | Seconds before an IP not re-confirmed by a feed is evicted (7 days) |
Actions¶
| Action | Behavior |
|---|---|
block | Response is refused (REFUSED response code) and logged with block_source = response_ip_filter |
alert | Response is delivered as-is but the detection event is logged for review |
Start with alert mode
If you are unsure about false positives, set action = "alert" for a few days. Check the query log for detections by filtering for "Malware Detection" in the queries page, then switch to action = "block" once validated.
ip_ttl_secs should be greater than refresh_interval_secs
If ip_ttl_secs is shorter than refresh_interval_secs, IPs will be evicted before the next feed refresh can re-confirm them. Ferrous DNS logs a warning at startup if this misconfiguration is detected.
Recommended Feeds¶
| Feed | URL | Description |
|---|---|---|
| Feodo Tracker IP Blocklist | https://feodotracker.abuse.ch/downloads/ipblocklist.txt | Emotet, Dridex, TrickBot, QakBot C2 IPs |
| SSL Blacklist | https://sslbl.abuse.ch/blacklist/sslipblacklist.txt | Cobalt Strike, Sliver, Metasploit C2 IPs |
Edge Cases¶
| Scenario | Behavior |
|---|---|
Empty ip_list_urls | Feature silently disabled even if enabled = true |
| All feed URLs fail to download | Existing IPs retained; warning logged |
| Feed returns invalid content | Lines that don't parse as IP addresses are silently skipped |
| Same IP in multiple feeds | DashSet deduplicates automatically |
| Legitimate domain resolves to a C2 IP | Blocked — use alert mode or the global allowlist for false positives |
| IPv6 C2 IPs | Fully supported — IpAddr handles both IPv4 and IPv6 |
Performance Impact¶
| Operation | Latency | Where |
|---|---|---|
| IP check (DashSet lookup) | ~10-40ns per IP | Hot path |
| Typical response (1-4 IPs) | ~40-160ns total | Hot path |
| Feed download (background) | ~1-5s per URL | Background task (every 24h) |
| Memory (typical feeds) | ~200KB-2MB | ~40 bytes per IP x 5K-50K IPs |
Negligible overhead
The hot-path check is a DashSet contains() call per IP in the response — the same O(1) pattern used by NXDomain hijack detection. Background feed downloads run once every 24 hours with no impact on DNS resolution.
Viewing Detections¶
Query Log¶
All blocked queries appear in the query log with their detection source. Navigate to Queries and use the Category dropdown to filter by Malware Detection — this shows DNS tunneling, DGA detection, DNS rebinding, NXDomain hijack, and response IP filter blocks in one view.
Each entry shows:
- The blocked domain
- The client that sent the query
- The detection source (
DNS Tunneling,DGA Detection,DNS Rebinding,NXDomain Hijack, orResponse IP Filter) - Timestamp and response status
Tunneling, DGA, rebinding, hijack, and response IP filter blocks are highlighted with an orange background for quick identification.
Dashboard¶
The dashboard automatically includes all malware detection blocks in the statistics. The source_stats breakdown shows dns_tunneling, dga_detection, dns_rebinding, nxdomain_hijack, and response_ip_filter alongside other block sources (blocklist, managed domain, rate limit).
Comparison with Other DNS Servers¶
| Feature | Ferrous DNS | Pi-hole | AdGuard Home | Blocky | NextDNS |
|---|---|---|---|---|---|
| DNS tunneling detection | |||||
| DGA detection | |||||
| DNS rebinding protection | |||||
| NXDomain hijack detection | N/A (cloud) | ||||
| Response IP filtering (C2) | |||||
| Explainable alerts | — | — | — | ||
| Configurable thresholds | — | — | — | ||
| Self-hosted / no cloud | |||||
| Zero hot-path overhead | — | — | — | N/A |
NXDomain hijack — unique to Ferrous DNS
Pi-hole has had an open feature request for ISP hijack detection since 2018 with no implementation. No other self-hosted DNS server detects and corrects NXDomain hijacking automatically. Ferrous DNS is the first to implement this as a built-in, zero-configuration feature.
NextDNS comparison
NextDNS offers "AI-Driven Threat Intelligence" as a cloud service but provides no visibility into why a domain was flagged — it is a black box. Ferrous DNS runs entirely on your hardware with full transparency: every detection shows the exact signal, measured value, and threshold.