Back to guides
Runbook · DDoS Mitigation

Your Resolver Is Someone Else's Weapon.

On 12 July 2026 our public resolver was doing something strange: 93% of every query it answered was for one name. It was not a bug and it was not our users. Somebody was using us to attack strangers, turning 71-byte queries into 2,257-byte answers and mailing 4.6 Mbps of them at people who never asked. This is what we found, why the obvious fix would have been a catastrophe, and what you can do today if you run AdGuard Home or Pi-hole with port 53 open to the internet.

By Ozy-666, creator and operator of dnsdoh.art · Published · 18 min read
The same spoofed query, before and after THE RESOLVER AS A WEAPON Attacker forges the source IP 71 B Your resolver answers, as designed cloudflare.com TXT 2,257 B answer The victim never sent anything 31.8× amplification ~250 queries/second sustained for hours ~4.6 Mbps of attack traffic, sent by us THE RESOLVER REFUSING Attacker exactly the same query 71 B Your resolver bucket is empty TC=1, no answer 32 B response The victim gets 32 useless bytes 0.45× de-amplification a real client just retries over TCP and resolves a spoofed source cannot: the reflection dies The attacker still sends the same packets. The only thing that changed is what the resolver is willing to say back. You cannot stop the queries arriving. You can stop being worth pointing at.

The whole incident in one picture: a 31.8× amplifier becomes a 0.45× de-amplifier, and no legitimate user notices.

A DNS amplification attack does not attack your resolver. It recruits it. That distinction changes everything about how you are supposed to respond.

The anomaly: one name eating the whole resolver

It started as a glance at the query log. A public resolver serves a long tail of names: some Google, some CDN, some ads getting blocked, thousands of different domains. Ours, that morning, looked like this:

12 July 2026, 12 hours of live query log
$ jq -r .QH /dev/shm/querylog.json | sort | uniq -c | sort -rn | head -3
3455950 cloudflare.com          ← 93.38% of all DNS traffic
   9124 www.google.com
   7733 googlevideo.com

Three and a half million queries for cloudflare.com in twelve hours, all of them TXT. Not A, not AAAA, not HTTPS: TXT, the record type that holds text and therefore holds a lot of bytes. No human browses to a TXT record. Nothing on a normal network asks for one three and a half million times.

This is the tell. When one qname and one qtype dominate a resolver's traffic, you are almost never looking at a client bug. You are looking at somebody standing behind your resolver and using it to hit something.

The mathematics of a weapon

Here is why cloudflare.com/TXT and not something else. Run it yourself, right now, against any resolver:

$ dig TXT cloudflare.com +bufsize=10000
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 26
;; MSG SIZE  rcvd: 2147

$ dig TXT samsung.com +bufsize=10000
;; flags: qr rd ra; QUERY: 1, ANSWER: 52
;; MSG SIZE  rcvd: 4131   ← 58× the query size

Big companies accumulate TXT records the way garages accumulate boxes: domain-verification tokens for every SaaS product they ever signed up for, SPF policies, DKIM keys, site-ownership proofs. Nobody ever deletes them. cloudflare.com carries 26 of them; samsung.com carries 52. To an attacker these are not corporate records, they are free ammunition: a huge, publicly resolvable, cacheable answer that any resolver on earth will hand out to anyone who asks. This is why the same handful of fat-TXT domains show up in reflection floods again and again. None of these companies did anything wrong, and their infrastructure is not the target - their record size is simply the payload.

Here is a sample of the ammunition, measured against the same 71-byte query. Run any of these yourself with dig TXT <domain> +bufsize=10000:

Domain TXT records Answer size Amplification
cisco.com886,816 B96.0×
microsoft.com614,890 B68.9×
samsung.com524,131 B58.2×
hp.com524,001 B56.4×
ebay.com362,645 B37.3×
cloudflare.com262,147 B30.2×
aol.com161,228 B17.3×
paypal.com121,131 B15.9×
google.com14981 B13.8×
yahoo.com11796 B11.2×

The attacker who hit us picked cloudflare.com, at 30×. They left the bigger guns on the table: cisco.com is a 96× amplifier, more than three times as loud. These figures were measured on 13 July 2026 against a public resolver; they tend to grow over the years as verification tokens pile up and nobody prunes them.

Now do the arithmetic that the attacker did. On our wire, during the flood:

  • • The query: 71 bytes on the wire.
  • • The answer: 2,257 bytes, too big for one UDP datagram, so it left as two IP fragments.
  • • The ratio: 31.8×.

One reconciliation, because a careful reader will notice it: the table above lists cloudflare.com at 2,147 bytes, but our wire shows 2,257. The 110-byte difference is the DNSSEC signatures. A plain TXT query returns the 2,147-byte answer; set the DNSSEC-OK bit and the resolver also attaches the RRSIG records that authenticate it, and the answer grows to 2,257. The attack set that bit - buying an extra 110 bytes of amplification for free, and confirming this was a tuned tool rather than a stray misconfiguration.

The attacker forges the source address of each query to be the address of whoever they want to hurt. Our resolver, doing exactly what a resolver is built to do, sends 2,257 bytes to that address. The attacker spends 71 bytes; the victim receives 2,257. At the ~250 queries per second we were sustaining, that is roughly 4.6 Mbps of attack traffic leaving our server, aimed at people we had never heard of.

The uncomfortable sentence. We were not the target of this attack. We were the weapon. Every packet of it was ours, sent from our IP, and it would have shown up in the victim's logs with our name on it.

One more detail worth knowing, because it is how you confirm the diagnosis: every one of those queries carried a DNS transaction ID of 0x0000, advertised an EDNS UDP buffer size of 10000, and set the DNSSEC-OK bit. No real resolver does the first two. A real client randomises its transaction ID (that randomness is what makes cache poisoning hard), and modern software advertises 1232 bytes, not 10000. Every field was set to squeeze the largest possible answer out of us: the huge buffer to avoid early truncation, the DNSSEC-OK bit to pull in the signatures. This is a purpose-built tool, not a misconfigured device.

The trap: the obvious fix is the worst thing you can do

The reflex is immediate and it is wrong. You have a list of IP addresses hammering your resolver. Ban them. Firewall the /24s. Done.

We ran tcpdump and looked up who those addresses actually belonged to. Roughly 1,792 distinct IPs, packed into just eight /24 blocks:

Source range Who it actually is
205.164.xSubnet Digital LLC, United States
45.181.x / 177.66.x / 177.91.xBrazilian access ISPs
167.249.xWIKI TELECOMUNICACOES, São Luís, Brazil

These are not the attacker. These are the victims. The source addresses are forged; they are the addresses the flood is being aimed at. Somewhere in Brazil there is an ISP whose customers were drowning in DNS answers they never requested, and we were one of the machines drowning them.

So look at what banning them would have accomplished. The attacker's goal is to make those networks unreachable. If we had blocked those ranges at the firewall, our resolver would have stopped answering anyone at those addresses - including the real users behind them - while the attacker simply spun up a new list of forged victims and carried on. We would have completed the denial of service on the attacker's behalf, for free, and called it a fix.

The rule we now operate by: under a reflection attack, never punish the source address. The source is a lie, and behind the lie is somebody being hurt. You cannot ban your way out of an attack whose return address is a victim. Shape the response instead - that is the only thing in the exchange that is actually yours.

This is also why "just block the attacker's IPs" advice you find on forums is actively dangerous for this attack class. It works fine for a direct flood. Applied to reflection, it turns your resolver into an accomplice twice over.

Why our rate limits never fired

We are not an unprotected box. There is a per-IP query cap and a per-subnet cap in nftables, sitting in front of everything:

/etc/nftables.conf - the limiters that sat empty all day
# per-IP query cap -> auto-ban on excess
udp dport 53 \
    meter dns_qps_ip { ip saddr limit rate over 30/second burst 100 packets } \
    add @dns_bad_ip { ip saddr timeout 30m } counter drop

# per-/24 cap -> ban the whole subnet on a spray
udp dport 53 \
    meter dns_qps_24 { ip saddr & 255.255.255.0 limit rate over 150/second burst 400 packets } \
    add @dns_bad_24 { ip saddr & 255.255.255.0 timeout 30m } counter drop

Both counters were at zero through the entire flood. Not "low" - zero. The attack was calibrated to stay underneath them: each spoofed source sent only 1 to 5 queries, spread across ~1,792 addresses. Thirty per second per IP is a generous ceiling and the attack never came close to it, because it never needed to. The volume came from the number of forged sources, not from the rate of any one of them.

There was even a global ceiling of 300 queries/second. The flood sat at ~250. Coincidence is not the word I would use.

This is the structural lesson, and it applies to every rate limiter on every resolver including the one built into AdGuard Home (ratelimit, per client IP): a limiter keyed on the source address cannot see an attack that has no real source address. Spoofing makes the key meaningless. The attacker gets to choose a fresh key for every packet.

The self-inflicted wound

One rule did fire. Months earlier we had added a blunt instrument: drop small UDP/53 packets when they exceed 150 per second, on the theory that amplification queries are small.

the rule that hurt us - do not copy this one
udp dport 53 udp length 30-70 \
    limit rate over 150/second burst 300 packets \
    counter name "drop_short_dns_flood" drop

# counter: 2,930,000+ packets dropped. Some of them were the attack.
# Some of them were our users.

Look at what else lives in a 30-70 byte UDP/53 packet. ssl.gstatic.com is 41 bytes on the wire. api.epicgames.dev is 43. Ordinary A queries from ordinary people are exactly the same size as the attack's queries, because a DNS query for a short name is a short packet, whoever sends it.

So the rule had a single 150/second budget, and the flood was spending it at 250/second. Legitimate users' queries arrived, found the budget already exhausted by the attack, and were dropped. Real people's DNS was broken - by our own defence, on behalf of the attacker. We call this the shared fate failure: a filter that cannot distinguish attack from user, given a bucket they must share, will starve the user.

The fix was to stop asking a layer-4 rule to do a layer-7 job. A packet filter can see size and rate. It cannot see the query name, and it cannot see how big the answer is going to be - which are the only two facts that actually separate this attack from a person opening a website. So we demoted the rule to what it is honestly good at - a backstop against raw packet-rate exhaustion - and moved the real defence up to the resolver:

the same rule, honest about its job
# Pure pps-exhaustion backstop. Fires only on a genuine packet flood,
# far above any legitimate aggregate. Amplification defence now belongs
# to the RRL layer, which can see the qname and the response size.
udp dport 53 udp length 30-70 \
    limit rate over 3000/second burst 6000 packets \
    counter name "drop_short_dns_flood" drop

# drops since the change: 0

If you have a rule like the first one in your firewall right now - and a lot of hardening guides recommend exactly that shape - it is worth checking its counter. It may be protecting you. It may also be quietly eating your own traffic.

Check your own resolver

Is this happening to you? Three checks.

If you run AdGuard Home or Pi-hole and UDP port 53 is reachable from the internet - whether you meant it to be or not - you can be conscripted exactly the way we were. Here is how to find out in about two minutes.

1. Are you an open resolver at all?

From any machine outside your network (a phone on mobile data, a cheap VPS, a friend's connection), ask your public IP to resolve something. If you get an answer, the entire internet can too.

$ dig @YOUR.PUBLIC.IP TXT cloudflare.com +short

# A list of TXT records = you are an open resolver.
# "connection timed out" or REFUSED = you are not. Good.

2. What does your top-queried domain look like?

This is the check that found our incident, and it needs no tooling at all. Open the dashboard.

  • AdGuard Home: Dashboard → "Top queried domains".
  • Pi-hole: Dashboard → "Top Permitted Domains".

A healthy resolver shows a spread: telemetry endpoints, CDNs, whatever your household actually uses, none of it dominant. One single domain at 60, 80, 93 percent is not a statistic, it is a symptom. Click into the query log and check the record type. If a fat-TXT domain like cloudflare.com, samsung.com or google.com is being asked for TXT thousands of times, from addresses that are not yours, you are being used as an amplifier right now.

3. Watch what you are sending out

The definitive proof is on the wire. This shows the answers leaving your box:

what an amplifier looks like from the inside
$ tcpdump -ni any -v 'udp and src port 53'

IP (length 1500) 194.180.189.33.53 > 45.181.231.105.61694: 27/0/1 cloudflare.com. TXT ...
                  ^^^ your server    ^^^ a stranger you have never served

If large answers are streaming to addresses that are not your clients, and those addresses never sent you anything, you are the weapon. The bytes are yours and so is the abuse complaint.

Stop being the weapon

The 30-second fix: refuse to serve the ammunition

You almost certainly cannot deploy response rate limiting in AdGuard Home or Pi-hole this afternoon, because neither ships it. But you can do something simpler that gets most of the benefit immediately, and it is the thing we would tell a friend to do first: stop answering the one query the attack depends on.

The attack needs a big answer. If the answer is small, the amplification is gone - the queries keep arriving, but there is nothing worth reflecting.

AdGuard Home

Filters → Custom filtering rules. Add one line per abused name, blocking only the TXT type with the $dnstype modifier:

AdGuard Home → Filters → Custom filtering rules
||cloudflare.com^$dnstype=TXT
||samsung.com^$dnstype=TXT
# ...and whatever else your query log shows being abused

This is surgical. $dnstype=TXT blocks the TXT lookup and nothing else: A, AAAA and HTTPS queries for the same domain keep working normally, so anyone on your network who actually visits cloudflare.com is unaffected. We measured what the blocked answer costs on our own resolver - a NODATA reply with a synthetic SOA, 117 bytes against a 71-byte query. That is 1.6× instead of 31.8×: a 95% cut in the amplification, from one line of config.

Pi-hole

Pi-hole's blocklist has no record-type modifier, so you have two options with an honest trade-off between them:

  • Blacklist the domain (Domains → Add to blacklist). Blunt: it blocks all record types for that name, so if a device on your network genuinely needs cloudflare.com, it breaks. Acceptable as an emergency stop, not as a permanent setting.
  • Filter the record type in dnsmasq, which is what Pi-hole runs underneath. Recent dnsmasq (2.90+, shipped with Pi-hole v6) supports filter-rr, which strips a record type from answers without touching the rest. In Pi-hole v6 put it in Settings → All settings → misc.dnsmasq_lines; on v5, drop a file in /etc/dnsmasq.d/:
/etc/dnsmasq.d/99-amp.conf - strips TXT from answers entirely
filter-rr=TXT
# TXT lookups now return an empty answer, for every domain.
# Almost nothing on a home network legitimately queries TXT.
# Do NOT use this on a mail server, which needs SPF/DKIM/DMARC.

This is broader than AdGuard Home's rule - it removes TXT everywhere, not just for the abused domain - but on a home or small-office resolver that is usually a fine trade, and it takes away the whole ammunition class rather than one round of it. Verify with dig @your-pihole TXT cloudflare.com: the answer count should be zero.

Be honest about what this is. Blocking the abused name is whack-a-mole, and we should say so plainly: the attacker is not attached to cloudflare.com. When their queries start returning 117 bytes, they will move to samsung.com, then to some other domain with a fat TXT record, and there are thousands. Type-filtering (filter-rr) is more durable than name-blocking because it removes the whole record class. But neither is a real defence against an attacker who is paying attention. They are a tourniquet: apply immediately, then fix the wound.

The real decision: is your resolver a public service or not?

Nearly every guide ends the same way: close port 53. It is not wrong, but it is really the answer to a question worth asking out loud first - did you mean to run a resolver that strangers can use? An open UDP/53 is not a quiet default that sits there harmlessly. Internet-wide scanners catalogue open resolvers continuously, and a freshly exposed one is typically found and put to work within days. So treat the exposure as a choice with two honest branches, not an accident to be tidied away.

Branch 1: you did not mean to be public (most people)

If your AdGuard Home or Pi-hole exists to serve your household, your office or your own devices, it has no business answering UDP/53 from the whole internet. Close it, and this entire attack class disappears - there is nothing to defend because there is nothing to reach.

  • • Bind the resolver to your LAN interface, or drop inbound UDP/53 at the firewall. One rule, done.
  • • Need to reach it from the road? Put it behind a VPN (WireGuard, Tailscale) rather than opening 53 to the world. The resolver stays private; you join its network.
  • • If a handful of known remote addresses genuinely need it, use AdGuard Home's allowed_clients (IPs, CIDRs, ClientIDs). A resolver that only answers clients it recognises cannot be recruited by a forged source.

Branch 2: you mean to be public

Then closing 53 is not on the table, and "just firewall it" is not advice you can take - which is exactly the position we are in, and the reason this whole article exists. Running a public resolver is a commitment: you will be found, you will be pointed at someone eventually, and the job is to make sure that when it happens, your resolver refuses to do any damage. That is a defence-in-depth problem, and no single switch solves it. Layer these, outermost first:

  • Turn on refuse_any. The classic amplification vector is the ANY query, which asks for every record at once. AdGuard Home has this setting and it should be on. We had it on, which is precisely why the attacker used TXT instead - so understand what it buys you: it closes one door, not the corridor.
  • Clamp the EDNS buffer size you honour. Our attacker advertised a 10000-byte buffer to force the biggest possible UDP answer. DNS Flag Day 2020 settled on 1232 bytes as the sane maximum (1280-byte minimum IPv6 MTU, minus headers); honouring 1232 means large answers truncate early by the protocol itself, capping amplification for free. Standard, RFC 6891-compliant, costs nothing.
  • Steer clients to encrypted transports. DoH, DoT and DoQ cannot be used for reflection at all: they are connection-oriented, so a client must complete a handshake before it receives a single byte, and a forged source address cannot complete a handshake. Every query that arrives over an encrypted transport is structurally incapable of being part of an amplification attack. Plain UDP/53 is the entire attack surface here - the more of your traffic lives on the encrypted transports, the smaller it gets.
  • Keep the tourniquet ready. The $dnstype=TXT filter (AdGuard Home) or filter-rr (Pi-hole) from the section above is your immediate response the moment you spot a flood in progress. It is not a permanent defence, but it stops the bleeding in one line while you deploy the real one.
  • The real fix: response rate limiting with a TC=1 slip. Neither AdGuard Home nor Pi-hole ships it, so this is the one piece you cannot just toggle - you either run resolver software that has it (BIND, Knot, PowerDNS all do) and turn it on, or you build it, as we did. It is the only item on this list that defeats an attacker who adapts, because it keys on the query instead of the source. The next section is how it works.

The honest version of "close port 53". A public UDP/53 resolver that you are not prepared to actively defend will, sooner or later, be used to hurt someone. That is not a reason never to run one - the encrypted DNS most people reading this actually want needs a public resolver behind it. It is a reason to run one on purpose, with the layers above in place, rather than by accident and then wonder why your bandwidth graph exploded.

The real fix: response rate limiting with a TC=1 slip

The proper defence for this attack has existed since 2012, when Paul Vixie and Vernon Schryver described Response Rate Limiting. BIND, Knot and PowerDNS all ship a form of it. Most operators never turn it on, and the DNS software most home users run - AdGuard Home, Pi-hole - does not implement it at all. We built it into our AdGuard Home fork. Here is the whole idea in three gates:

the entire algorithm
# Runs after the answer is resolved, before it is written to the wire.

GATE 1 - transport.  UDP only. DoH / DoT / DoQ / TCP are
                   connection-verified, cannot be spoofed, never touched.

GATE 2 - size.       Is the response bigger than 512 bytes?
                   If not, send it. This alone exempts 99.7% of real
                   traffic: an A record is tiny, and a small answer
                   cannot amplify anything.

GATE 3 - budget.     Charge two token buckets, 5 tokens/s, burst 20:
                     bucket A: (source /24, qname, qtype)   <- fairness
                     bucket B: (qname, qtype)               <- the killer
                   If either is empty -> SLIP.

SLIP =             reply with the header only: TC=1 (truncated),
                   empty answer section, 32 bytes.

Bucket B is the one that kills the attack, and it is the piece that most rate limiters get wrong. The attacker's whole strategy was to vary the thing everyone keys on - the source address. So we key on the thing they cannot vary: the query itself. Every one of those 1,792 forged sources was asking for the same name and the same type, so all 1,792 of them converge on one bucket. The carpet-bomb collapses into a single budget of 5 answers per second. Spreading the attack across more IPs does not help them; it changes nothing at all.

And the slip is what makes it safe. We do not drop the query - dropping is indistinguishable from an outage, and a real client behind that /24 would just sit there timing out. We answer, with the truncation bit set, which is a completely standard DNS instruction meaning "too big for UDP, ask me again over TCP." A real client obeys it, opens a TCP connection, and gets its full answer, and we verified this works. A spoofed client cannot: to complete a TCP handshake you must receive the SYN-ACK, which goes to the address you forged, not to you. The very thing that makes the attack possible - lying about your address - is the thing that makes it impossible to follow the retry.

The elegance is that we never decide who is an attacker. We never see the attacker, never identify them, never ban anybody. We simply make the resolver's most expensive answer scarce, and hand out a 32-byte "ask me over TCP" to everyone who exceeds the budget. Honest clients follow the instruction. Liars cannot. The attack sorts itself out.

The result on the wire, measured with dig: the 2,257-byte answer becomes a 32-byte response. Against a 71-byte query that is 0.45× - our resolver now emits fewer bytes than it receives. As an amplifier, it is worse than useless. It is a net absorber.

Proving it before trusting it

A defence that truncates real users' answers is worse than the attack. So we did not flip it on. We ran the engine in shadow mode first - the full decision logic, running live against the real flood, deciding what it would do and writing that to a ledger, while the resolver kept answering normally.

Then the step that actually earns trust: parity. The shadow engine and the enforcement engine are two separate pieces of software - the policy brain lives in our Go WAF, the enforcement lives in the resolver fork - so we made them share one frozen hash of the query name and checked that, over the same 30 seconds of the same live flood, they reached identical verdicts: 98.0% slip, 5.0 answered per second, both of them, to the decimal. Only then did we flip enforcement on.

Across 67,905 shadow decisions the false-positive ledger stayed clean: not one legitimate query was ever slated for truncation. In the live packet capture taken minutes after enforcement went live, a real user's web.api.vk.com lookup was served in full, 190 bytes, while the flood around it was being reduced to 32-byte stubs.

The twist: success made us blind

Minutes after enforcement went live, our monitor reported the flood had stopped. Zero percent slip. Nothing happening. We had won so completely that the attack had apparently given up on the spot.

It had not. The monitor read the resolver's query log, and gated on response size - the same 512-byte gate as the engine. But enforcement truncates the response before it is written to that log. So the log now recorded a stream of harmless 32-byte answers, the monitor's size gate exempted every one of them, and the tool that had found the attack in the first place went completely blind to it. The flood was still running at full strength. The counters inside the enforcement engine were climbing by four thousand per window. On the wire, a six-second capture showed 632 truncated responses against 14 full ones.

A general lesson worth stealing. When your mitigation sits upstream of your telemetry, a working mitigation and a vanished attack look exactly the same. If we had trusted that monitor we would have concluded the attack was over and possibly rolled the defence back. Any time you deploy a fix, ask: could this fix have broken the instrument I am using to check whether it worked? We built a second monitor that reads the enforcement counters directly, and stopped believing the first one.

The verdict

Enforcement went live at 15:46 on 12 July. The attack kept coming for another fifteen and a half hours, getting nothing but 32-byte stubs, and then - at 07:12 the next morning - it stopped, and has not come back. Reflection capacity is rented by the hour, and an amplifier with a 0.45× ratio is not worth the rent.

Metric Before After
Response to a spoofed query2,257 B (2 fragments)32 B (TC=1)
Amplification factor31.8×0.45×
Attack traffic we emitted~4.6 Mbps~0.15 Mbps
Share of the flood neutralised0%98.2%
Legitimate DNSdropped as collateraluntouched
Cost per queryn/a0.061 ms median
Responses neutralised, totaln/a12,301,925

Twelve million reflected responses that never reached a victim, at a measured cost of 0.061 milliseconds each. No scrubbing appliance, no upstream provider, no traffic contract - a few hundred lines of Go, and a firewall rule that we made less aggressive, not more.

And the honest residual, because a defence you cannot state the limits of is a defence you do not understand: the budget of 5 answers per second gets spent on the attack, so roughly 90 kbps of full-size answers still reached those victims for as long as the flood ran. We killed 98.2% of it, not 100%. The remaining 1.8% is the price of not dropping queries - of leaving a door open so that a real client behind a victim's /24 can still resolve a name.

What this incident is actually about

We did not stop the attacker. We never saw them, we still cannot see them, and for all we know their queries will start arriving again tonight. We did not defeat a botnet, and any write-up claiming otherwise about an attack like this is selling something.

What we did was narrower and, I think, more useful: we refused to be the weapon. The packets still arrive. We simply stopped being willing to turn 71 bytes into 2,257 and post them to a stranger. And we did it without banning a single address - which mattered, because every address we could have banned belonged to somebody who was already being hurt.

If you take one thing from this: check your top-queried domain today. It takes ten seconds, it is the same check that found this, and the reason it matters is that an amplification attack running through your resolver is completely invisible from the inside. Nothing breaks. Nothing gets slow. No alert fires. Your resolver is working perfectly, exactly as designed, at full speed - for somebody else.

Encrypted transports cannot be reflected

This entire attack class needs plain, connectionless UDP/53. DoH, DoT and DoQ require a handshake, and a forged address cannot complete one. Ours are public and free.

Set up encrypted DNS

Related reading: the encrypted DNS protocols compared, spoofing and cache poisoning (the other half of what forged addresses enable), why DNS uses UDP and when it falls back to TCP, and the anatomy of a hardened DNS zone.

Ozy-666 Author · Creator of dnsdoh.art

Ozy-666 builds and operates dnsdoh.art, an encrypted DNS resolver serving DoH, DoH3, DoT and DoQ. This is a first-hand account: the resolver in it is his, the attack happened on 12 July 2026, and every number in this article - the 31.8× ratio, the 0.061 ms, the 12,301,925 truncated responses - was measured on the live server while it was happening. The rate limiter described here was designed, shadow-tested and deployed during the incident.