Panoptes › ECH on Linux
Enable Encrypted Client Hello (ECH) on Linux
You encrypted your DNS, yet every HTTPS connection still announces the site's name in the clear. Your distribution's curl rejects --ech, and you are not sure whether your browser is using ECH at all. This page covers what ECH hides, why it does nothing without secure DNS, how to build a curl that speaks it, how to set it up in Firefox and Chromium-based browsers, and how to prove it works.
What ECH hides, and what it does not
HTTPS encrypts what you send and receive, but not which site you are talking to. The first message of a TLS handshake, the ClientHello, carries the hostname in the SNI field before any encryption exists. Your ISP, the café router or a corporate middlebox can read it, and much real-world filtering and logging works exactly that way.
With ECH, the site publishes a public key in a DNS HTTPS record. The client encrypts the real ClientHello, hostname included, to that key and wraps it in an outer ClientHello that names only a cover name, such as the provider's. An observer sees a connection to the cover name, not the site behind it.
What it does not do:
- The destination IP is still visible. Behind a big CDN, thousands of sites share an address, so that says little. A site on its own IP is identified by the address alone. A VPN hides the IP from your ISP but shows both ends to the VPN operator.
- The site has to support it. Only sites that publish an ECH key in DNS get the protection. Many Cloudflare-fronted sites do; much of the web does not yet.
- Traffic analysis still works. Packet sizes and timing still reveal things.
- Some networks block it, deliberately or through middleboxes that fail on an unfamiliar handshake.
The DNS half is not optional
The ECH key arrives in a DNS lookup. If that lookup travels in plaintext, the hostname leaks in the query instead, and anyone on the path can strip the key from the answer so the client falls back to a normal, readable handshake. ECH is only worth having with DNS-over-HTTPS or DNS-over-TLS protecting the lookup.
Check whether a site publishes an ECH key with an ech= value in its HTTPS record:
dig +short HTTPS defo.ie
resolvectl query -t HTTPS defo.ieIf your dig does not know the HTTPS type, ask for it by number: dig +short defo.ie TYPE65.
Does your curl support ECH?
curl -V | grep -w ECHAn ECH-capable build lists ECH on its Features: line. Without it, curl --ech true stops with option --ech: the installed libcurl version does not support this. Distribution builds usually lack it: the curl project marks ECH as experimental, and it has to be switched on when curl is built (--enable-ech) against a TLS library that implements it. According to curl's build notes, those are OpenSSL 4.0.0 or later, BoringSSL, AWS-LC, wolfSSL and Rustls.
Build an ECH-capable curl
These steps follow curl's own docs/ECH.md. Everything stays under $HOME/code, so your system curl and OpenSSL are untouched. You need git, a C compiler, make, perl, autoconf, automake, libtool and pkg-config. If curl's configure stops on a missing library, install that library's development package.
mkdir -p $HOME/code && cd $HOME/code
git clone --depth 1 --branch openssl-4.0.0 https://github.com/openssl/openssl
cd openssl
./config --libdir=lib --prefix=$HOME/code/openssl-local-inst
make -j"$(nproc)"
make install_swcd $HOME/code
git clone --depth 1 https://github.com/curl/curl
cd curl
autoreconf -fi
LDFLAGS="-Wl,-rpath,$HOME/code/openssl-local-inst/lib/" ./configure --with-ssl=$HOME/code/openssl-local-inst --enable-ech
make -j"$(nproc)"configure should end with WARNING: ECH is enabled but marked EXPERIMENTAL. If it does not, ECH is not enabled. The rpath in LDFLAGS lets the new binary find its OpenSSL without touching the system library path. Do not add that directory to /etc/ld.so.conf.d, which would hand an experimental TLS library to every program on the machine.
Test ECH with curl
cd $HOME/code/curl
./src/curl -V | grep -w ECH
./src/curl -sS --ech hard --doh-url https://one.one.one.one/dns-query https://defo.ie/ech-check.php | grep -oiE 'SSL_ECH_STATUS: [a-z_]+'
./src/curl -sS --ech hard --doh-url https://one.one.one.one/dns-query https://tls-ech.dev/ | grep -o 'You are[^<]*'--ech hardrefuses to fall back to a cleartext SNI, so a successful transfer means ECH was used.--ech truetries ECH if possible and carries on without it otherwise.--doh-urlmatters. curl only gets theHTTPSrecord, and so the ECH key, through DNS-over-HTTPS. Without it, you would have to paste the key in with--ech ecl:.- A working run prints
SSL_ECH_STATUS: SUCCESSfrom defo.ie andYou are using ECH.from tls-ech.dev. Plain curl getsNOT_TRIEDandYou are not using ECH. - Most ECH failures exit with code 101 (
CURLE_ECH_REQUIRED). If one test site fails and another passes, the problem is that server or the draft version it speaks, not your build. Test against more than one.
Firefox, LibreWolf and other Firefox-based browsers
In current Firefox source, the ECH preferences network.dns.echconfig.enabled and network.dns.http3_echconfig.enabled default to true. Check them in about:config, and set them back to true if something changed them. The part that usually needs doing is secure DNS, which Firefox calls TRR:
| Preference | Value | Meaning |
|---|---|---|
network.trr.mode | 2 | DoH first, the system resolver as fallback |
network.trr.mode | 3 | DoH only, never the system resolver |
network.trr.uri | https://cloudflare-dns.com/dns-query | The DoH server to use (any https:// DoH endpoint) |
The DNS over HTTPS section of Firefox's Privacy & Security settings sets the same thing. To make the settings survive a preferences reset, put them in user.js in your profile folder (about:profiles shows its path). Firefox reads that file at every start:
user_pref("network.dns.echconfig.enabled", true);
user_pref("network.dns.http3_echconfig.enabled", true);
user_pref("network.trr.mode", 2);
user_pref("network.trr.uri", "https://cloudflare-dns.com/dns-query");Mode 2 falls back to the system resolver if DoH fails, so a hostile network can still push you to plaintext lookups. Mode 3 does not fall back, but it breaks captive portals and names that only your local DNS knows.
Chrome, Brave, Chromium and Vivaldi
Chrome has shipped ECH enabled by default since Chrome 117, so there is nothing to switch on for ECH itself. It still needs the site's HTTPS record, so turn on Use secure DNS under Settings → Privacy and security → Security and pick a provider. That keeps the lookup encrypted and the key intact. Organisations can turn ECH off with the EncryptedClientHelloEnabled policy; if ECH never works, check chrome://policy. Brave, Vivaldi, Edge and other Chromium-based browsers build on the same network stack, but confirm with a test page rather than assuming.
Leave Tor Browser and Mullvad Browser alone. They resolve names through their proxy circuit on purpose, and pointing them at a DoH resolver would send your lookups around it.
Test ECH in the browser
- defo.ie/ech-check.php reports
SSL_ECH_STATUSand the inner and outer SNI. - tls-ech.dev says "You are using ECH" or "You are not using ECH".
- crypto.cloudflare.com/cdn-cgi/trace prints an
sni=line.sni=plaintextmeans the hostname went out in the clear.
Restart the browser after changing settings. A Chromium-based browser that is still running when you edit its files can overwrite your change when it exits.
Automated: two scripts from Panoptes do this for you. ech-build.sh builds Stephen Farrell's OpenSSL ECH branch (ECH-draft-13c by default) as a static library in /opt/openssl-ech, so no other program can link it, then curl against it into /usr/local. Package updates leave both alone, and /usr/bin/curl stays installed. Note that /usr/local/bin/curl then comes first in PATH, including under sudo, and the build is deliberately minimal: no HTTP/3, and without --full no brotli, zstd, GSSAPI, SCP, SFTP, SMB or LDAP. --verify runs a live --ech hard handshake, and --uninstall hands curl back to your distribution. ech-browsers.sh finds every Firefox-family and Chromium-family browser, including Flatpak and Snap installs, and writes the preferences above to user.js, or sets Chromium's secure DNS in Local State. It backs up each file, supports --list, --dry-run and --revert, skips Chromium browsers that are still running, and never touches Tor Browser or Mullvad Browser. Its Firefox default is TRR mode 2 with Cloudflare's DoH endpoint. checkdns checks both halves: the ech= key in DNS and a real handshake with defo.ie.
FAQ
Is ECH on by default in Firefox and Chrome?
Chrome has shipped ECH enabled since Chrome 117. Current Firefox sets network.dns.echconfig.enabled and network.dns.http3_echconfig.enabled to true by default. In both, ECH only happens when the browser gets the site's HTTPS DNS record, so turn on secure DNS and confirm at defo.ie/ech-check.php.
Why does curl say the installed libcurl version does not support --ech?
Your curl was built without ECH. Run curl -V and look for ECH on the Features line. Distribution builds usually lack it, because ECH is experimental in curl and has to be enabled at build time against a TLS library that implements it, so build your own.
Why does curl --ech hard fail with error 101?
Exit code 101 is CURLE_ECH_REQUIRED: ECH could not be done. Check that you passed --doh-url, because curl only fetches the HTTPS record over DoH, and that the site's HTTPS record has an ech= value. If one test site fails and another works, the problem is on that server.
Does ECH hide which site I visit from my ISP?
It hides the hostname in the TLS handshake, not the destination IP address. On a large CDN that address is shared by many sites and says little; a site on its own IP is still identifiable. Packet sizes and timing also remain visible.
Should I enable DoH and ECH in Tor Browser or Mullvad Browser?
No. Both resolve names through their proxy circuit on purpose. Pointing them at a DoH resolver would send your lookups around the circuit, which weakens the anonymity they are built for.
Sources
- curl(1) man page:
--ech,--doh-url, the ECH feature flag - curl docs/ECH.md: supported TLS libraries, build steps, test sites
- CURLOPT_ECH: experimental status
- Firefox StaticPrefList.yaml: ECH preference defaults
- Mozilla wiki: Trusted Recursive Resolver:
network.trr.modevalues - Chrome Platform Status: TLS Encrypted Client Hello: shipped in Chrome 117, enterprise policy
- resolvectl(1):
query --type=