URLs, DNS and TLS
Before a single byte of HTTP is sent, three things must happen: the URL is parsed, the hostname is resolved to an IP address, and a connection is established and encrypted. Each step has its own protocol, its own caching layer, and its own set of failures.
URL anatomy
Section titled “URL anatomy”A URL is a URI that also tells you how to reach the thing. The full grammar:
scheme userinfo host port path query fragment │ │ │ │ │ │ │ ┌─┴─┐ ┌────┴────┐ ┌─────┴─────┐ ┌──┴─┐ ┌─┴──┐ ┌───────┴───────┐ ┌───┴───┐ https://ada:pw123@api.example.com:8443/v2/users?role=admin&page=2#results └─┬─┘ separator| Component | Notes |
|---|---|
| Scheme | https, http, ws, wss, mailto, file, data. Decides the protocol. Case-insensitive, always written lowercase. |
| Userinfo | user:password@. Deprecated for HTTP — browsers strip it, and it leaks credentials. Still used by some non-HTTP schemes. |
| Host | A domain name, an IPv4 literal (93.184.215.14), or an IPv6 literal in brackets ([2606:2800:21f:cb07::]). |
| Port | Optional. Defaults from the scheme: 80 for http, 443 for https. |
| Path | Hierarchical, /-separated. Case-sensitivity depends entirely on the server. |
| Query | Everything after ?. Conventionally key=value pairs joined by &, but that convention is not in the URL spec — the server decides. |
| Fragment | Everything after #. Never sent to the server. Purely client-side: it scrolls to an element or drives a client-side router. |
Origin vs site
Section titled “Origin vs site”Two terms that look interchangeable and are not:
- An origin is
scheme + host + port.https://app.example.comandhttps://app.example.com:8443are different origins. This is what the same-origin policy uses. - A site is roughly
scheme + registrable domain(eTLD+1).app.example.comandapi.example.comare the same site, different origins.SameSitecookies use this.
Relative URLs
Section titled “Relative URLs”A relative reference is resolved against a base URL:
| Reference | Resolved against https://example.com/a/b/page |
|---|---|
other |
https://example.com/a/b/other |
/other |
https://example.com/other |
../other |
https://example.com/a/other |
//cdn.example.com/x |
https://cdn.example.com/x (protocol-relative — inherits the scheme) |
?q=1 |
https://example.com/a/b/page?q=1 |
#top |
https://example.com/a/b/page#top |
Percent-encoding
Section titled “Percent-encoding”URLs may only contain a limited ASCII set. Everything else — spaces, non-ASCII characters, and any reserved character used as data rather than as a delimiter — is encoded as % followed by two hex digits of its UTF-8 bytes.
Reserved characters, which have structural meaning: : / ? # [ ] @ ! $ & ' ( ) * + , ; =
| Character | Encoded |
|---|---|
| space | %20 (or + inside a query string only) |
/ |
%2F |
? |
%3F |
# |
%23 |
& |
%26 |
= |
%3D |
+ |
%2B |
é |
%C3%A9 (two UTF-8 bytes) |
// Encodes everything unsafe EXCEPT the structural characters : / ? # [ ] @ & = + $ , ;encodeURI('https://example.com/a b?q=c&d=é');// => 'https://example.com/a%20b?q=c&d=%C3%A9'
// Encodes structural characters too — use this for individual valuesencodeURIComponent('a&b=c/d');// => 'a%26b%3Dc%2Fd'The rule: encodeURIComponent for each value, never for a whole URL. In practice, let a URL library build it:
const url = new URL('https://api.example.com/search');url.searchParams.set('q', 'rust & go');url.searchParams.set('page', '2');url.toString();// => 'https://api.example.com/search?q=rust+%26+go&page=2'# curl --get --data-urlencode does the same thing from the shellcurl -v --get --data-urlencode 'q=rust & go' https://httpbin.org/getInternationalised domains
Section titled “Internationalised domains”Hostnames on the wire are ASCII. münchen.de is converted by Punycode to xn--mnchen-3ya.de before DNS lookup. Browsers display the Unicode form but will fall back to showing the raw Punycode when the script mix looks like a homograph attack.
DNS maps names to addresses. It is a globally distributed, hierarchical, cached key-value store, and it runs over UDP port 53 (falling back to TCP for large answers).
The resolution chain
Section titled “The resolution chain”Reading www.example.com right to left — the hierarchy runs from the root down:
- Stub resolver — the OS library in your application. Checks the local cache and
/etc/hosts. - Recursive resolver — your ISP’s, or
1.1.1.1/8.8.8.8, or your router. Does the actual work and caches results for everyone behind it. - Root servers (
.) — 13 logical addresses, heavily anycast. They answer: “ask the.comservers.” - TLD servers (
.com) — answer: “askns1.example.comforexample.com.” - Authoritative servers — hold the real zone file and give the final answer.
The resolver then caches that answer for its TTL and returns it. Steps 3–5 usually never happen for popular domains, because something in the chain already has the answer cached.
# Watch the full delegation chain, one step at a timedig +trace example.comRecord types
Section titled “Record types”| Type | Holds | Example |
|---|---|---|
A |
An IPv4 address | example.com. 300 IN A 93.184.215.14 |
AAAA |
An IPv6 address | example.com. 300 IN AAAA 2606:2800:21f:cb07:: |
CNAME |
An alias to another name | www.example.com. IN CNAME example.com. |
MX |
Mail servers, with a priority number | example.com. IN MX 10 mail.example.com. |
TXT |
Arbitrary text. Used for SPF, DKIM, and domain-ownership proofs. | example.com. IN TXT "v=spf1 include:_spf.google.com ~all" |
NS |
The authoritative nameservers for a zone | example.com. IN NS a.iana-servers.net. |
SOA |
Zone metadata: primary NS, admin email, serial, timers | |
PTR |
Reverse lookup — IP back to a name | |
CAA |
Which certificate authorities may issue certs for this domain | |
SRV |
Service location: host + port for a protocol | |
HTTPS / SVCB |
Connection hints (ALPN, IP hints) so a client can go straight to HTTP/3 |
Every record carries a time to live in seconds. Resolvers must not serve it beyond that. A 300-second TTL means a change propagates within 5 minutes; an 86400 TTL means a day.
The operational pattern before a migration: lower the TTL to 60 a day ahead, make the change, verify, then raise it back. Raising it early does nothing — caches still hold the old, long TTL.
# Short answer onlydig +short example.com A
# Full response with sections and TTLsdig example.com
# A specific record typedig example.com MXdig example.com TXT
# Ask a specific resolver, bypassing your local onedig @1.1.1.1 example.com
# Which nameservers are authoritativedig example.com NS +short
# Reverse lookupdig -x 93.184.215.14
# Older, simpler tool — still everywherenslookup example.comnslookup -type=MX example.comReading dig output: the ANSWER SECTION holds the records, the number before IN is the remaining TTL, and the flags: line tells you whether the answer was authoritative (aa) or came from a cache.
Encrypted DNS
Section titled “Encrypted DNS”Classic DNS is plaintext UDP — any network observer sees every hostname you look up. DoH (DNS over HTTPS, port 443) and DoT (DNS over TLS, port 853) encrypt the query to the resolver. Browsers ship DoH and may use it instead of the OS resolver, which is a common reason /etc/hosts edits appear not to work in Chrome or Firefox.
The TCP handshake
Section titled “The TCP handshake”With an IP address in hand, the client opens a TCP connection. Three packets:
Client Server │ ── SYN (seq=x) ─────────────► │ "I want to talk, my sequence starts at x" │ ◄── SYN-ACK (seq=y, ack=x+1) ─│ "OK, mine starts at y, I got yours" │ ── ACK (ack=y+1) ───────────► │ "Got yours. Connection open."One full round trip before any application data moves. That round trip is pure latency — on a 100 ms link it costs 100 ms no matter how fast either machine is. This is why connection reuse (keep-alive, HTTP/2 multiplexing) is one of the highest-leverage performance wins available.
TCP then provides an ordered, reliable byte stream: sequence numbers detect loss, ACKs confirm delivery, retransmission fixes gaps, and congestion control (slow start, etc.) throttles the sender. HTTP just writes bytes and trusts all of that.
# Is a port open and reachable?nc -vz example.com 443
# Watch the handshake itself (needs root)sudo tcpdump -n 'host example.com and tcp port 443' -c 6TLS (Transport Layer Security — the successor to SSL, though “SSL” persists in tool names) wraps the TCP stream. HTTP over TLS is HTTPS.
What TLS provides
Section titled “What TLS provides”Three guarantees, and it is worth keeping them separate:
- Confidentiality — the traffic is encrypted; an observer sees ciphertext.
- Integrity — tampering is detected; a middlebox cannot silently rewrite bytes.
- Authentication — you are talking to the server that controls the certificate for that hostname.
What it does not provide: anonymity (the destination IP and the SNI hostname are visible), safety (an attacker’s site can have a perfectly valid certificate), or any protection once the bytes reach either endpoint.
The handshake, at a high level
Section titled “The handshake, at a high level”TLS 1.3 (RFC 8446, 2018) simplified this considerably and removed every legacy cipher that had caused trouble. One round trip:
- ClientHello — supported TLS versions, cipher suites, a key-share for guessed key-exchange groups, the SNI (the hostname being requested), and ALPN (which application protocols the client speaks:
h2,http/1.1). - ServerHello — chosen cipher suite, the server’s key-share, chosen ALPN protocol. From here on, everything is encrypted.
- Certificate + CertificateVerify — the server’s certificate chain, plus a signature proving it holds the matching private key.
- Finished on both sides — a MAC over the whole handshake, so any tampering with earlier messages is detected.
Both sides derive the same symmetric session keys from the exchanged key-shares (an ephemeral Diffie-Hellman exchange). Because the private keys are ephemeral and discarded, capturing traffic today and stealing the server’s certificate key later does not decrypt it — that property is forward secrecy, and TLS 1.3 makes it mandatory.
TLS 1.2 needed two round trips and allowed non-forward-secret key exchange. It is still widely supported; anything below 1.2 is deprecated and should be off.
Certificates and the chain of trust
Section titled “Certificates and the chain of trust”A certificate binds a public key to an identity (one or more hostnames), signed by a certificate authority. Verification walks a chain:
Leaf: CN=example.com ← signed byIntermediate: CN=DigiCert TLS RSA SHA256 2020 CA1 ← signed byRoot: CN=DigiCert Global Root CA ← in your OS/browser trust storeThe client checks, for every link: the signature is valid, the current time is inside the validity window, the certificate is not revoked, and — for the leaf — the requested hostname matches a name in the Subject Alternative Name extension. (The old CN field is ignored by modern browsers.) Roots are self-signed; they are trusted because they are pre-installed, not because anything vouches for them.
Public certificates are also logged to Certificate Transparency logs, which are publicly searchable — useful both for catching mis-issuance and, on the offensive side, for enumerating a target’s subdomains.
TLS happens before HTTP, so the server has no Host header yet — but it must pick a certificate. Server Name Indication solves this: the client puts the hostname in the ClientHello, in plaintext.
That plaintext hostname is why a network observer still knows which sites you visit over HTTPS. Encrypted Client Hello (ECH) addresses it and is rolling out, but is not yet universal.
Practical consequence: on a shared IP you must send the right SNI or you get the wrong certificate.
# Wrong: no SNI, server returns its default certopenssl s_client -connect example.com:443 </dev/null
# Right: SNI set explicitlyopenssl s_client -connect example.com:443 -servername example.com </dev/nullInspecting TLS
Section titled “Inspecting TLS”# Full handshake trace, chain, protocol and cipheropenssl s_client -connect example.com:443 -servername example.com </dev/null
# Just the certificate, decodedopenssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \ | openssl x509 -noout -text
# Expiry dates onlyopenssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \ | openssl x509 -noout -dates
# Subject Alternative Names — every hostname the cert coversopenssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \ | openssl x509 -noout -ext subjectAltName
# Force a version to test supportopenssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/nullIn the output, look for Protocol : TLSv1.3, Cipher : TLS_AES_256_GCM_SHA384, and Verify return code: 0 (ok). A non-zero verify code names the exact failure — expired, self-signed, unable to get local issuer certificate (a missing intermediate, the single most common misconfiguration).
curl -v shows a condensed version of the same information, which is usually enough:
curl -v https://example.com/ -o /dev/null 2>&1 | grep -E '^\*'HTTPS and HSTS
Section titled “HTTPS and HSTS”A user typing example.com gets http:// by default, and that first plaintext request can be hijacked. Two mitigations:
- HSTS — the
Strict-Transport-Securityresponse header tells the browser to use HTTPS for this host for the next N seconds, convertinghttp://links internally before any request goes out. - The HSTS preload list — a list compiled into browsers, so even the very first visit is HTTPS. Requires
includeSubDomainsandpreloadin the header and a submission to hstspreload.org.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadSee security headers for the rest of the set.
Key points
Section titled “Key points”- A URL is scheme, optional userinfo, host, optional port, path, query and fragment — and the fragment never leaves the browser.
- Percent-encode individual values with
encodeURIComponent, or build URLs with a URL library. - DNS is a cached hierarchy: stub → recursive resolver → root → TLD → authoritative, with TTLs governing staleness.
- TCP costs one round trip before any data; TLS 1.3 costs one more. Reusing connections avoids both.
- TLS gives confidentiality, integrity and server authentication — not safety, not anonymity.
- Certificates are trusted via a chain up to a pre-installed root; SNI is sent in plaintext so the server can pick the right one.