{"schemaVersion":1,"id":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy","title":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","description":"A comprehensive guide to the DNS privacy stack, DNS-over-HTTPS, DNS-over-TLS, DNSSEC validation, Encrypted Client Hello (ECH), Oblivious DoH, with honest analysis of what each protocol actually protects against and a practical implementation guide.","language":"en","datePublished":"2026-01-08","dateModified":null,"contentFormat":"text/markdown","content":"Networking\n\n# DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis\n\n2026-01-087 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/dns-over-https-encrypted-client-hello-privacy>)\n\n## The Metadata Leak Problem\n\nWhen you visit ` https://secret-stuff.example.com `, your HTTPS traffic is encrypted. But the connection process leaks metadata at multiple points:\n\n```\nStep 1: DNS Resolution\n Client → ISP DNS: \"What's the IP of secret-stuff.example.com?\"\n ISP DNS → Client: \"It's 203.0.113.42\"\n [LEAKED: The domain you're visiting]\n\nStep 2: TCP Connection\n Client → 203.0.113.42: SYN\n [LEAKED: The IP you're connecting to]\n\nStep 3: TLS Handshake\n Client → Server: ClientHello { SNI: \"secret-stuff.example.com\" }\n [LEAKED: The domain again, in cleartext]\n\nStep 4: Encrypted Application Data\n [PROTECTED: The actual HTTP request and response]\n```\n\nThree out of four steps leak the domain you're visiting. Encrypted DNS only fixes step 1. ECH fixes step 3. Step 2 is fundamentally hard (requires a proxy, VPN, or Tor).\n\n## DNS-over-HTTPS (DoH): The Basics\n\nDoH wraps DNS queries in standard HTTPS requests, using ` application/dns-message ` content type:\n\n```bash\n# DoH query using curl (wire format)\ncurl -s -H 'Accept: application/dns-message' \\\n 'https://cloudflare-dns.com/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB' \\\n | python3 -c \"import sys; data=sys.stdin.buffer.read(); print(data.hex())\"\n\n# DoH query using JSON format (Cloudflare/Google extension)\ncurl -s -H 'Accept: application/dns-json' \\\n 'https://cloudflare-dns.com/dns-query?name=example.com&type=A' | jq .\n\n# Response:\n{\n \"Status\": 0,\n \"TC\": false,\n \"RD\": true,\n \"RA\": true,\n \"AD\": true,\n \"CD\": false,\n \"Question\": [{\"name\": \"example.com\", \"type\": 1}],\n \"Answer\": [\n {\"name\": \"example.com\", \"type\": 1, \"TTL\": 3600,\n \"data\": \"93.184.216.34\"}\n ]\n}\n```\n\nSetting up a local DoH resolver with [` dnscrypt-proxy `](<https://github.com/DNSCrypt/dnscrypt-proxy>):\n\n```toml\n# /etc/dnscrypt-proxy/dnscrypt-proxy.toml\nlisten_addresses = ['127.0.0.1:53', '[::1]:53']\nserver_names = ['cloudflare', 'cloudflare-ipv6']\ndoh_servers = true\nrequire_dnssec = true\nrequire_nofilter = true\nrequire_nolog = true\n\n# Connection settings\ntimeout = 5000\nkeepalive = 30\ncert_refresh_delay = 240\n\n# Caching\ncache = true\ncache_size = 4096\ncache_min_ttl = 2400\ncache_max_ttl = 86400\ncache_neg_min_ttl = 60\ncache_neg_max_ttl = 600\n\n# Oblivious DoH relays\n[anonymized_dns]\nroutes = [\n { server_name='cloudflare', via=['anon-relay-1', 'anon-relay-2'] }\n]\n```\n\n## DNS-over-TLS (DoT): The Enterprise Alternative\n\nDoT uses TLS directly on port 853, without the HTTP layer:\n\n```python\nimport ssl\nimport socket\nimport struct\n\ndef dot_query(domain, resolver='1.1.1.1', port=853):\n # Build DNS query\n query = build_dns_query(domain, qtype='A')\n\n # Wrap with 2-byte length prefix (TCP DNS framing)\n framed = struct.pack('!H', len(query)) + query\n\n # TLS connection\n ctx = ssl.create_default_context()\n ctx.check_hostname = True\n ctx.verify_mode = ssl.CERT_REQUIRED\n\n with socket.create_connection((resolver, port)) as sock:\n with ctx.wrap_socket(sock, server_hostname='cloudflare-dns.com') as tls:\n tls.sendall(framed)\n\n # Read response length\n length_data = tls.recv(2)\n length = struct.unpack('!H', length_data)[0]\n\n # Read response\n response = tls.recv(length)\n return parse_dns_response(response)\n```\n\nDoT vs DoH trade-offs:\n\n```\n DoH (port 443) DoT (port 853)\nIndistinguishable Yes (looks like No (dedicated port)\nfrom HTTPS traffic normal browsing)\nBlockable by FW Hard (blocks all Easy (block port 853)\n HTTPS)\nEnterprise control Hard to manage Easy to detect/redirect\nPerformance HTTP/2 multiplexing Dedicated TLS, less\n with other traffic overhead per query\nPadding support Via HTTP/2 frames Via EDNS0 padding\n```\n\n## DNSSEC: Integrity, Not Confidentiality\n\nDNSSEC adds cryptographic signatures to DNS records. It does NOT encrypt anything, it only proves the response wasn't modified in transit.\n\n```bash\n# Check DNSSEC validation with dig\ndig +dnssec example.com\n\n# Look for:\n# - 'ad' flag in header (Authenticated Data)\n# - RRSIG records (signatures)\n# - DNSKEY records (zone signing keys)\n\n# Verify the chain of trust manually\ndig +trace +dnssec example.com\n\n# Chain: root (.) → .com → example.com\n# Each level signs the delegation to the next\n# The root zone's key is the trust anchor (built into resolvers)\n```\n\nDNSSEC validation on your local resolver:\n\n```bash\n# Using unbound as a validating resolver\n# /etc/unbound/unbound.conf\nserver:\n interface: 127.0.0.1\n port: 5353\n do-not-query-localhost: no\n\n # DNSSEC validation\n auto-trust-anchor-file: \"/var/lib/unbound/root.key\"\n val-clean-additional: yes\n val-permissive-mode: no # Strict: fail on DNSSEC errors\n harden-dnssec-stripped: yes\n\n # Forward to DoH resolver\n forward-zone:\n name: \".\"\n forward-tls-upstream: yes\n forward-addr: 1.1.1.1@853#cloudflare-dns.com\n forward-addr: 1.0.0.1@853#cloudflare-dns.com\n```\n\n## Encrypted Client Hello (ECH): The Missing Piece\n\nECH encrypts the entire ClientHello (including SNI) using a public key published in the DNS record of the server. Here's how it works:\n\n```\nWithout ECH:\n Client → Server: ClientHello {\n SNI: \"secret-stuff.example.com\" ← VISIBLE to on-path observer\n supported_ciphers: [...]\n supported_groups: [...]\n }\n\nWith ECH:\n 1. Client fetches HTTPS DNS record for secret-stuff.example.com\n → Contains ECH config with public key\n\n 2. Client → Server: ClientHello {\n SNI: \"cloudflare-ech.com\" ← \"Outer\" SNI (public-facing domain)\n encrypted_client_hello: {\n // Encrypted with server's ECH public key:\n inner_SNI: \"secret-stuff.example.com\"\n supported_ciphers: [...]\n }\n }\n\n Observer sees: connection to \"cloudflare-ech.com\"\n Server decrypts: actual destination is \"secret-stuff.example.com\"\n```\n\nThe DNS record looks like:\n\n```bash\n# Fetch ECH configuration via DNS HTTPS record\ndig +short HTTPS cloudflare.com\n# 1 . alpn=\"h3,h2\" ipv4hint=104.16.132.229 ech=AEX+...\n\n# The ech= parameter contains:\n# - KEM (Key Encapsulation Mechanism) ID\n# - Public key for encrypting the inner ClientHello\n# - Symmetric cipher suite\n# - Maximum name length\n```\n\nTesting ECH in your browser:\n\n```javascript\n// Check if ECH is active on a connection\n// In Firefox: about:networking#dns\n// Look for \"hasECH: true\" in the connection info\n\n// Programmatic check (from web page):\n// Currently no standard API, but you can test against\n// defo.ie/ech-check.php or similar test servers\n\n// Firefox about:config settings:\n// network.dns.echconfig.enabled = true\n// network.dns.use_https_rr_as_altsvc = true\n```\n\n## Oblivious DoH (ODoH): Trust Nobody\n\nStandard DoH has a trust problem: you're replacing your ISP as the entity that sees your DNS queries with your DoH resolver ([Cloudflare](<https://cloudflare.com/>), Google, etc.). ODoH fixes this:\n\n```\nStandard DoH:\n Client →→ Resolver\n Resolver sees: client IP + query\n Problem: resolver knows everything\n\nOblivious DoH:\n Client → Relay → Resolver\n Relay sees: client IP, but query is encrypted to resolver\n Resolver sees: query content, but not client IP (sees relay IP)\n Nobody sees both!\n```\n\nThe protocol works like this:\n\n```\n1. Client fetches resolver's ODoH target key (via DNS)\n2. Client encrypts DNS query with resolver's public key\n3. Client sends encrypted query to relay (standard HTTPS POST)\n4. Relay forwards encrypted blob to resolver (can't read it)\n5. Resolver decrypts, resolves, encrypts response\n6. Response flows back: resolver → relay → client\n```\n\n```bash\n# Configure ODoH with dnscrypt-proxy\n[anonymized_dns]\nroutes = [\n { server_name = 'cloudflare', via = ['odoh-relay-crypto-sx'] },\n { server_name = 'cloudflare', via = ['odoh-relay-amsterdam'] }\n]\n\n# Performance comparison:\n# Standard DNS (UDP): ~5 ms\n# DoH (direct): ~25 ms\n# DoT (direct): ~20 ms\n# ODoH (via relay): ~35-40 ms\n# Double-relay ODoH: ~55-70 ms\n```\n\n## The Complete Privacy Stack\n\nHere is a recommended complete privacy stack configuration:\n\n```bash\n# Stack:\n# 1. dnscrypt-proxy (local port 53) → ODoH to Cloudflare via relay\n# 2. DNSSEC validation enabled\n# 3. Firefox with ECH enabled\n# 4. HTTPS-only mode\n\n# /etc/dnscrypt-proxy/dnscrypt-proxy.toml\nlisten_addresses = ['127.0.0.1:53']\nserver_names = ['cloudflare']\ndoh_servers = true\nrequire_dnssec = true\n\n[anonymized_dns]\nroutes = [\n { server_name='cloudflare', via=['anon-relay-nl'] }\n]\n\n# Firefox about:config\nnetwork.dns.echconfig.enabled = true\nnetwork.dns.use_https_rr_as_altsvc = true\nnetwork.trr.mode = 3 # DoH only, no fallback to system DNS\nnetwork.trr.uri = https://127.0.0.1:3000/dns-query # Local proxy\ndom.security.https_only_mode = true\n```\n\n## Honest Privacy Analysis\n\nHere is an honest assessment of what each layer actually provides:\n\n| Threat | Plain DNS | DoH/DoT | + DNSSEC | + ECH | + ODoH |\n| --- | --- | --- | --- | --- | --- |\n| ISP sees domains | Yes | **No** | No | No | No |\n| Café WiFi sniffing | Yes | **No** | No | No | No |\n| DNS spoofing | Yes | No | **No** | No | No |\n| Resolver sees queries | Yes | Yes | Yes | Yes | **No** |\n| SNI leaks domain | Yes | Yes | Yes | **No** | No |\n| Destination IP visible | Yes | Yes | Yes | Yes | Yes |\n\nThat last row is the elephant in the room. Even with the full stack, anyone who can see your network traffic knows the IP address you're connecting to. For shared hosting (like Cloudflare), the IP maps to millions of sites, so it's not very revealing. For sites on dedicated IPs, the destination IP alone identifies the site.\n\nFor the last remaining leak, you need a proxy, VPN, or Tor. There's no DNS-level solution for it.\n\nThe privacy stack described above is about raising the floor, making passive surveillance significantly harder without dramatically impacting performance. It's not about achieving perfect anonymity. If you need that, you need Tor. But for everyday privacy against ISP monitoring, corporate network inspection, and café WiFi sniffing, DoH + ECH + ODoH gets you about 90% of the way there with under 50ms of added latency.\n\n## Frequently asked questions\n\n### What's the difference between DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT)?\n\nDoH encapsulates DNS queries in HTTPS requests on port 443, making them indistinguishable from regular HTTPS traffic. DoT uses a dedicated port (853) with TLS, which makes it easy for network administrators to identify and block. DoH is harder to block but also harder to manage in enterprise environments.\n\n### Does Encrypted Client Hello (ECH) replace the need for a VPN?\n\nECH encrypts the SNI field in TLS handshakes, preventing on-path observers from seeing which domain you're connecting to. Combined with DoH, it covers the two main metadata leaks in HTTPS. However, a VPN also hides your destination IP, which ECH doesn't. For comprehensive privacy, you still need either a VPN or Tor.\n\n### What is Oblivious DoH and when should I use it?\n\nOblivious DoH (ODoH) adds a relay between the client and resolver, so the resolver never sees the client's IP and the relay never sees the DNS query content. Use it when you need DNS privacy from both your network path AND your DNS resolver. The latency overhead is typically 5-15ms per query.\n\n## Tips\n\n1. **Tip:** DoH protects DNS queries from your ISP, but your resolver (Cloudflare, Google) still sees everything. If you need real DNS privacy, run ` Oblivious DoH ` through a relay, neither the relay nor the resolver can link your IP to your queries.\n2. **Tip:** Encrypted Client Hello (ECH) is the missing piece that DoH alone cannot solve. Without ECH, the SNI in TLS ClientHello still leaks which domain you're connecting to, even if your DNS queries are encrypted. Check your browser's ` about:config ` for ` network.dns.echconfig.enabled `.\n3. **Tip:** DNSSEC validates that DNS records haven't been tampered with, but it does NOT encrypt anything, your queries and responses are still visible to on-path observers. You need both DNSSEC (integrity) and DoH/DoT (confidentiality) for complete protection.\n\n- dns\n- privacy\n- doh\n- dot\n- dnssec\n- ech\n- encrypted-client-hello\n- networking\n- security\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#title"},{"level":2,"text":"The Metadata Leak Problem","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#the-metadata-leak-problem"},{"level":2,"text":"DNS-over-HTTPS (DoH): The Basics","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dns-over-https-doh-the-basics"},{"level":2,"text":"DNS-over-TLS (DoT): The Enterprise Alternative","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dns-over-tls-dot-the-enterprise-alternative"},{"level":2,"text":"DNSSEC: Integrity, Not Confidentiality","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dnssec-integrity-not-confidentiality"},{"level":2,"text":"Encrypted Client Hello (ECH): The Missing Piece","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#encrypted-client-hello-ech-the-missing-piece"},{"level":2,"text":"Oblivious DoH (ODoH): Trust Nobody","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#oblivious-doh-odoh-trust-nobody"},{"level":2,"text":"The Complete Privacy Stack","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#the-complete-privacy-stack"},{"level":2,"text":"Honest Privacy Analysis","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#honest-privacy-analysis"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#questions"},{"level":3,"text":"What's the difference between DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT)?","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-1"},{"level":3,"text":"Does Encrypted Client Hello (ECH) replace the need for a VPN?","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-2"},{"level":3,"text":"What is Oblivious DoH and when should I use it?","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#article-tips"}],"sections":[{"heading":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","level":1,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy","content":"Networking"},{"heading":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","level":1,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#title","content":"2026-01-087 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/dns-over-https-encrypted-client-hello-privacy>)"},{"heading":"The Metadata Leak Problem","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#the-metadata-leak-problem","content":"When you visit ` https://secret-stuff.example.com `, your HTTPS traffic is encrypted. But the connection process leaks metadata at multiple points:\n\n```\nStep 1: DNS Resolution\n Client → ISP DNS: \"What's the IP of secret-stuff.example.com?\"\n ISP DNS → Client: \"It's 203.0.113.42\"\n [LEAKED: The domain you're visiting]\n\nStep 2: TCP Connection\n Client → 203.0.113.42: SYN\n [LEAKED: The IP you're connecting to]\n\nStep 3: TLS Handshake\n Client → Server: ClientHello { SNI: \"secret-stuff.example.com\" }\n [LEAKED: The domain again, in cleartext]\n\nStep 4: Encrypted Application Data\n [PROTECTED: The actual HTTP request and response]\n```\n\nThree out of four steps leak the domain you're visiting. Encrypted DNS only fixes step 1. ECH fixes step 3. Step 2 is fundamentally hard (requires a proxy, VPN, or Tor)."},{"heading":"DNS-over-HTTPS (DoH): The Basics","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dns-over-https-doh-the-basics","content":"DoH wraps DNS queries in standard HTTPS requests, using ` application/dns-message ` content type:\n\n```bash\n# DoH query using curl (wire format)\ncurl -s -H 'Accept: application/dns-message' \\\n 'https://cloudflare-dns.com/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB' \\\n | python3 -c \"import sys; data=sys.stdin.buffer.read(); print(data.hex())\"\n\n# DoH query using JSON format (Cloudflare/Google extension)\ncurl -s -H 'Accept: application/dns-json' \\\n 'https://cloudflare-dns.com/dns-query?name=example.com&type=A' | jq .\n\n# Response:\n{\n \"Status\": 0,\n \"TC\": false,\n \"RD\": true,\n \"RA\": true,\n \"AD\": true,\n \"CD\": false,\n \"Question\": [{\"name\": \"example.com\", \"type\": 1}],\n \"Answer\": [\n {\"name\": \"example.com\", \"type\": 1, \"TTL\": 3600,\n \"data\": \"93.184.216.34\"}\n ]\n}\n```\n\nSetting up a local DoH resolver with [` dnscrypt-proxy `](<https://github.com/DNSCrypt/dnscrypt-proxy>):\n\n```toml\n# /etc/dnscrypt-proxy/dnscrypt-proxy.toml\nlisten_addresses = ['127.0.0.1:53', '[::1]:53']\nserver_names = ['cloudflare', 'cloudflare-ipv6']\ndoh_servers = true\nrequire_dnssec = true\nrequire_nofilter = true\nrequire_nolog = true\n\n# Connection settings\ntimeout = 5000\nkeepalive = 30\ncert_refresh_delay = 240\n\n# Caching\ncache = true\ncache_size = 4096\ncache_min_ttl = 2400\ncache_max_ttl = 86400\ncache_neg_min_ttl = 60\ncache_neg_max_ttl = 600\n\n# Oblivious DoH relays\n[anonymized_dns]\nroutes = [\n { server_name='cloudflare', via=['anon-relay-1', 'anon-relay-2'] }\n]\n```"},{"heading":"DNS-over-TLS (DoT): The Enterprise Alternative","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dns-over-tls-dot-the-enterprise-alternative","content":"DoT uses TLS directly on port 853, without the HTTP layer:\n\n```python\nimport ssl\nimport socket\nimport struct\n\ndef dot_query(domain, resolver='1.1.1.1', port=853):\n # Build DNS query\n query = build_dns_query(domain, qtype='A')\n\n # Wrap with 2-byte length prefix (TCP DNS framing)\n framed = struct.pack('!H', len(query)) + query\n\n # TLS connection\n ctx = ssl.create_default_context()\n ctx.check_hostname = True\n ctx.verify_mode = ssl.CERT_REQUIRED\n\n with socket.create_connection((resolver, port)) as sock:\n with ctx.wrap_socket(sock, server_hostname='cloudflare-dns.com') as tls:\n tls.sendall(framed)\n\n # Read response length\n length_data = tls.recv(2)\n length = struct.unpack('!H', length_data)[0]\n\n # Read response\n response = tls.recv(length)\n return parse_dns_response(response)\n```\n\nDoT vs DoH trade-offs:\n\n```\n DoH (port 443) DoT (port 853)\nIndistinguishable Yes (looks like No (dedicated port)\nfrom HTTPS traffic normal browsing)\nBlockable by FW Hard (blocks all Easy (block port 853)\n HTTPS)\nEnterprise control Hard to manage Easy to detect/redirect\nPerformance HTTP/2 multiplexing Dedicated TLS, less\n with other traffic overhead per query\nPadding support Via HTTP/2 frames Via EDNS0 padding\n```"},{"heading":"DNSSEC: Integrity, Not Confidentiality","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#dnssec-integrity-not-confidentiality","content":"DNSSEC adds cryptographic signatures to DNS records. It does NOT encrypt anything, it only proves the response wasn't modified in transit.\n\n```bash\n# Check DNSSEC validation with dig\ndig +dnssec example.com\n\n# Look for:\n# - 'ad' flag in header (Authenticated Data)\n# - RRSIG records (signatures)\n# - DNSKEY records (zone signing keys)\n\n# Verify the chain of trust manually\ndig +trace +dnssec example.com\n\n# Chain: root (.) → .com → example.com\n# Each level signs the delegation to the next\n# The root zone's key is the trust anchor (built into resolvers)\n```\n\nDNSSEC validation on your local resolver:\n\n```bash\n# Using unbound as a validating resolver\n# /etc/unbound/unbound.conf\nserver:\n interface: 127.0.0.1\n port: 5353\n do-not-query-localhost: no\n\n # DNSSEC validation\n auto-trust-anchor-file: \"/var/lib/unbound/root.key\"\n val-clean-additional: yes\n val-permissive-mode: no # Strict: fail on DNSSEC errors\n harden-dnssec-stripped: yes\n\n # Forward to DoH resolver\n forward-zone:\n name: \".\"\n forward-tls-upstream: yes\n forward-addr: 1.1.1.1@853#cloudflare-dns.com\n forward-addr: 1.0.0.1@853#cloudflare-dns.com\n```"},{"heading":"Encrypted Client Hello (ECH): The Missing Piece","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#encrypted-client-hello-ech-the-missing-piece","content":"ECH encrypts the entire ClientHello (including SNI) using a public key published in the DNS record of the server. Here's how it works:\n\n```\nWithout ECH:\n Client → Server: ClientHello {\n SNI: \"secret-stuff.example.com\" ← VISIBLE to on-path observer\n supported_ciphers: [...]\n supported_groups: [...]\n }\n\nWith ECH:\n 1. Client fetches HTTPS DNS record for secret-stuff.example.com\n → Contains ECH config with public key\n\n 2. Client → Server: ClientHello {\n SNI: \"cloudflare-ech.com\" ← \"Outer\" SNI (public-facing domain)\n encrypted_client_hello: {\n // Encrypted with server's ECH public key:\n inner_SNI: \"secret-stuff.example.com\"\n supported_ciphers: [...]\n }\n }\n\n Observer sees: connection to \"cloudflare-ech.com\"\n Server decrypts: actual destination is \"secret-stuff.example.com\"\n```\n\nThe DNS record looks like:\n\n```bash\n# Fetch ECH configuration via DNS HTTPS record\ndig +short HTTPS cloudflare.com\n# 1 . alpn=\"h3,h2\" ipv4hint=104.16.132.229 ech=AEX+...\n\n# The ech= parameter contains:\n# - KEM (Key Encapsulation Mechanism) ID\n# - Public key for encrypting the inner ClientHello\n# - Symmetric cipher suite\n# - Maximum name length\n```\n\nTesting ECH in your browser:\n\n```javascript\n// Check if ECH is active on a connection\n// In Firefox: about:networking#dns\n// Look for \"hasECH: true\" in the connection info\n\n// Programmatic check (from web page):\n// Currently no standard API, but you can test against\n// defo.ie/ech-check.php or similar test servers\n\n// Firefox about:config settings:\n// network.dns.echconfig.enabled = true\n// network.dns.use_https_rr_as_altsvc = true\n```"},{"heading":"Oblivious DoH (ODoH): Trust Nobody","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#oblivious-doh-odoh-trust-nobody","content":"Standard DoH has a trust problem: you're replacing your ISP as the entity that sees your DNS queries with your DoH resolver ([Cloudflare](<https://cloudflare.com/>), Google, etc.). ODoH fixes this:\n\n```\nStandard DoH:\n Client →→ Resolver\n Resolver sees: client IP + query\n Problem: resolver knows everything\n\nOblivious DoH:\n Client → Relay → Resolver\n Relay sees: client IP, but query is encrypted to resolver\n Resolver sees: query content, but not client IP (sees relay IP)\n Nobody sees both!\n```\n\nThe protocol works like this:\n\n```\n1. Client fetches resolver's ODoH target key (via DNS)\n2. Client encrypts DNS query with resolver's public key\n3. Client sends encrypted query to relay (standard HTTPS POST)\n4. Relay forwards encrypted blob to resolver (can't read it)\n5. Resolver decrypts, resolves, encrypts response\n6. Response flows back: resolver → relay → client\n```\n\n```bash\n# Configure ODoH with dnscrypt-proxy\n[anonymized_dns]\nroutes = [\n { server_name = 'cloudflare', via = ['odoh-relay-crypto-sx'] },\n { server_name = 'cloudflare', via = ['odoh-relay-amsterdam'] }\n]\n\n# Performance comparison:\n# Standard DNS (UDP): ~5 ms\n# DoH (direct): ~25 ms\n# DoT (direct): ~20 ms\n# ODoH (via relay): ~35-40 ms\n# Double-relay ODoH: ~55-70 ms\n```"},{"heading":"The Complete Privacy Stack","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#the-complete-privacy-stack","content":"Here is a recommended complete privacy stack configuration:\n\n```bash\n# Stack:\n# 1. dnscrypt-proxy (local port 53) → ODoH to Cloudflare via relay\n# 2. DNSSEC validation enabled\n# 3. Firefox with ECH enabled\n# 4. HTTPS-only mode\n\n# /etc/dnscrypt-proxy/dnscrypt-proxy.toml\nlisten_addresses = ['127.0.0.1:53']\nserver_names = ['cloudflare']\ndoh_servers = true\nrequire_dnssec = true\n\n[anonymized_dns]\nroutes = [\n { server_name='cloudflare', via=['anon-relay-nl'] }\n]\n\n# Firefox about:config\nnetwork.dns.echconfig.enabled = true\nnetwork.dns.use_https_rr_as_altsvc = true\nnetwork.trr.mode = 3 # DoH only, no fallback to system DNS\nnetwork.trr.uri = https://127.0.0.1:3000/dns-query # Local proxy\ndom.security.https_only_mode = true\n```"},{"heading":"Honest Privacy Analysis","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#honest-privacy-analysis","content":"Here is an honest assessment of what each layer actually provides:\n\n| Threat | Plain DNS | DoH/DoT | + DNSSEC | + ECH | + ODoH |\n| --- | --- | --- | --- | --- | --- |\n| ISP sees domains | Yes | **No** | No | No | No |\n| Café WiFi sniffing | Yes | **No** | No | No | No |\n| DNS spoofing | Yes | No | **No** | No | No |\n| Resolver sees queries | Yes | Yes | Yes | Yes | **No** |\n| SNI leaks domain | Yes | Yes | Yes | **No** | No |\n| Destination IP visible | Yes | Yes | Yes | Yes | Yes |\n\nThat last row is the elephant in the room. Even with the full stack, anyone who can see your network traffic knows the IP address you're connecting to. For shared hosting (like Cloudflare), the IP maps to millions of sites, so it's not very revealing. For sites on dedicated IPs, the destination IP alone identifies the site.\n\nFor the last remaining leak, you need a proxy, VPN, or Tor. There's no DNS-level solution for it.\n\nThe privacy stack described above is about raising the floor, making passive surveillance significantly harder without dramatically impacting performance. It's not about achieving perfect anonymity. If you need that, you need Tor. But for everyday privacy against ISP monitoring, corporate network inspection, and café WiFi sniffing, DoH + ECH + ODoH gets you about 90% of the way there with under 50ms of added latency."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#questions","content":""},{"heading":"What's the difference between DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT)?","level":3,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-1","content":"DoH encapsulates DNS queries in HTTPS requests on port 443, making them indistinguishable from regular HTTPS traffic. DoT uses a dedicated port (853) with TLS, which makes it easy for network administrators to identify and block. DoH is harder to block but also harder to manage in enterprise environments."},{"heading":"Does Encrypted Client Hello (ECH) replace the need for a VPN?","level":3,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-2","content":"ECH encrypts the SNI field in TLS handshakes, preventing on-path observers from seeing which domain you're connecting to. Combined with DoH, it covers the two main metadata leaks in HTTPS. However, a VPN also hides your destination IP, which ECH doesn't. For comprehensive privacy, you still need either a VPN or Tor."},{"heading":"What is Oblivious DoH and when should I use it?","level":3,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#question-3","content":"Oblivious DoH (ODoH) adds a relay between the client and resolver, so the resolver never sees the client's IP and the relay never sees the DNS query content. Use it when you need DNS privacy from both your network path AND your DNS resolver. The latency overhead is typically 5-15ms per query."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#article-tips","content":"1. **Tip:** DoH protects DNS queries from your ISP, but your resolver (Cloudflare, Google) still sees everything. If you need real DNS privacy, run ` Oblivious DoH ` through a relay, neither the relay nor the resolver can link your IP to your queries.\n2. **Tip:** Encrypted Client Hello (ECH) is the missing piece that DoH alone cannot solve. Without ECH, the SNI in TLS ClientHello still leaks which domain you're connecting to, even if your DNS queries are encrypted. Check your browser's ` about:config ` for ` network.dns.echconfig.enabled `.\n3. **Tip:** DNSSEC validates that DNS records haven't been tampered with, but it does NOT encrypt anything, your queries and responses are still visible to on-path observers. You need both DNSSEC (integrity) and DoH/DoT (confidentiality) for complete protection.\n\n- dns\n- privacy\n- doh\n- dot\n- dnssec\n- ech\n- encrypted-client-hello\n- networking\n- security\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/dns-over-https-encrypted-client-hello-privacy"},{"text":"dnscrypt-proxy","url":"https://github.com/DNSCrypt/dnscrypt-proxy"},{"text":"Cloudflare","url":"https://cloudflare.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy"},"headline":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","description":"A comprehensive guide to the DNS privacy stack, DNS-over-HTTPS, DNS-over-TLS, DNSSEC validation, Encrypted Client Hello (ECH), Oblivious DoH, with honest analysis of what each protocol actually protects against and a practical implementation guide.","datePublished":"2026-01-08","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Networking","keywords":"dns, privacy, doh, dot, dnssec, ech, encrypted-client-hello, networking, security","wordCount":1305,"timeRequired":"PT7M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/dns-over-https-encrypted-client-hello-privacy"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"DNS Privacy Stack: DoH, DoT, DNSSEC, Encrypted Client Hello, and Comparative Privacy Analysis","item":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What's the difference between DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT)?","acceptedAnswer":{"@type":"Answer","text":"DoH encapsulates DNS queries in HTTPS requests on port 443, making them indistinguishable from regular HTTPS traffic. DoT uses a dedicated port (853) with TLS, which makes it easy for network administrators to identify and block. DoH is harder to block but also harder to manage in enterprise environments."}},{"@type":"Question","name":"Does Encrypted Client Hello (ECH) replace the need for a VPN?","acceptedAnswer":{"@type":"Answer","text":"ECH encrypts the SNI field in TLS handshakes, preventing on-path observers from seeing which domain you're connecting to. Combined with DoH, it covers the two main metadata leaks in HTTPS. However, a VPN also hides your destination IP, which ECH doesn't. For comprehensive privacy, you still need either a VPN or Tor."}},{"@type":"Question","name":"What is Oblivious DoH and when should I use it?","acceptedAnswer":{"@type":"Answer","text":"Oblivious DoH (ODoH) adds a relay between the client and resolver, so the resolver never sees the client's IP and the relay never sees the DNS query content. Use it when you need DNS privacy from both your network path AND your DNS resolver. The latency overhead is typically 5-15ms per query."}}]}]}],"contentHash":"6ea02bc2cf430d228ec54e69b1885bc109974f1dbe06d8a6bbf4ee81d8512185","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/dns-over-https-encrypted-client-hello-privacy","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":63576,"contentBytes":12223}}
