VisualEther Session — Chrome DNS-over-HTTPS (DoH) Analysis

PCAP: chrome-doh-example-with-secrets.pcapng (41 packets, IPv6, embedded TLS keylog DSB)  |  Session date: 2026-05-03  |  Tools: VisualEther MCP server, tshark

Turn 1 — Generate FXT templates

User
Please generate explore and sessions fxt files for @chrome-doh-example-with-secrets.pcapng
Claude

Ran analyze_capture. Detected 5 protocols: DNS, TLS, HTTP/2, TCP, IPv6. The capture is Chrome resolving names via Cloudflare's DoH endpoint (chrome.cloudflare-dns.com) over IPv6, with TLS secrets embedded in the pcapng DSB block so tshark auto-decrypts the HTTP/2 + inner DNS payload.

Best matching template: http2/explore.fxt.xml (60% coverage). Used the bundled HTTP/2 samples as a starting point and customized for IPv6 (added tcpv6-message / udpv6-message duplicates of every tcp-message / udp-message template) plus inner-DNS-over-TCP templates so the decrypted DoH payload renders.

FileTemplates (incl. v6)Session typesOutcome
explore.fxt.xml~50025 / 41 frames matched
sessions.fxt.xml~602 (http2-session, dns-session)1 dns success::resolved, 1 http2 success::closed, 4 http2 incomplete::no-close

Turn 2 — Inner DNS missing in explore mode

User
The explore mode does not show DNS messages. I see them in dns-session.
Claude

Diagnosed: explore uses first-match-wins. Inner-DNS templates were placed after the HTTP/2 DATA template, so HTTP/2 DATA absorbed the frame before DNS got a chance. Sessions mode uses multi-match (per-session-type), so DNS still appeared there.

Fix: Moved all inner-DNS templates (dns.qry.type, dns.a, dns.aaaa, dns.flags.response) above the HTTP/2 DATA & control frames in explore.fxt.xml. Same matched-frame count (25/41), but now DoH inner DNS Query/Response render inline with the HTTP/2 flow.

Turn 3 — Generate hosts.txt

User
Please generate the hosts.txt file
Claude

Ran extract_endpoints. Two IPv6 endpoints:

2603:3005:1402:a740:c985:8b1c:f4ed:fc26    Chrome Client
2606:4700::6812:dad                        chrome.cloudflare-dns.com

Saved as hosts.txt (tab-separated IPv6<TAB>Name). Re-ran both explore and extract_sessions with hosts_file=hosts.txt so friendly names appear in the diagrams.

Turn 4 — Add lightbulb remarks for DoH

User
Please add lightbulb remarks to explain the DoH messages in both fxt files.
Claude

Added <remark> elements (rendered as 💡 callouts) focused on why DoH behaves the way it does, not just what each message is.

explore.fxt.xml remarks added on:

sessions.fxt.xml remarks added on:

Turn 5 — dns-session::0x0000 explained

User
In dns session with key: dns-session::0x0000, please explain why cloudflare-dns is also sending DNS Queries to the browser.
Claude

Two distinct quirks:

Why dns.id = 0x0000 — RFC 8484 §4.1 recommends DoH clients set the DNS transaction ID to 0:

In order to maximize HTTP cache friendliness, DoH clients … SHOULD use a DNS ID of 0 in every DNS request.

Correlation is no longer needed at the DNS layer — the HTTP/2 stream pairs request/response. Zeroing the ID lets shared HTTP caches deduplicate identical ?dns=… GETs across clients. Chrome follows this, so every DoH query+response collapses into a single dns-session bucket keyed by 0x0000.

Why the resolver appears to send queries back — it isn't. A DNS response echoes the original Question section verbatim before the Answer. Tshark dissects that echoed Question with the same dns.qry.type / dns.qry.name fields it uses for actual queries. In multi-match mode the response packet matches both the session-start template (Question echo → 🔍 DNS Query) and the session-stop template (dns.a → 📋 DNS Response), and since the server is the source the query-shaped render points server→client.

Turn 6 — Update remarks to clarify

User
This is fine. Let us update the lightbulb remark to clarify this.
Claude

Updated the dns-session start remarks for A and AAAA queries in sessions.fxt.xml to call out:

  1. dns.id=0x0000 per RFC 8484 §4.1 (collapses all queries into one bucket)
  2. Server→client arrows are the Question section echoed inside the Response — multi-match fires the start template alongside the stop template.

Turn 7 — Mirror inner DNS into http2-session

User
Should we display the DNS interactions in the http2-session as well?
Claude

Added duplicates of the inner-DNS templates tagged session-type="http2-session" (no session-start/session-stop, so they decorate existing sessions instead of creating new ones).

Turn 8 — DNS not appearing in http2-session viewer

User
I don't see the DNS messages in http://127.0.0.1:56043/.../http2-session/success/00002_viewer.html?
Claude

The session router needs the session key field present on the matched message to bucket it. The new inner-DNS http2-session templates didn't include http2.streamid as a <param>. Added http2.streamid to all four (Query A, Query AAAA, Response A, Response AAAA — both tcp-message and tcpv6-message variants).

Still not enough — see next turn.

Turn 9 — Specify multi-match at root

User
We have to specify the session key.
Claude

Investigated fxt.rs and FXT_SYNTAX_REFERENCE.md. Per-template session keys are not supported ("no inline session-key"). The real missing piece was the root attribute multi-match="true" — without it, each packet matches only the first template, so a response packet was routed only to dns-session and never to http2-session.

Added <FXT multi-match="true"> to sessions.fxt.xml. Outcome counts changed dramatically:

BeforeAfter
dns-session success::resolved13
http2-session success::2xx04
http2-session success::closed11
http2-session incomplete::no-close40

Inner DNS Query/Response now renders inline within each http2-session timeline alongside the POST/200 OK pair, and the four DoH requests now correctly close as success::2xx instead of being orphaned as incomplete.

Turn 10 — DoH response times

User
How much is the response time for DoH requests?
Claude

Extracted http2.time (Wireshark's request→response delta on the same stream) via tshark filtered to http2.headers.status==200:

StreamResponse time
123.2 ms
533.2 ms
333.4 ms
738.4 ms

Mean ≈ 32 ms, range 23–38 ms. These measure only the round-trip + server processing; the TCP/TLS handshake is amortized once across all four queries thanks to HTTP/2 multiplexing.

Artifacts & findings

FilePurpose
chrome-doh-example-with-secrets.pcapngSource capture (DoH over IPv6, TLS keys in DSB block)
explore.fxt.xmlCombined-flow templates (HTTP/2 + inner DNS + TLS + TCP, IPv4 + IPv6)
sessions.fxt.xmlSession tracking (http2-session keyed by http2.streamid, dns-session keyed by dns.id); multi-match="true"
hosts.txtIPv6 → friendly name (Chrome Client, chrome.cloudflare-dns.com)
out/chrome-doh-example-with-secrets/Generated viewers (explore HTML + per-session HTML)

Key findings