| Both sides previous revisionPrevious revisionNext revision | Previous revision |
| design:dns [2026/08/27 22:49] – Self-review pass before the reviewers report: drop an unverifiable 73% (the extraction asserted it; the only '73' near 'heard of DNS' in the paper is the string 'Firefox 73'), replace an unsupported 'wrong 60%' with measured venue shares, replace 'the amp karel.kubicek.claude | design:dns [2026/08/27 23:21] (current) – Generic (fable) review applied. Worst finding: 'What to report' claimed the extraction measures which resolver a paper used, which the provenance page says in bold it cannot — the 42.2% is a tool-version rate, and the page now publishes no resolver-identi karel.kubicek.claude |
|---|
| |
| <WRAP important> | <WRAP important> |
| **A DNS answer is a property of the query, not only of the name.** It depends on which resolver you asked, where you asked from, what your source port happened to be, and what was already in a cache. In a run on 2026-08-27 from one machine, **28 of the 75** Tranco top-100 names that had an address at all got **completely disjoint** answers from Google, Cloudflare and Quad9 — no address returned by one resolver was returned by either other. Another **25 of the 100** had no A record at the apex at all. Details and the script are in [[#Measure your own resolver dependence]]. | **A DNS answer is a property of the query, not only of the name.** It depends on which resolver you asked, where you asked from, what your source port happened to be, and what was already in a cache. In a run on 2026-08-27 from one machine, Google, Cloudflare and Quad9 **did not all return the same addresses for 29 of the 75** Tranco top-100 names that had an IPv4 address at all (38.7%), and for **10** of those no two of the three shared a single address. Another **25 of the 100** have no A record at the apex at all. Details and the script are in [[#Measure your own resolver dependence]]. |
| </WRAP> | </WRAP> |
| |
| Everything below about "the literature" is a claim about **seven venues** — CCS, IMC, NDSS, PETS, USENIX Security, TheWebConf and IEEE S&P, 2010–2026, **5,859** papers with extracted full text. EuroS&P, ACSAC, RAID, AsiaCCS, CHI and SOUPS are not in it, and neither is the DNS-operations literature (DNS-OARC, RIPE, IETF). A DNS venue survey would look different; see [[Literature:Corpus]] for what this corpus is and is not. | Everything below about "the literature" is a claim about **seven venues** — CCS, IMC, NDSS, PETS, USENIX Security, TheWebConf and IEEE S&P, 2010–2026, **5,859** extracted papers (full text is readable for 5,855 of them; the other 4 count as negatives in every full-text probe below). EuroS&P, ACSAC, RAID, AsiaCCS, CHI and SOUPS are not in it, and neither is the DNS-operations literature (DNS-OARC, RIPE, IETF). A DNS venue survey would look different; see [[Literature:Corpus]] for what this corpus is and is not. |
| |
| ===== Is this your page? ===== | ===== Is this your page? ===== |
| ^ Instrument ^ Papers ^ State on 2026-08-27 ^ Use it when ^ | ^ Instrument ^ Papers ^ State on 2026-08-27 ^ Use it when ^ |
| | **ZDNS** {[izhikevich2022_zdns]} | 13 | Current. ''zmap/zdns'' v2.1.1 (2026-05-28), pushed 2026-08-24. | You need to resolve millions of names. Its own paper reports //"90K lookups per second when using an external recursive resolver"// and resolving //"50M domains in 10 minutes"//. This is the current default for large active DNS measurement. | | | **ZDNS** {[izhikevich2022_zdns]} | 13 | Current. ''zmap/zdns'' v2.1.1 (2026-05-28), pushed 2026-08-24. | You need to resolve millions of names. Its own paper reports //"90K lookups per second when using an external recursive resolver"// and resolving //"50M domains in 10 minutes"//. This is the current default for large active DNS measurement. | |
| | ''dig'' / ''drill'' / ''nslookup'' | 18 | Current, but they are debugging tools. | Reproducing one answer, or checking a claim by hand. Not a measurement pipeline: 18 papers name one, and in most of those it is a spot check. | | | ''dig'' / ''drill'' / ''nslookup'' / ''tor-resolve'' | 18 | Current, but they are debugging tools. | Reproducing one answer, or checking a claim by hand. Not a measurement pipeline. Note the family is 13 ''dig''/''Dig'', 2 ''nslookup'', 1 ''drill'', 1 ''tor-resolve'' and one string that is not a lookup tool at all (a paper's own "host name matching tool"); **no paper in this corpus names ''host(1)''**, which is why the family label does not. | |
| | **DNS libraries** — ''dnspython'', ''ldns'', ''miekg/dns'', ''getdns'' | 15 | Current. ''dnspython'' 2.8.0 (2025-09-07); ''miekg/dns'' pushed 2026-08-19 with no tagged releases (pin a commit, not a tag). | You need control over the wire format — EDNS options, 0x20 encoding, malformed queries, per-query source ports. | | | **DNS libraries** — ''dnspython'', ''ldns'', ''miekg/dns'', ''getdns'' | 16 | Current. ''dnspython'' 2.8.0 (2025-09-07); ''miekg/dns'' **v1.1.73** (2026-08-19) — it publishes no GitHub //Release// objects, only git tags, so a check that reads the releases API concludes there is nothing to pin. Pin the tag. | You need control over the wire format — EDNS options, 0x20 encoding, malformed queries, per-query source ports. | |
| | **massdns** | 5 | Maintained but slow-moving: v1.1.0 (2024-03-09), last push 2026-04-15. | Bulk stub resolution against a resolver you already have. ZDNS supersedes it for new work. | | | **massdns** | 5 | Maintained but slow-moving: v1.1.0 (2024-03-09), last push 2026-04-15. | Bulk stub resolution against a resolver you already have. ZDNS supersedes it for new work. | |
| | **fpdns** | 7 | Legacy. Fingerprints resolver //software// by behaviour. | Historical comparison. All 7 uses are pre-2024. | | | **fpdns** | 7 | Old (last upstream activity long predates this window) but **not abandoned in practice**: 1 use in 2018, 3 in 2023, **3 in 2024**. | Inferring which resolver implementation is answering, from its behaviour, when it will not tell you (see failure mode 2). Every recent use is in an attack paper that needed to know what it was attacking. | |
| | **DNSViz** | 3 | Current: v0.11.1 (2025-04-21). | DNSSEC chain validation and diagnosis, one zone at a time. | | | **DNSViz** | 2 | Current: v0.11.1 (2025-04-21). | DNSSEC chain validation and diagnosis, one zone at a time. The fold's ''dnssec-tool'' family is 3 papers, but the third is a 2014 use of the //Extended DNSSEC Validator// Firefox extension as a code base — a different artefact, and this row is not its currency. | |
| | **home-grown code** | 35 | — | See the warning below. | | | **home-grown code** | 34 | — | See the warning below. | |
| |
| <WRAP important> | <WRAP important> |
| **A quarter of the instrument population wrote its own resolver, and no two called it the same thing.** 35 papers name a DNS tool the fold maps to "home-grown", and there are **35 distinct strings** for those 35 papers: ''custom DNS measurement code'', ''active DNS crawler'', ''large-scale active DNS measurement system'', ''custom full-graph DNS resolver'', ''Stanford::DNSserver''. One paper, one name, no reuse. That is not a criticism — many of these needed behaviour no library offers — but it does mean **there is almost no shared, comparable active-DNS methodology in this literature**, and it is why a methods section that says "we resolved the domains" tells a replicator nothing. | **A seventh of the population wrote its own DNS code, and almost no two called it the same thing.** 34 papers name a tool the fold maps to "home-grown", under **34 distinct strings**; only two of those strings are used by more than one paper, and both are generic descriptions rather than a shared tool (DNS crawler, authoritative DNS server). The rest are one-offs: ''custom DNS measurement code'', ''active DNS crawler'', ''large-scale active DNS measurement system'', ''custom full-graph DNS resolver'', ''Stanford::DNSserver''. One paper, one name, no reuse. That is not a criticism — many of these needed behaviour no library offers — but it does mean **there is almost no shared, comparable active-DNS methodology in this literature**, and it is why a methods section that says "we resolved the domains" tells a replicator nothing. |
| </WRAP> | </WRAP> |
| |
| |
| ^ Source ^ Papers ^ State on 2026-08-27 ^ What it is ^ | ^ Source ^ Papers ^ State on 2026-08-27 ^ What it is ^ |
| | **Farsight DNSDB** (now DomainTools) | 28 | **Renamed and re-homed.** ''farsightsecurity.com'' **no longer resolves at all**; ''dnsdb.info'' resolves (104.244.14.71) and 301s to ''domaintools.com/platform'', as does the old ''/products/farsight-dnsdb/'' URL. Commercial, with an academic access route. | The largest passive-DNS archive in this literature. A 2019 citation now points at a dead brand. | | | **Farsight DNSDB** (now DomainTools) | 28 | **Renamed and re-homed.** ''farsightsecurity.com'' **no longer resolves at all**; ''dnsdb.info'' still resolves and 301s to ''domaintools.com/platform'', as does the old ''/products/farsight-dnsdb/'' URL. (Its address rotates between checks, which is a small joke at this page's expense — do not pin it.) Commercial, with an academic access route. | The largest passive-DNS archive in this literature. A 2019 citation now points at a dead brand. | |
| | **OpenINTEL** | 14 | Current and actively extended: forward and reverse DNS, zone-based and top-list-based, ccTLD apex lists, a real-time zone-change feed. Academic, by request. | Daily active measurement of large zones since 2015. The right source for "what did this name resolve to on that day" at zone scale. | | | **OpenINTEL** | 14 | Current and actively extended: forward and reverse DNS, zone-based and top-list-based, ccTLD apex lists, a real-time zone-change feed. Academic, by request. | Daily active measurement of large zones since 2015. The right source for "what did this name resolve to on that day" at zone scale. | |
| | **Rapid7 Open Data** (Sonar FDNS / RDNS) | 8 | **Still live**, contrary to what a 2022-era memory will tell you: ''sonar.fdns_v2'' last updated 2026-08-23 and ''sonar.rdns_v2'' 2026-08-26. | Bulk forward and reverse DNS over the IPv4 space. | | | **Rapid7 Open Data** (Sonar FDNS / RDNS) | 8 | **Still live**, contrary to what a 2022-era memory will tell you: ''sonar.fdns_v2'' last updated 2026-08-23, ''sonar.rdns_v2'' 2026-08-26. The **host has moved**: every path under ''opendata.rapid7.com'' now 301s to ''sonardata.rapid7.com''. | Bulk forward and reverse DNS over the IPv4 space. | |
| | **ICANN CZDS** / zone files | 3 | Current. Per-TLD application. | The authoritative list of registered names in participating zones. It is the denominator a "we resolved the top 1M" study does not have. | | | **ICANN CZDS** / zone files | 3 | Current. Per-TLD application. | The authoritative list of registered names in participating zones. It is the denominator a "we resolved the top 1M" study does not have. | |
| | **Active DNS Project**; **360 / 114DNS**; **Spamhaus**; **DNS Observatory** | 2 / 4 / — / 1 | Various. | Regional and vendor feeds; the Chinese feeds are the only route to some of that namespace. | | | **Active DNS Project**; **360 / 114DNS**; **DNS Observatory** | 2 / 4 / 1 | Various. | Regional and vendor feeds; the Chinese feeds are the only route to some of that namespace. Spamhaus's passive-DNS API and seven other one-off feeds fold into the 8-paper "other passive-DNS feeds" row above rather than getting their own. | |
| |
| ==== Where you ask from ==== | ==== Where you ask from ==== |
| |
| **RIPE Atlas** appears in 77 papers corpus-wide and 24 of the 244 here. It is not in the membership rule because most of those 77 use it for traceroute, not DNS — but it is the standard way to ask the same question from thousands of vantage points, and DNS is one of its native measurement types. Two papers on this page are built on it: {[randall2021_home]} for interception and {[bhaskar2022_many]} for censorship. | **RIPE Atlas** appears in **75** papers corpus-wide and **24** of the 244 here — and **21** of those 24 are IMC papers, which is another way of saying the same thing as the venue table. It is not in the membership rule because most of the 75 use it for traceroute, not DNS. But it is the standard way to ask the same DNS question from thousands of vantage points, and DNS is one of its native measurement types: {[randall2021_home]} localises interception with it, {[moura2019_cache]} probes recursive resolvers worldwide with it, {[aldalky2019_look]} uses it for globally distributed probes, and {[randall2020_trufflehunter]} uses it to emulate geographically distributed users filling resolver caches. {[bhaskar2022_many]}, by contrast, does **not** use RIPE Atlas — it sends packets from its own vantage point, which is exactly why source-address choice is the variable it can manipulate. |
| |
| **Generic scanners** — ZMap and XMap (112 papers), ZGrab (31), Scapy (33) — appear in 38 of the 244. ZMap is how you find open resolvers; it is not a DNS toolkit. [[Design:Automated measurements]] covers the scan branch generally. | **Generic scanners** — ZMap and XMap (112 papers), ZGrab (31), Scapy (33) — appear in 38 of the 244. ZMap is how you find open resolvers; it is not a DNS toolkit. [[Design:Automated measurements]] covers the scan branch generally. |
| ==== 1. The answer depends on which resolver you asked ==== | ==== 1. The answer depends on which resolver you asked ==== |
| |
| This is the oldest finding on the page and it has never stopped being true. Ager et al. {[ager2010_comparing]} sent the same queries to the local resolver, Google and OpenDNS from 60-plus vantage points in 2010 and found that //"the answers to the DNS resolvers differ in terms of subnets for approximately 2,000 out of our 10,000 host names. In half of these cases, the returned IP addresses even belong to different ASs and countries."//((Quoted with whitespace normalised: both the .cols rendering and a pypdf re-extraction of the PDF render the figures as 2 , 000 and 10, 000, and hyphenate dif- fer across a line break. Checked 2026-08-27; see [[provenance:design:dns]].)) Sixteen years later the run in [[#Measure your own resolver dependence]] gets 37.3% disjoint on a 75-name comparable set. | This is the oldest finding on the page and it has never stopped being true. Ager et al. {[ager2010_comparing]} sent the same queries to the local resolver, Google and OpenDNS from 60-plus vantage points in 2010 and found that //"the answers to the DNS resolvers differ in terms of subnets for approximately 2,000 out of our 10,000 host names. In half of these cases, the returned IP addresses even belong to different ASs and countries."//((Quoted with whitespace normalised: both the .cols rendering and a pypdf re-extraction of the PDF render the figures as 2 , 000 and 10, 000, and hyphenate dif- fer across a line break. Checked 2026-08-27; see [[provenance:design:dns]].)) Sixteen years later the run in [[#Measure your own resolver dependence]] gets **38.7%** not-identical on a 75-name comparable set, with **13.3%** of them sharing no address between any pair of resolvers. |
| |
| Two mechanisms do most of it. CDNs and GeoDNS answer according to where they think you are, and **EDNS Client Subnet** (RFC 7871 — note it is **Informational**, not a standard) is how a public resolver tells them. Al-Dalky et al. {[aldalky2019_look]} measured how resolvers actually use it and found the behaviour is not uniform: //"3382 out of 4147 resolvers in the CDN dataset send 100% of their A and AAAA queries with an ECS option"//, and that //"103 recursive resolvers, or over half of all recursive resolvers we could study"//, //"don't control caching based on scope at all"// — they reuse a cached response outside the prefix it was scoped for — a caching bug that hands the wrong client the wrong answer. Anycast does the rest; [[Design:IP classification]] covers why geolocating an anycast address is meaningless. | Two mechanisms do most of it. CDNs and GeoDNS answer according to where they think you are, and **EDNS Client Subnet** (RFC 7871 — note it is **Informational**, not a standard) is how a public resolver tells them. Al-Dalky et al. {[aldalky2019_look]} measured how resolvers actually use it and found the behaviour is not uniform: //"3382 out of 4147 resolvers in the CDN dataset send 100% of their A and AAAA queries with an ECS option"//, and that //"103 recursive resolvers, or over half of all recursive resolvers we could study"//, //"don't control caching based on scope at all"// — they reuse a cached response outside the prefix it was scoped for — a caching bug that hands the wrong client the wrong answer. Anycast does the rest; [[Design:IP classification]] covers why geolocating an anycast address is meaningless. |
| |
| ^ Protocol ^ RFC and status (checked 2026-08-27) ^ Papers in the 244, by window ^ Where it stands ^ | ^ Protocol ^ RFC and status (checked 2026-08-27) ^ Papers in the 244, by window ^ Where it stands ^ |
| | **DoT** — DNS over TLS | RFC 7858, May 2016, Proposed Standard, updated by RFC 8310 | 0 / 0 / 23 / 7 / 4 | Shipped and stable. Android's "Private DNS" is DoT, which is why it matters for mobile measurement even though browser work ignores it. | | | **DoT** — DNS over TLS | RFC 7858, May 2016, Proposed Standard, updated by RFC 8310 | 0 / 0 / 23 / 7 / 4 | Shipped and stable, but **check what your Android handset is actually speaking**: Private DNS launched as DoT in Android 9, and since a July 2022 Google Play system update //"Android devices from Android 11 onwards will use DoH3 instead of DoT for well-known DNS servers which support it"//.((Google Online Security Blog, //DNS-over-HTTP/3 in Android//, 19 July 2022, ''security.googleblog.com/2022/07/dns-over-http3-in-android.html''; fetched and the sentence re-read on 2026-08-27.)) DoH3 is HTTP/3-framed DNS — not DoT, and not RFC 9250 DoQ either. | |
| | **DoH** — DNS over HTTPS | RFC 8484, October 2018, Proposed Standard | 0 / 0 / 23 / 16 / 9 | The one the browsers deployed. The most-measured of the encrypted transports and still the default assumption. | | | **DoH** — DNS over HTTPS | RFC 8484, October 2018, Proposed Standard | 0 / 0 / 23 / 16 / 9 | The one the browsers deployed. The most-measured of the encrypted transports and still the default assumption. | |
| | **DoQ** — DNS over QUIC | RFC 9250, May 2022, Proposed Standard | 0 / 0 / 4 / 1 / 1 | Standardised, barely measured: **6** of the 244 mention it at all (10 across the whole 5,859). An open area, not a settled method. | | | **DoQ** — DNS over QUIC | RFC 9250, May 2022, Proposed Standard | 0 / 0 / 4 / 1 / 1 | Standardised, barely measured: **6** of the 244 mention it at all (10 across the whole 5,859). An open area, not a settled method. | |
| | | **DoC** — DNS over CoAP | RFC 9953, **March 2026**, Proposed Standard | 0 / 0 / 0 / 0 / 0 | Five months old at the time of writing. The zero is a **run probe, not an assumption**: the phrase "DNS over CoAP" appears in no paper in the corpus, and the 20 that mention CoAP at all mean the IoT protocol (a bare DoC matches two papers and both are homographs — a US government department and a table symbol). It targets constrained networks, so it is unlikely to reach a web crawl; it is here because a table headed "what is actually current" that omitted a five-month-old standardised transport would be describing 2024. | |
| | **ODoH** — Oblivious DoH | RFC 9230, June 2022, **Experimental** | 0 / 0 / 5 / 4 / 2 | Experimental in the RFC sense as well as the deployment sense. Do not describe it as a deployed default. | | | **ODoH** — Oblivious DoH | RFC 9230, June 2022, **Experimental** | 0 / 0 / 5 / 4 / 2 | Experimental in the RFC sense as well as the deployment sense. Do not describe it as a deployed default. | |
| | **DDR** — Discovery of Designated Resolvers | RFC 9462, November 2023, Proposed Standard (with DNR, RFC 9463) | 0 / 0 / 0 / 0 / 1 | The frontier, and the answer to "how does a client find an encrypted resolver at all". **Exactly one paper in this corpus measures it.** | | | **DDR** — Discovery of Designated Resolvers | RFC 9462, November 2023, Proposed Standard (with DNR, RFC 9463) | 0 / 0 / 0 / 0 / 1 | The frontier, and the answer to "how does a client find an encrypted resolver at all". **Exactly one paper in this corpus measures it.** | |
| |
| <WRAP tip> | <WRAP tip> |
| **DDR is where the measurement question now is, and the one paper that looked says the deployment is broken.** Ververis et al. {[ververis2025_path]} measured DDR adoption and configuration and report //"widespread misconfigurations, including incomplete and incorrect DDR configurations that prevent clients from successfully transitioning to encrypted resolvers. In over 99% of observed cases, DDR-compliant clients may fail to upgrade to DoE due to these deployment issues"//. One paper is not a literature. If you want an encrypted-DNS topic that is not already crowded, this is it — and cite it as one measurement, not as an established rate. | **DDR is where the measurement question now is, and what it measures is unsettled.** Ververis et al. {[ververis2025_path]} — the one paper in this corpus that measures DDR — report //"widespread misconfigurations, including incomplete and incorrect DDR configurations that prevent clients from successfully transitioning to encrypted resolvers. In over 99% of observed cases, DDR-compliant clients may fail to upgrade to DoE due to these deployment issues"//. |
| | |
| | One paper is not a literature, and this one's central finding is contested outside these seven venues. Nosyk, Duda and Korczyński analysed //"over 321k DDR-enabled open resolvers"// and found not brokenness but **centralisation**: //"an apparent dominance of the Google Public DNS with 80.8% of DDR-enabled resolvers designating dns.google"//, Cloudflare second at 12.4%, and //"these are designated by 97.4% of DDR-enabled resolvers, highlighting the reliance on a handful of big operators"//.((Yevheniya Nosyk, //Discovering the Discovery of Designated Resolvers//, APNIC Blog, 2 September 2025 — "Andrzej Duda and Maciej Korczyński contributed to this work"; ''blog.apnic.net/2025/09/02/discovering-the-discovery-of-designated-resolvers/'', fetched 2026-08-27.)) The same group then published //The Illusion of DDR Deployment// at ANRW 2026, arguing that resolvers scored as misconfigured are in many cases **transparent forwarders** — a different explanation for the same observation.((doi ''10.1145/3822163.3827932''. Confirmed via Crossref on 2026-08-27: title, the three authors, //Proceedings of the 2026 Applied Networking Research Workshop//, published 2026-07-20. The full text is behind an ACM 403 from this host, so this sentence rests on the metadata plus the authors' APNIC write-up, not on a reading of the paper.)) |
| | |
| | So: if you want an encrypted-DNS topic that is not already crowded, this is it — and neither the misconfiguration rate nor its interpretation is settled. Cite the 99% as one measurement under dispute, not as a rate. Note also where all of this is published: PoPETs, ANRW and an operator blog. **A DNS measurement literature review confined to these seven venues would have found one of the three.** |
| </WRAP> | </WRAP> |
| | |
| | **One trust boundary the table above does not cover.** Every protocol in it encrypts the **stub-to-recursive** hop — your machine to the resolver. The **recursive-to-authoritative** hop is a separate problem with a separate document, RFC 9539, //Unilateral Opportunistic Deployment of Encrypted Recursive-to-Authoritative DNS// (February 2024, **Experimental**). If your paper says "we measured encrypted DNS", say which hop; a 100%-DoH client can still have every one of its queries travel the second hop in the clear. |
| |
| **Two dated facts a student will otherwise get wrong.** | **Two dated facts a student will otherwise get wrong.** |
| |
| * **Firefox's DoH default is not a compile-time default and you cannot read it out of the source tree.** ''network.trr.mode'' has value **0** in ''modules/libpref/init/StaticPrefList.yaml'' on ''mozilla-firefox/firefox'' ''main'', and ''network.trr.uri'' is the empty string in ''all.js''. The rollout is server-side: Mozilla's Remote Settings ''doh-providers'' collection is public and returns **5 provider records** today — Cloudflare (the only one with ''autoDefault'' true), Shaw, Comcast Xfinity, NextDNS and CIRA Canadian Shield.((''firefox.settings.services.mozilla.com/v1/buckets/main/collections/doh-providers/records'', no authentication, fetched 2026-08-27. Command in [[provenance:design:dns]].)) If your paper says "Firefox sends DNS to Cloudflare by default", say **in which region and on what date**, and check it against that collection rather than against the tree. | * **Firefox's DoH default is not a compile-time default and you cannot read it out of the source tree.** ''network.trr.mode'' has value **0** in ''modules/libpref/init/StaticPrefList.yaml'' on ''mozilla-firefox/firefox'' ''main'', and ''network.trr.uri'' is the empty string in ''all.js''. The rollout is server-side: Mozilla's Remote Settings ''doh-providers'' collection is public and returns **5 provider records** today — Cloudflare (the only one with ''autoDefault'' true), Shaw, Comcast Xfinity, NextDNS and CIRA Canadian Shield.((''firefox.settings.services.mozilla.com/v1/buckets/main/collections/doh-providers/records'', no authentication, fetched 2026-08-27. Command in [[provenance:design:dns]].)) If your paper says "Firefox sends DNS to Cloudflare by default", say **in which region and on what date**, and check it against that collection rather than against the tree. |
| * **Encrypted transport is not encrypted content to your resolver.** Siby et al. {[siby2020_encrypted]} show DoH traffic still fingerprints: they identify //"domains with high accuracy in closed and open world settings, using 124 times less data than attacks on HTTPS flows"//, and a classifier over TLS record lengths alone recovers which resolver a client is using (//"We can identify resolvers"// … //"with 95% accuracy"//). Users largely do not know any of this is happening — Nisenoff et al. {[nisenoff2023_awareness]} report that //"Of the participants who reported having heard of DNS, only 59.9% had heard of encrypted DNS"//, and that of their 107 remaining browser participants //"85 (79.4%) had the default settings for their browser selected"//. | * **Encrypted transport is not encrypted content to your resolver.** Siby et al. {[siby2020_encrypted]} show DoH traffic still fingerprints: they identify //"domains with high accuracy in closed and open world settings, using 124 times less data than attacks on HTTPS flows"//, and a classifier over TLS record lengths alone recovers which resolver a client is using (//"We can identify resolvers"// … //"with 95% accuracy"//). Users largely do not know any of this is happening — Nisenoff et al. {[nisenoff2023_awareness]} report that //"73% of all participants reported having heard of DNS before the survey"//, that //"Of the participants who reported having heard of DNS, only 59.9% had heard of encrypted DNS"//, and that of the 107 participants who could reach their browser's DNS settings menu //"85 (79.4%) had the default settings for their browser selected"//. |
| |
| ===== DNS as the instrument for a web question ===== | ===== DNS as the instrument for a web question ===== |
| ===== Active DNS measurement is scanning ===== | ===== Active DNS measurement is scanning ===== |
| |
| Sending queries to resolvers you do not own is a scan, and it has a failure mode the rest of the scan branch does not: **an open resolver will amplify whatever you send it at somebody else.** Kührer et al. {[khrer2015_going]} //"find up to 20 million open resolvers in the weekly IPv4 scans in 2015"//, and **58 of the 244** papers here match a full-text probe for amplification or reflection (**15** of them say so in the title). If your measurement queries open resolvers, you are one misconfigured source address away from being the attack. | Sending queries to resolvers you do not own is a scan, and it has a failure mode the rest of the scan branch does not: **an open resolver will amplify whatever you send it at somebody else.** Kührer et al. {[khrer2015_going]} //"find up to 20 million open resolvers in the weekly IPv4 scans in 2015"//, and **58 of the 244** papers here match a full-text probe for amplification or reflection (**15** of them say so in the title or the slug). If your measurement queries open resolvers, you are one misconfigured source address away from being the attack. |
| |
| The DNS population states more about ethics than the corpus average on three of four fields, and **less** on the one that names an ethics board — which is the pattern you would expect from work that mitigates harm operationally and does not go to an IRB because it has no human subjects: | The DNS population states more about ethics than the corpus average on three of four fields, and **less** on the one that names an ethics board. A plausible reading is that this is work which mitigates harm operationally and mostly has no human subjects to take to an IRB — but 16 of the 244 are tagged ''interview-or-survey'' and 11 ''user-study'', so that is a tendency here, not a property: |
| |
| ^ Field ^ DNS population (241 empirical of the 244) ^ Whole corpus (5,118 empirical) ^ | ^ Field ^ DNS population (241 empirical of the 244) ^ Whole corpus (5,118 empirical) ^ |
| </WRAP> | </WRAP> |
| |
| A DNS methods section is complete when a replicator can answer all of these. The extraction has a field for only the first two, and the table above is how often those are answered (**50.8%** and **42.2%**); for the rest the schema has no field, so this list is a standard, not a measurement: | A DNS methods section is complete when a replicator can answer all of these. **Almost none of it is measurable from the extraction, and the one item that matters most is the least measurable.** The table above measures two things that are only adjacent to this list: whether a paper named a **vantage location** (50.8%), which is the "where from" item, and whether it named **any tool version at all** (42.2%), which is not about resolvers. For **which resolver** — the first and most important item — the schema has no field, and a full-text probe for resolver IP literals is an upper bound on mentions, not on use. **This page therefore publishes no reporting rate for resolver identification**, and you should distrust any page that does. The list below is a standard, not a measurement: |
| |
| * **Which resolver**, by address and by name, including whether it was your own recursive server, the system resolver, or a public one. If more than one, which for which measurement. | * **Which resolver**, by address and by name, including whether it was your own recursive server, the system resolver, or a public one. If more than one, which for which measurement. |
| * **Where from** — the source addresses and networks, and whether they were datacenter or residential ([[Design:Crawling location]]). | * **Where from** — the source addresses and networks, and whether they were datacenter or residential ([[Design:Crawling location]]). |
| * **The transport** — Do53 over UDP or TCP, DoT, DoH, DoQ — and whether you fell back. | * **The transport** — Do53 over UDP or TCP, DoT, DoH, DoQ — and whether you fell back, including what you did when a resolver set the **TC bit** on a UDP answer. Only **9.0%** of the 244 mention truncation or TCP fallback at all. |
| | * **Which record types, and which address family.** A study that queries A only and a study that queries A and AAAA produce different "did not resolve" sets, and neither is wrong until you fail to say which you did. **26.6%** of the 244 mention AAAA, **30.7%** CNAME, and **3.3%** the SVCB/HTTPS record — so dual-stack is the minority practice here, not the default. |
| * **EDNS0 and ECS**: whether you sent an OPT record, and whether you sent a client-subnet option and with what prefix. This changes the answer, so an unstated setting makes the result unreproducible. | * **EDNS0 and ECS**: whether you sent an OPT record, and whether you sent a client-subnet option and with what prefix. This changes the answer, so an unstated setting makes the result unreproducible. |
| * **Query rate and retry policy**, and what you did on SERVFAIL and timeout. A timeout silently recorded as "no record" is the most common way a DNS dataset acquires a systematic hole. | * **Query rate and retry policy**, and what you did on SERVFAIL and timeout. A timeout silently recorded as "no record" is the most common way a DNS dataset acquires a systematic hole. |
| |
| Python 3.8+, standard library only: no dnspython, no dig, nothing to install. | Python 3.8+, standard library only: no dnspython, no dig, nothing to install. |
| | |
| | **This script queries A records only.** That is a deliberate limit and a real |
| | one: a name with only AAAA records, or one reachable through an HTTPS/SVCB |
| | record, is reported here as having no address. If your study is dual-stack, |
| | query both families and report both -- "the name did not resolve" and "the name |
| | has no IPv4 address" are different findings and this script can only tell you |
| | the second. |
| |
| What it reports, per name: | What it reports, per name: |
| * the A-record set each resolver returned | * the A-record set each resolver returned, and whether the answer was |
| | truncated (TC set) rather than complete |
| * whether all resolvers agree, agree on the /24, or disagree outright | * whether all resolvers agree, agree on the /24, or disagree outright |
| * the TTL each resolver returned, and the TTL on an immediate second query | * the TTL each resolver returned, and the TTL on an immediate second query |
| |
| import argparse | import argparse |
| | import itertools |
| import json | import json |
| import random | import random |
| |
| def parse_answers(buf, txid): | def parse_answers(buf, txid): |
| """Return (rcode, [(ttl, ip), ...]) for the A records in a response.""" | """Return (rcode, truncated, [(ttl, ip), ...]) for the A records in a response. |
| | |
| | The TC (truncation) bit matters here and is easy to skip. This script sends |
| | no EDNS0 OPT record on purpose (see build_query), so a UDP response is |
| | capped at 512 bytes; a resolver with more addresses than fit sets TC and |
| | returns a partial answer. Recording that as the resolver's complete set |
| | would manufacture "partial" and "disjoint" verdicts out of nothing. When TC |
| | is set the result is flagged rather than silently trusted -- a real |
| | measurement would retry over TCP, which this script deliberately does not |
| | do, because the point is to show you the condition, not to paper over it. |
| | """ |
| if len(buf) < 12: | if len(buf) < 12: |
| raise ValueError("short response") | raise ValueError("short response") |
| raise ValueError("transaction id mismatch: possible off-path answer") | raise ValueError("transaction id mismatch: possible off-path answer") |
| rcode = flags & 0x0F | rcode = flags & 0x0F |
| | truncated = bool(flags & 0x0200) |
| off = 12 | off = 12 |
| for _ in range(qd): | for _ in range(qd): |
| out.append((ttl, socket.inet_ntoa(buf[off:off + 4]))) | out.append((ttl, socket.inet_ntoa(buf[off:off + 4]))) |
| off += rdlen | off += rdlen |
| return rcode, out | return rcode, truncated, out |
| |
| |
| def query(server, name, timeout=3.0): | def query(server, name, timeout=3.0): |
| """One UDP query. Returns a dict; never raises.""" | """One UDP query. Returns a dict; never raises.""" |
| txid, pkt = build_query(name, txid=None) | |
| sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| sock.settimeout(timeout) | |
| started = time.time() | started = time.time() |
| | deadline = started + timeout |
| try: | try: |
| | # Inside the try: an unencodable name must return a dict like every |
| | # other failure, not raise. The docstring promises that. |
| | txid, pkt = build_query(name, txid=None) |
| sock.sendto(pkt, (server, 53)) | sock.sendto(pkt, (server, 53)) |
| # Accept only a datagram from the address we asked. This does not make | # Accept only a datagram from the address we asked. This does not make |
| # does -- but it stops a stray answer from a different box being | # does -- but it stops a stray answer from a different box being |
| # recorded as this resolver's. | # recorded as this resolver's. |
| | # |
| | # The timeout is a TOTAL deadline, not a per-datagram one. Calling |
| | # settimeout() once outside the loop restarts the clock on every packet |
| | # received, so a stream of unrelated datagrams arriving faster than the |
| | # timeout keeps one query alive indefinitely -- which is a bad property |
| | # for a script whose subject is off-path packets. |
| while True: | while True: |
| | remaining = deadline - time.time() |
| | if remaining <= 0: |
| | raise socket.timeout("total deadline exceeded") |
| | sock.settimeout(remaining) |
| data, peer = sock.recvfrom(4096) | data, peer = sock.recvfrom(4096) |
| if peer[0] == server: | if peer[0] == server: |
| break | break |
| rcode, answers = parse_answers(data, txid) | rcode, truncated, answers = parse_answers(data, txid) |
| return {"ok": True, "rcode": RCODE.get(rcode, str(rcode)), | return {"ok": True, "rcode": RCODE.get(rcode, str(rcode)), |
| | "truncated": truncated, |
| "rtt_ms": round((time.time() - started) * 1000, 1), | "rtt_ms": round((time.time() - started) * 1000, 1), |
| "ttls": [t for t, _ in answers], | "ttls": [t for t, _ in answers], |
| except Exception as exc: # timeout, refused, malformed | except Exception as exc: # timeout, refused, malformed |
| return {"ok": False, "error": f"{type(exc).__name__}: {exc}", | return {"ok": False, "error": f"{type(exc).__name__}: {exc}", |
| | "truncated": False, |
| "rtt_ms": round((time.time() - started) * 1000, 1), | "rtt_ms": round((time.time() - started) * 1000, 1), |
| "ttls": [], "ips": []} | "ttls": [], "ips": []} |
| |
| def verdict(per_resolver): | def verdict(per_resolver): |
| """Classify one name's answers across resolvers.""" | """Classify one name's answers across resolvers. |
| | |
| | The comparison is PAIRWISE, deliberately. An earlier version of this |
| | function intersected all answering resolvers at once, which meant that with |
| | three resolvers, two agreeing exactly and one differing produced an empty |
| | three-way intersection and was labelled "disjoint" -- contradicting the word |
| | itself. On a 100-name run, 17 of 28 names labelled "disjoint" had two of the |
| | three resolvers returning byte-identical addresses. If you extend this to |
| | more resolvers, keep the comparison pairwise. |
| | |
| | Verdicts, in order of severity: |
| | identical every answering resolver returned exactly the same set |
| | partial some pair shares an address, but not all sets are equal |
| | same-/24 no pair shares an address, but some pair shares a /24 |
| | disjoint no pair shares an address or even a /24 |
| | no-A-record every resolver said NOERROR and returned no A record |
| | one-answered exactly one resolver returned an address and the others |
| | returned NOERROR with none -- the most extreme disagreement |
| | there is, and easy to mistake for a dead name |
| | insufficient fewer than two resolvers answered, for some other reason |
| | """ |
| answering = {n: r for n, r in per_resolver.items() if r["ok"] and r["ips"]} | answering = {n: r for n, r in per_resolver.items() if r["ok"] and r["ips"]} |
| if len(answering) < 2: | if len(answering) < 2: |
| for r in per_resolver.values()): | for r in per_resolver.values()): |
| return "no-A-record" | return "no-A-record" |
| | if (len(answering) == 1 |
| | and all(r["ok"] and r["rcode"] == "NOERROR" for r in per_resolver.values())): |
| | return "one-answered" |
| return "insufficient" | return "insufficient" |
| sets = [frozenset(r["ips"]) for r in answering.values()] | sets = [set(r["ips"]) for r in answering.values()] |
| if len(set(sets)) == 1: | if all(a == b for a, b in itertools.combinations(sets, 2)): |
| return "identical" | return "identical" |
| if set.intersection(*[set(s) for s in sets]): | if any(a & b for a, b in itertools.combinations(sets, 2)): |
| return "overlapping" | return "partial" |
| nets = [frozenset(slash24(ip) for ip in s) for s in sets] | nets = [{slash24(ip) for ip in s} for s in sets] |
| if set.intersection(*[set(n) for n in nets]): | if any(a & b for a, b in itertools.combinations(nets, 2)): |
| return "same-/24" | return "same-/24" |
| return "disjoint" | return "disjoint" |
| | |
| | |
| | |
| | def _self_test(): |
| | """Cases from a real run, including the ones that caught the bug this |
| | function used to have. `python3 resolver_disagreement.py --self-test`.""" |
| | def r(ips, ok=True, rcode="NOERROR"): |
| | return {"ok": ok, "rcode": rcode, "rtt_ms": 1.0, "ttls": [60] * len(ips), |
| | "ips": sorted(ips)} |
| | cases = [ |
| | # all three identical |
| | ({"a": r(["1.1.1.1"]), "b": r(["1.1.1.1"]), "c": r(["1.1.1.1"])}, "identical"), |
| | # facebook.com in the 2026-08-27 run: two byte-identical, one different. |
| | # The all-at-once intersection called this "disjoint". It is not. |
| | ({"a": r(["157.240.17.35"]), "b": r(["157.240.17.35"]), "c": r(["157.240.0.35"])}, |
| | "partial"), |
| | # wikipedia.org: two identical, third differs and is in another /24 |
| | ({"a": r(["185.15.58.224"]), "b": r(["185.15.58.224"]), "c": r(["185.15.59.224"])}, |
| | "partial"), |
| | # google.com: three mutually disjoint /16s -- genuinely disjoint |
| | ({"a": r(["74.125.29.100"]), "b": r(["172.217.208.100"]), "c": r(["142.250.154.100"])}, |
| | "disjoint"), |
| | # no address in common but a shared /24 |
| | ({"a": r(["9.9.9.1"]), "b": r(["9.9.9.2"]), "c": r(["9.9.9.3"])}, "same-/24"), |
| | # every resolver says NOERROR with no A record: a property of the zone |
| | ({"a": r([]), "b": r([]), "c": r([])}, "no-A-record"), |
| | # a real failure is not the same thing |
| | ({"a": r([], ok=False), "b": r([]), "c": r([])}, "insufficient"), |
| | # NXDOMAIN everywhere is also not "no A record at the apex" |
| | ({"a": r([], rcode="NXDOMAIN"), "b": r([], rcode="NXDOMAIN"), |
| | "c": r([], rcode="NXDOMAIN")}, "insufficient"), |
| | # only one resolver answered |
| | ({"a": r(["1.2.3.4"]), "b": r([]), "c": r([])}, "one-answered"), |
| | # two resolvers, sets overlap partially |
| | ({"a": r(["1.2.3.4", "1.2.3.5"]), "b": r(["1.2.3.5", "9.9.9.9"])}, "partial"), |
| | ] |
| | bad = 0 |
| | for i, (given, want) in enumerate(cases, 1): |
| | got = verdict(given) |
| | if got != want: |
| | bad += 1 |
| | print(f" FAIL case {i}: got {got!r}, want {want!r}") |
| | print(f"self-test: {len(cases) - bad}/{len(cases)} pass") |
| | return 0 if bad == 0 else 1 |
| |
| |
| ap.add_argument("--timeout", type=float, default=3.0) | ap.add_argument("--timeout", type=float, default=3.0) |
| ap.add_argument("--json", help="write the full per-query record here") | ap.add_argument("--json", help="write the full per-query record here") |
| | ap.add_argument("--self-test", action="store_true", |
| | help="check verdict() against known cases and exit") |
| args = ap.parse_args() | args = ap.parse_args() |
| | |
| | if args.self_test: |
| | raise SystemExit(_self_test()) |
| |
| names = list(args.names) | names = list(args.names) |
| elif ttl2 == ttl1: | elif ttl2 == ttl1: |
| cached = f" (2nd query TTL {ttl2}: unchanged)" | cached = f" (2nd query TTL {ttl2}: unchanged)" |
| | trunc = " TRUNCATED (TC set, answer incomplete)" if a.get("truncated") else "" |
| print(f" {label:<12} {a['rcode']:<9} {a['rtt_ms']:>6.1f} ms " | print(f" {label:<12} {a['rcode']:<9} {a['rtt_ms']:>6.1f} ms " |
| f"ttl={ttl1:<6} {', '.join(a['ips']) or '(no A record)'}{cached}") | f"ttl={ttl1:<6} {', '.join(a['ips']) or '(no A record)'}{cached}{trunc}") |
| record.append({"name": name, "verdict": v, "first": first, "second": second}) | record.append({"name": name, "verdict": v, "first": first, "second": second}) |
| print() | print() |
| |
| print(f"summary A: all {len(names)} names") | print(f"summary A: all {len(names)} names") |
| for v in ("identical", "overlapping", "same-/24", "disjoint", | for v in ("identical", "partial", "same-/24", "disjoint", |
| "no-A-record", "insufficient"): | "one-answered", "no-A-record", "insufficient"): |
| if v in tally: | if v in tally: |
| print(f" {v:<14} {tally[v]:>4} {100 * tally[v] / len(names):5.1f}%") | print(f" {v:<14} {tally[v]:>4} {100 * tally[v] / len(names):5.1f}%") |
| comparable = sum(tally.get(v, 0) for v in | comparable = sum(tally.get(v, 0) for v in |
| ("identical", "overlapping", "same-/24", "disjoint")) | ("identical", "partial", "same-/24", "disjoint")) |
| | disagree = comparable - tally.get("identical", 0) |
| print() | print() |
| print(f"summary B: the {comparable} names where at least two resolvers " | print(f"summary B: the {comparable} names where at least two resolvers " |
| f"returned an address") | f"returned an address") |
| for v in ("identical", "overlapping", "same-/24", "disjoint"): | for v in ("identical", "partial", "same-/24", "disjoint"): |
| if v in tally and comparable: | if v in tally and comparable: |
| print(f" {v:<14} {tally[v]:>4} {100 * tally[v] / comparable:5.1f}%") | print(f" {v:<14} {tally[v]:>4} {100 * tally[v] / comparable:5.1f}%") |
| | if comparable: |
| | print(f" {'NOT identical':<14} {disagree:>4} {100 * disagree / comparable:5.1f}%" |
| | f" <- the headline: at least one resolver disagreed with another") |
| print() | print() |
| print("Summary B is the denominator to quote. Summary A mixes two different") | print("Summary B is the denominator to quote. Summary A mixes two different") |
| print("list has no address to disagree about.") | print("list has no address to disagree about.") |
| print() | print() |
| print("'disjoint' means no resolver returned an address any other resolver") | print("'disjoint' means NO PAIR of resolvers returned an address in common, and") |
| print("returned. If your pipeline resolves names once, through one resolver,") | print("no pair even shared a /24. 'partial' means some pair overlapped and some") |
| print("that is the share of your dataset another resolver would have") | print("pair did not. The number to quote for \"how much does my choice of") |
| print("disagreed with entirely -- from this vantage point, at this instant.") | print("resolver matter\" is NOT identical: that is the share of your name list") |
| | print("where resolvers did not all agree -- from this vantage point, at this") |
| | print("instant. 'disjoint' alone understates it; quoting 'disjoint' as though") |
| | print("it meant \"any disagreement\" overstates it.") |
| |
| if args.json: | if args.json: |
| </file> | </file> |
| |
| Real output, run on 2026-08-27 from a single European vantage point against the **Tranco top 100** (list ID ''46W9X''), abridged mechanically by scripts/build_dns_codeblock.mjs (the two header lines, the google.com stanza with each address list cut after two entries, and both summary blocks, all verbatim). The full 100-name output and the JSON are on [[provenance:design:dns]]: | Real output, run on 2026-08-27 from a single European vantage point against the **Tranco top 100** (list ID ''46W9X''), abridged mechanically by ''scripts/build_dns_codeblock.mjs'' — the two header lines, the **first** stanza whose verdict is ''disjoint'' with each address list cut after two entries, and both summary blocks, all verbatim. The example is selected by verdict rather than pinned to a name, because a name's verdict changes between runs. The full 100-name output and the JSON are on [[provenance:design:dns]]: |
| |
| <code> | <code> |
| # resolvers: Google (8.8.8.8), Cloudflare (1.1.1.1), Quad9 (9.9.9.9) | # resolvers: Google (8.8.8.8), Cloudflare (1.1.1.1), Quad9 (9.9.9.9) |
| # names: 100 run at 2026-08-27T22:31:01Z | # names: 100 run at 2026-08-27T23:17:26Z |
| |
| google.com -> disjoint | googleapis.com -> disjoint |
| Google NOERROR 12.3 ms ttl=209 74.125.29.100, 74.125.29.101, ... | Google NOERROR 39.5 ms ttl=300 74.125.29.103, 74.125.29.104, ... |
| Cloudflare NOERROR 9.8 ms ttl=15 172.217.208.100, 172.217.208.101, ... | Cloudflare NOERROR 10.1 ms ttl=172 172.217.208.103, 172.217.208.104, ... |
| Quad9 NOERROR 14.7 ms ttl=135 142.250.154.100, 142.250.154.101, ... | Quad9 NOERROR 14.3 ms ttl=165 192.178.183.103, 192.178.183.104, ... |
| |
| summary A: all 100 names | summary A: all 100 names |
| identical 46 46.0% | identical 46 46.0% |
| same-/24 1 1.0% | partial 19 19.0% |
| disjoint 28 28.0% | disjoint 10 10.0% |
| no-A-record 25 25.0% | no-A-record 25 25.0% |
| |
| summary B: the 75 names where at least two resolvers returned an address | summary B: the 75 names where at least two resolvers returned an address |
| identical 46 61.3% | identical 46 61.3% |
| same-/24 1 1.3% | partial 19 25.3% |
| disjoint 28 37.3% | disjoint 10 13.3% |
| | NOT identical 29 38.7% <- the headline: at least one resolver disagreed with another |
| </code> | </code> |
| |
| Three things to take from it, none of which is a claim about the Internet — it is one machine, one instant, three resolvers, one hundred names: | Five things to take from it, none of which is a claim about the Internet — it is one machine, one instant, three resolvers, one hundred names: |
| |
| - **''google.com'' returns three mutually disjoint address sets** from three public resolvers at the same moment. Any downstream claim keyed on "the IP of google.com" inherits whichever one you happened to ask. | - **The number to quote is "not identical", not "disjoint".** 29 of the 75 comparable names (**38.7%**) got answers that were not all the same. **10** were fully disjoint: no two of the three resolvers shared a single address. Quoting the disjoint figure as though it meant "any disagreement" understates it by two thirds; quoting "any disagreement" as though it meant disjoint overstates it by three times. The script prints both and says which to use. |
| | - **And "not identical" is the stable one.** This list was run three times over ninety minutes. ''identical'' came back **46** every time and ''no-A-record'' **25** every time, so ''not identical'' was **29** every time — but the split between ''partial'' and ''disjoint'' moved (28/0, then 21/8, then 19/10) as CDN mappings churned under the measurement. A figure that moves by a factor of three between runs an hour apart is not a figure to build an argument on; the one that did not move is. |
| - **25 of the 100 names have no A record at the apex.** They are CDN and infrastructure zones. A pipeline that resolves and drops failures loses them silently. | - **25 of the 100 names have no A record at the apex.** They are CDN and infrastructure zones. A pipeline that resolves and drops failures loses them silently. |
| - **Summary B is the number to quote**, and the script says so in its own output. Summary A mixes "resolvers disagree" with "there was nothing to disagree about", and reporting 28% instead of 37.3% would understate the first by hiding it behind the second. | - **This is IPv4 only.** The script asks for A records, so "no address" here means "no IPv4 address" — a name reachable only over IPv6, or through an HTTPS/SVCB record, is indistinguishable from one that does not resolve. **26.6%** of the 244 papers mention AAAA at all. If your study is dual-stack, that is a design decision you have to make and report, and this run does not make it for you. |
| | - **Read the verdict logic before trusting any of this.** The first version of this script intersected all three resolvers' answer sets at once, so two resolvers agreeing exactly and a third differing came out as //disjoint// — the opposite of what the word means. It mislabelled **17 of 28** names on the first run, and this page published the wrong figure until a reviewer caught it. The comparison is now pairwise and ''%%--self-test%%'' checks it against ten cases including that one. If you extend the script to more resolvers, keep the comparison pairwise. |
| |
| Run it with ''%%--resolvers%%'' and add the resolver your machine was actually handed by DHCP, from ''/etc/resolv.conf''. That one is usually the most interesting row, and it is the one no public comparison includes. | Run it with ''%%--resolvers%%'' and add the resolver your machine was actually handed by DHCP, from ''/etc/resolv.conf''. That one is usually the most interesting row, and it is the one no public comparison includes. |
| ===== Use in publications ===== | ===== Use in publications ===== |
| |
| Figures from ''scripts/report_dns.mjs'' over the 5,859-paper extraction. Every count is of papers, with its denominator in the table header or the sentence. The tool fold is ''scripts/dns_fold.mjs'' (84 self-tests, 2 unmapped strings out of 976 candidate tuples); it names its off-topic families rather than discarding them silently, because a loose enough candidate sweep for ''hnsd'' and ''resolvectl'' also catches every speech-recognition tool in the corpus. | Figures from ''scripts/report_dns.mjs'' over the 5,859-paper extraction. Every count is of papers, with its denominator in the table header or the sentence. The tool fold is ''scripts/dns_fold.mjs'' (85 self-tests, 2 unmapped strings out of 976 candidate tuples); it names its off-topic families rather than discarding them silently, because a loose enough candidate sweep for ''hnsd'' and ''resolvectl'' also catches every speech-recognition tool in the corpus. |
| |
| ==== What the instrument families look like ==== | ==== What the instrument families look like ==== |
| | resolver-software | 62 | 62 | yes | | | resolver-software | 62 | 62 | yes | |
| | query-tool | 60 | 60 | yes | | | query-tool | 60 | 60 | yes | |
| | custom / home-grown | 35 | 35 | yes | | | custom / home-grown | 34 | 34 | yes | |
| | public-resolver | 29 | 29 | yes | | | public-resolver | 29 | 29 | yes | |
| | dnssec-tool | 3 | 3 | yes | | | dnssec-tool | 3 | 3 | yes | |
| | open-resolver-census | 1 | 1 | yes | | | open-resolver-census | 1 | 1 | yes | |
| | scanner-generic (ZMap, ZGrab, Scapy) | 156 | 38 | no | | | scanner-generic (ZMap, ZGrab, Scapy) | 156 | 38 | no | |
| | vantage-platform (RIPE Atlas) | 77 | 24 | no | | | vantage-platform (RIPE Atlas 75, MobileAtlas 2) | 77 | 24 | no | |
| |
| Rows do not sum to 244: a paper naming BIND and Unbound is in one row twice, and most papers name instruments in several kinds. | Rows do not sum to 244: a paper naming BIND and Unbound is in one row twice, and most papers name instruments in several kinds. |
| ==== The resolver software people actually run ==== | ==== The resolver software people actually run ==== |
| |
| For the papers that stood up a resolver — to test it, to control it, or to serve crafted answers — the ranking over the whole corpus is BIND **29**, Unbound **25**, PowerDNS **11**, dnsmasq **9**, Knot **7**, NSD **5**, Microsoft DNS and Simple DNS Plus **5**, MaraDNS **3**, and single-digit counts for CoreDNS, djbdns, systemd-resolved, gdnsd and hnsd. Seven of those papers use the software as part of an attack rig rather than to measure DNS in the world (a ''dnsmasq'' in a rogue access point); they are listed individually on [[provenance:design:dns]] so the count can be adjusted. | For the papers that stood up a resolver — to test it, to control it, or to serve crafted answers — the ranking over the whole corpus is BIND **29**, Unbound **25**, PowerDNS **11**, dnsmasq **9**, Knot **7**, NSD **5**, Microsoft DNS and Simple DNS Plus **5**, MaraDNS **3**, and single-digit counts for CoreDNS, djbdns, systemd-resolved, gdnsd and hnsd. **Six** of the papers in this family run the software as part of an attack or lab rig rather than to measure DNS in the world, and all six are dnsmasq — a rogue access point, a malicious 4G base station, a spoofing VPN test bed. A seventh lab-rig case sits in the DNSSEC-tool family. All seven are listed individually on [[provenance:design:dns]] so the count can be adjusted. |
| |
| ==== LLMs have arrived, but not as a DNS classifier ==== | ==== LLMs have arrived, but not as a DNS classifier ==== |
| |
| 16 of the 244 match a full-text probe for LLM or a named model, rising 0 / 0 / 3 / 5 / 8 across the five windows. **A probe hit is not a method**, so all sixteen were read. What they actually do: | 16 of the 244 match a **case-sensitive** full-text probe for %%/\b(?:LLM|large language model|GPT-[345]|ChatGPT|Llama|Gemini|Claude)\b/%%, rising 0 / 0 / 3 / 5 / 8 across the five windows. The case sensitivity is load-bearing and the report prints both: matching case-insensitively gives **19** papers and 0/0/3/7/9, because it also fires on ordinary words. **A probe hit is not a method**, so all sixteen case-sensitive hits were read. What they actually do: |
| |
| * **One classifies DNS with an LLM**: a CCS 2024 poster on DoH tunnel detection. | * **One classifies DNS with an LLM**: a CCS 2024 poster on DoH tunnel detection. |
| * **The rest are acknowledgements and related work.** | * **The rest are acknowledgements and related work.** |
| |
| So: an LLM is a defensible labelling aid for the *semantics of hostnames*, where the input is a string and a human could do the job. It is not, on this evidence, current practice for classifying DNS behaviour, and nothing in the 2025–2026 slice supports writing one into a methods section as the standard approach. Both of those slices are provisional years — see the caveat on the year table. | So: an LLM is a defensible labelling aid for the *semantics of hostnames*, where the input is a string and a human could do the job. It is not, on this evidence, current practice for classifying DNS behaviour, and nothing in the 2025–2026 slice supports writing one into a methods section as the standard approach. Note where the signal lives: the 8 is the **2025–2026 window, which is provisional** — see the caveat on the year table — so the growth is real but rests on the thinnest years in the corpus. |
| |
| ===== Methodology and limitations of these figures ===== | ===== Methodology and limitations of these figures ===== |