One Domain, Different Answers: Reading Cloudflare and Google DoH JSON

2026-11-04来源 dev.toEnglish

A colleague says a site loads on one network and fails on another. dig +short on your machine returns one address; a public resolver returns another.

The same happens between two public resolvers: they can return different address sets for one name, and the JSON shows which mechanism produced the difference. The numbers below were measured on 2026-09-30 and 2026-11-03 with the ZeroTool DNS Lookup; each applies to that date and those inputs.

Both endpoints speak the same schema

Cloudflare and Google expose a JSON profile of DNS over HTTPS: a GET with ?name= and ?type=. The binary form is RFC 8484; the JSON profile has no RFC. Cloudflare's documentation says it follows Google's schema, so the field names line up: Status (the RCODE), AD, Answer, Comment for resolver diagnostics. Google adds extended_dns_errors.

The JSON profile works from a page because a cross-origin GET is enough. On 2026-11-03 both endpoints returned access-control-allow-origin: *. The tool sends Accept: application/dns-json, a CORS-safelisted header, so no preflight. A POST with a custom Content-Type triggers one.

ECS explains most of the A-record differences

Google Public DNS normally sends approximate network information to authoritative servers, usually zeroing out the last part of the IPv4 address. edns_client_subnet=0.0.0.0/0 disables that. Cloudflare's FAQ says 1.1.1.1 does not send ECS. A CDN that answers per subnet therefore hands the two resolvers different addresses.

For www.qq.com on 2026-09-30:

edns_client_subnet Google answer
114.114.114.0/24 121.14.77.201, 121.14.77.221
8.8.8.0/24 43.159.109.55
139.130.4.0/24 43.168.224.173

Cloudflare ignored the same parameter and returned 43.159.109.55, matching Google's 8.8.8.0/24 answer in that run. That is one name on one day, not a rule. Comparing resolvers means comparing vantage points: Google's answer moves with the subnet you name, Cloudflare's does not.

do=1, and why AD is unreliable without it

AD (Authenticated Data) reports DNSSEC validation. RFC 4035 §3.2.3 defines when it may be set; RFC 6840 §5.8 says a resolver should set it only when the query carried the DO or AD bit.

Without do=1 the flag varies. On 2026-09-30, 30 queries per type for example.com: A, AAAA and NS returned "AD": false every time, MX returned true every time. With do=1, and from Google either way, every answer had true.

Send do=1 if you intend to read AD. Both resolvers then include RRSIG records in Answer, and the RDATA text is not byte-identical: on 2026-11-03 Google lowercased the covered type mnemonic, Cloudflare uppercased it. Compare sets, not strings.

SERVFAIL is not a diagnosis. EDE is.

A broken chain returns SERVFAIL and AD: false. SERVFAIL alone does not separate "DNSSEC validation failed" from "authoritative servers unreachable". The Extended DNS Error option (RFC 8914) carries a code plus text. For DNSSEC the relevant codes are 1, 2, 5, 6, 7, 8, 9, 10 and 12.

dnssec-failed.org, 2026-11-03:

  • Cloudflare: Status: 2, Comment: ["EDE(9): DNSKEY Missing no SEP matching the DS found for dnssec-failed.org."]
  • Google: Status: 2, a Comment beginning "DNSSEC validation failure", and extended_dns_errors: [{ "info_code": 9, "extra_text": "No DNSKEY matches DS RRs of dnssec-failed.org" }]

Code 9 is "DNSKEY Missing" (RFC 8914 §4.10). Cloudflare puts EDE(n): text strings in an array; Google puts a sentence in Comment and the code in extended_dns_errors. Parse the code, not the sentence. Google also adds Comment: "Response from <IP>." on an uncached answer.

The same TXT record, 425 and 420 characters

TXT values are sequences of character-strings, each at most 255 bytes (RFC 1035 §3.3). A long DKIM key is stored as several strings, and consumers join them with nothing between (RFC 6376 §3.6.2.2).

In the 2026-11-03 query, Cloudflare returned each string quoted and space-separated; Google returned one unquoted string. For cf2024-1._domainkey.zerotool.dev, Cloudflare returned 425 characters with one " " join after the first 255-character string, Google 420. The gap is four quote characters plus the joining space.

Google's documentation shows quoted, segmented TXT examples, so treat quoting as presentation, not a contract. Strip quotes when present, and never insert a separator between segments.

A public resolver is not the system resolver

dig asks the resolver configured in the OS, or the server you name. A DoH call asks Cloudflare or Google: different cache, different network position.

  • /etc/hosts and VPN overrides: application lookups go through them; dig and nslookup normally do not read the hosts file, and a public DoH resolver sees neither.
  • Split-horizon: internal names return internal addresses inside the network and NXDOMAIN outside.
  • Search domains: the OS may append a suffix to a bare hostname; the JSON endpoints need the full name.
  • Browser Secure DNS changes which resolver answers navigation, not the resolver the DoH request asks.
  • Bootstrap: the page must resolve cloudflare-dns.com or dns.google first, so a filter that blocks them stops the lookup.

Six misdiagnoses

  1. "Still propagating." Usually caching. Check the TTL, then ask a second resolver; old on one and new on the other means cache expiry.
  2. "DNS is broken on one machine." Check for a cached NXDOMAIN. RFC 2308 §5 lets a resolver cache the negative answer for the SOA minimum, so a fixed zone keeps failing until the timer expires.
  3. "The CNAME is missing." The resolver followed it and returned A/AAAA, or the apex uses CNAME flattening. Read the name field of each answer.
  4. "The TXT record is truncated." It is multiple 255-byte strings. Join them without a separator.
  5. "The internal name does not resolve." A split-horizon zone answers inside the network and returns NXDOMAIN outside.
  6. "DNSSEC is fine because the site loads." A validating resolver can fail where a non-validating one succeeds.

NXDOMAIN and NODATA differ. Status: 3 means the name does not exist; NOERROR with zero answers means the name exists without that type.

Reproduce it

const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent('zerotool.dev')}&type=MX&do=1&cd=0`; const res = await fetch(url, { headers: { accept: 'application/dns-json' } }); const json = await res.json(); console.log(json.Answer); // [{ name, type, TTL, data }, ...] 

Swap the host to https://dns.google/resolve?name=…&type=…&do=1&cd=0 and add edns_client_subnet to see the ECS effect. Add AbortSignal.timeout(5000) and a try/catch; the tool cancels each query after 5 seconds.

One HTTP detail matters: in the same 2026-11-03 query, Google's response carried cache-control: private, max-age=300 and Cloudflare's carried none. A repeat query can be served from a browser or intermediary cache for up to five minutes, which looks like a resolver that ignored your change.

The longer write-up, including record types, CAA, PTR and SRV, is in the DoH lookup guide.

原文:https://www.a8q9.cn/en/article/one-domain-different-answers-reading-cloudflare-and-google-doh-json-ea7465.html

相关文章