Everyone knows the padlock means "encrypted". Fewer people know what it leaves out.
When your browser opens https://example.com, it sends a first message to the server called the ClientHello. That message contains the hostname, in plain text, in a field called SNI (Server Name Indication). Your ISP, the café router and anyone else on the path can read it. They cannot read the page, the URL path or the form data. They can read which site you went to.
You do not have to take that on trust. Here is a script that builds a ClientHello and looks inside it.
TL;DR
- The SNI field carries the site name unencrypted in every classic TLS connection, including TLS 1.3.
- Encrypted Client Hello (ECH), now RFC 9849, hides it, but only when the browser, the server and DNS all cooperate.
- A VPN moves the observer: your ISP sees one tunnel endpoint instead of a list of hostnames, and the VPN operator can see what the ISP used to see.
Look inside a ClientHello (15 lines)
This script performs the first step of a TLS handshake against nothing at all. It never touches the network. It only asks Python's ssl module what it would send.
import ssl
host = "example.com"
ctx = ssl.create_default_context()
incoming, outgoing = ssl.MemoryBIO(), ssl.MemoryBIO()
tls = ctx.wrap_bio(incoming, outgoing, server_hostname=host)
try:
tls.do_handshake() # no server: it only produces the ClientHello
except ssl.SSLWantReadError:
pass
client_hello = outgoing.read()
print("first byte:", hex(client_hello[0]), "(0x16 = TLS handshake record)")
print("hostname readable in raw bytes:", host.encode() in client_hello)
Run it with python3 sni_demo.py. On my machine it prints:
first byte: 0x16 (0x16 = TLS handshake record)
hostname readable in raw bytes: True
Those raw bytes are exactly what leaves your laptop before any encryption starts. The hostname is sitting in them.
Why TLS 1.3 did not fix this
TLS 1.3 encrypts most of the handshake, but the ClientHello has to come first, before client and server share any keys. The server needs the name in order to pick the right certificate when one IP address hosts many sites. So the name goes out in the open.
You can see the same thing from a command line against Cloudflare's trace endpoint:
curl -s https://www.cloudflare.com/cdn-cgi/trace | grep sni
On my machine this prints sni=plaintext. Cloudflare is telling you what it saw.
What an observer on your line sees
For a single HTTPS visit, a passive observer gets:
- The server IP address. Always visible, it is how packets get there.
- The hostname from SNI. The part this article is about.
- The DNS lookup that came before. A separate channel, covered in the next section.
- Timing and volume. Roughly how much data moved and when.
They do not get the URL path, query string, cookies or page content. example.com/health-clinic/appointments leaks example.com, not the path.
DNS is the second leak
Before the ClientHello, your device looked up the IP of example.com. By default that lookup is unencrypted and goes to a resolver your network chose. Even if SNI were hidden, the DNS query would still name the site. That is why ECH is paired with encrypted DNS: the browser fetches the ECH key from DNS, and doing that over plain DNS would tell the observer where you are going anyway.
What ECH changes
RFC 9849 defines Encrypted Client Hello. The idea: the client sends an outer ClientHello with a generic public name, and puts the real one inside an encrypted inner ClientHello. The server publishes a public key for this in DNS. RFC 9848 covers how that key is bootstrapped through DNS service bindings.
For ECH to work on a given connection, three things have to line up:
- The browser supports it. Mozilla documents the state of Firefox's implementation on its ECH wiki page, and Chrome has shipped support too. Check the current release notes for your version, because defaults have changed over time.
- The site's DNS publishes the key. No key, no ECH.
- DNS itself is encrypted. Otherwise the lookup gives the site away.
When all three hold, the observer sees the outer name, which is often a CDN's shared hostname, and the destination IP. If many sites sit behind the same IP, that is a real hiding place. If a site has an IP of its own, the IP alone still gives it away. ECH hides the name, not the address.
You can check your own browser: open https://www.cloudflare.com/cdn-cgi/trace and read the sni= line. My curl prints plaintext. A browser that actually sent an encrypted inner name should show something different. If yours still says plaintext, ECH is not active on that connection.
What a VPN does to this picture
A VPN does not make SNI encrypted. It moves your connection to the website out of your ISP's sight.
- Your ISP sees a tunnel to one VPN server and a stream of encrypted packets. No per-site hostnames, no per-site IPs.
- The VPN operator sees the traffic leaving its servers, including the SNI of connections that do not use ECH. You have moved trust from one party to another, so the operator's logging policy matters more than the protocol it uses.
- The website still knows you connected. It sees the VPN server's IP instead of yours.
So the two tools solve different problems. ECH hides the name from everyone on the path. A VPN hides the whole list from your local network. They stack.
Optional: see it on the wire
If you have Wireshark's tshark installed, you can watch your own handshakes (I have not run this variant on this machine, so check the interface name for yours):
sudo tshark -i en0 -Y 'tls.handshake.extensions_server_name' \
-T fields -e ip.dst -e tls.handshake.extensions_server_name
Open a few sites and you will see hostnames scroll by. Now connect to a VPN, capture on the physical interface again, and compare: the hostnames should be gone from that interface and the list shrinks to one destination.
FAQ
Does HTTPS hide which website I visit?
No. It hides the content and the path. The hostname goes out in the ClientHello and in the DNS lookup unless ECH and encrypted DNS are both in use.
What is SNI?
Server Name Indication, a field in the TLS ClientHello that tells the server which hostname you want, so one IP can serve many certificates.
Does TLS 1.3 encrypt SNI?
Not in the standard handshake. The ClientHello comes before any shared keys exist, so SNI is sent in plain text. ECH is the extension that fixes this.
What is Encrypted Client Hello?
A TLS extension standardized in RFC 9849 that wraps the real ClientHello, and with it the hostname, in encryption using a key the server publishes in DNS.
Does a VPN hide the sites I visit from my ISP?
Yes, from your ISP. It sees an encrypted tunnel to one server. The VPN provider can see what your ISP no longer can, so choose the provider as carefully as the protocol.
Can the website still identify me behind a VPN?
Yes, through cookies, logins and browser fingerprint. A VPN changes your IP, not who you are signed in as.
Bottom line
If you are worried about the person sitting on your network, hide the whole list with a tunnel. If you are worried about everyone on the path, you need ECH and encrypted DNS as well, and you need sites that support them. Try the 15-line script first: seeing your own hostname in the raw bytes explains the problem better than any diagram.
If you want a tunnel to compare against, Atomic VPN is one you can use for that comparison.
Got a different sni= result in your browser? Tell me which browser and version in the comments.
Top comments (0)