Learn Application Layer Networking: From HTTP/1.1 to HTTP/3 (QUIC)
Goal
Deeply understand the protocols that power the modern web - from the foundational text-based HTTP/1.1 and the hierarchical DNS system to the encrypted binary streams of HTTP/2, the UDP-based revolution of HTTP/3, and the global infrastructure of Content Delivery Networks (CDNs).
Why Application Layer Networking Matters
The application layer is where the “magic” of the internet becomes usable for humans. It’s the layer that translates a user’s intent into a structured conversation between machines. While the lower layers (IP, TCP/UDP) handle routing and reliability, the application layer defines the meaning of the data.
Understanding this layer is the difference between being a developer who “uses APIs” and an engineer who “builds systems.” When a page loads slowly, is it a TCP slow-start issue, a DNS resolution bottleneck, or an inefficient HTTP/2 multiplexing priority? To answer this, you must understand the wire format, the state machines, and the security handshakes that happen in the first few milliseconds of every connection.
Real-World Impact of Application Layer Knowledge
Why does this matter for your career?
-
Debugging Production Issues: When your API returns 502 errors intermittently, is it a DNS TTL problem, a TLS certificate chain issue, or HTTP/2 stream prioritization gone wrong? Engineers who understand the application layer can diagnose these issues in minutes, not days.
-
Performance Optimization: A single misconfigured
Cache-Controlheader can cost companies millions in unnecessary bandwidth. Understanding HTTP caching semantics directly impacts infrastructure costs and user experience. -
Security Architecture: Every major breach involves some form of network communication. Understanding TLS handshakes, certificate validation, and protocol vulnerabilities is essential for building secure systems.
-
System Design Interviews: “Design a CDN” or “Explain what happens when you type google.com” are standard questions. Deep protocol knowledge separates senior engineers from juniors.
The Numbers Tell the Story (2025 Data):
- HTTP traffic accounts for over 80% of all internet traffic by volume
- DNS handles over 123.55 billion queries daily on UltraDNS alone (Vercara 2024 report), with 7 trillion DNS requests daily across Akamai’s infrastructure (Akamai DNS statistics)
- The average web page makes 70+ HTTP requests to fully load
- TLS 1.3 adoption: 62.1% to 70.1% of websites (SSL Labs 2024), reduced handshake latency by 40% (from 2-RTT to 1-RTT)
- HTTP/2 adoption: approximately 60-68% of all web traffic (down slightly from peak as HTTP/3 adoption grows)
- HTTP/3 adoption: 35.9% of websites globally (W3Techs Oct 2025), with 30%+ of global web traffic already using HTTP/3 (Internet Society 2025)
- Major platforms (Google, Meta, Apple, Cloudflare, Akamai, Fastly) serve billions of HTTP/3 requests daily at scale
- 87.6% of websites now use valid SSL/TLS certificates, up from 18.5% six years ago (SSL Insights 2025)
- Over 305 million SSL certificates active on the internet as of July 2025 (SSL Dragon 2025)
Common Application Layer Problems You’ll Encounter:
| Problem | Root Cause | What You’ll Learn to Diagnose |
|---|---|---|
| “Site works in Chrome, fails in curl” | TLS version mismatch or missing SNI | TLS handshake negotiation |
| “API calls timeout randomly” | DNS TTL expiration during connection pool | DNS caching behavior |
| “Page loads slowly on mobile” | No HTTP/2 multiplexing, many round trips | Protocol evolution benefits |
| “CORS errors in production only” | HTTP headers stripped by proxy | Header semantics and proxies |
| “WebSocket disconnects after 60s” | Intermediate proxy timeout | Connection keep-alive mechanisms |
Visual Foundation: The OSI Model and Application Layer
Understanding where the application layer sits in the networking stack is fundamental to diagnosing issues:
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE OSI MODEL (Simplified) │
│ Focusing on Application Layer Context │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Layer 7: APPLICATION <─── YOU ARE HERE ───> │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ HTTP/1.1, HTTP/2, HTTP/3, DNS, TLS, WebSocket, gRPC, GraphQL │ │
│ │ │ │
│ │ This layer defines WHAT the data means: │ │
│ │ - Request/Response semantics (GET, POST, 200 OK, 404) │ │
│ │ - Data encoding (JSON, Protobuf, XML) │ │
│ │ - Authentication (cookies, tokens, certificates) │ │
│ │ - Caching rules (Cache-Control, ETags) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 6: PRESENTATION (Often merged with Application) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Encryption (TLS), Compression (gzip, brotli), Character encoding │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 5: SESSION (Often merged with Application) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Session management, connection state, multiplexing │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 4: TRANSPORT │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TCP: Reliable, ordered, connection-oriented │ │
│ │ UDP: Fast, connectionless, best-effort │ │
│ │ QUIC: UDP + reliability + encryption (user-space) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 3: NETWORK │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ IP (IPv4/IPv6): Addressing and routing between networks │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 2: DATA LINK │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Ethernet, Wi-Fi: Local network frame delivery │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Layer 1: PHYSICAL │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Cables, radio waves, fiber optics: Raw bit transmission │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP Request Flow: From Browser to Server
This diagram shows what happens when you type a URL and press Enter:
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP REQUEST LIFECYCLE (HTTPS) │
│ From "Enter" Key to Rendered Page │
└─────────────────────────────────────────────────────────────────────────────┘
USER TYPES: https://api.example.com/users
┌──────────────────────────────────────────────────────────────────────────────┐
│ PHASE 1: DNS RESOLUTION ~50ms │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Browser DNS Resolver Root/TLD/Auth │
│ │ │ │ │
│ │ ── "api.example.com?" ──► │ │ │
│ │ │ ─── Query Root ───────────►│ │
│ │ │ ◄── ".com is at X" ───────│ │
│ │ │ ─── Query .com TLD ───────►│ │
│ │ │ ◄── "example.com at Y" ───│ │
│ │ │ ─── Query Authoritative ──►│ │
│ │ │ ◄── "93.184.216.34" ──────│ │
│ │ ◄─ "93.184.216.34" ──────│ │ │
│ │ │ │ │
│ Result: IP Address 93.184.216.34 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ PHASE 2: TCP HANDSHAKE ~30ms │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │ ────────── SYN (seq=100) ────────────────────────────────►│ │
│ │ │ │
│ │ ◄───────── SYN-ACK (seq=300, ack=101) ───────────────────│ │
│ │ │ │
│ │ ────────── ACK (seq=101, ack=301) ───────────────────────►│ │
│ │ │ │
│ Result: TCP Connection Established (3-way handshake complete) │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ PHASE 3: TLS HANDSHAKE (TLS 1.3) ~50ms │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │ ── ClientHello ─────────────────────────────────────────►│ │
│ │ (supported ciphers, key_share, SNI: api.example.com) │ │
│ │ │ │
│ │ ◄─ ServerHello + EncryptedExtensions + Certificate ──────│ │
│ │ (selected cipher, server key_share, cert chain) │ │
│ │ ◄─ CertificateVerify + Finished ─────────────────────────│ │
│ │ │ │
│ │ [Client verifies certificate chain] │ │
│ │ [Both derive shared secret via ECDHE] │ │
│ │ │ │
│ │ ── Finished ────────────────────────────────────────────►│ │
│ │ │ │
│ Result: Encrypted channel established (1-RTT in TLS 1.3) │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ PHASE 4: HTTP REQUEST/RESPONSE ~100ms │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ │ │ │
│ │ ══════════ Encrypted HTTP Request ═══════════════════════►│ │
│ │ GET /users HTTP/1.1 │ │
│ │ Host: api.example.com │ │
│ │ Accept: application/json │ │
│ │ Authorization: Bearer eyJhbG... │ │
│ │ │ │
│ │ [Server │ │
│ │ processes │ │
│ │ request] │ │
│ │ │ │
│ │ ◄═════════ Encrypted HTTP Response ══════════════════════│ │
│ │ HTTP/1.1 200 OK │ │
│ │ Content-Type: application/json │ │
│ │ Content-Length: 1234 │ │
│ │ Cache-Control: max-age=60 │ │
│ │ │ │
│ │ {"users": [...]} │ │
│ │ │ │
│ Result: Data received, connection may stay open (keep-alive) │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
TOTAL TIME: ~230ms for first request (subsequent requests skip DNS & TLS)
The Protocol Stack: HTTP, TLS, TCP, and IP
This diagram shows how data is encapsulated as it travels down the stack:
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROTOCOL ENCAPSULATION │
│ How Your HTTP Request Becomes Network Packets │
└─────────────────────────────────────────────────────────────────────────────┘
APPLICATION DATA (What you write):
┌─────────────────────────────────────────────────────────────────────────────┐
│ GET /api/users HTTP/1.1 │
│ Host: api.example.com │
│ Accept: application/json │
│ │
│ {"query": "active"} │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ TLS Layer encrypts
┌─────────────────────────────────────────────────────────────────────────────┐
│ TLS RECORD │
│ ┌────────────┬─────────────────────────────────────────────────────────┐ │
│ │ TLS Header │ Encrypted Application Data │ │
│ │ (5 bytes) │ (AES-GCM ciphertext + authentication tag) │ │
│ │ │ │ │
│ │ Type: 0x17 │ 0x8a 0x3f 0xc2 0x91 0x4d 0x7e 0xb8 0x5c 0xa2... │ │
│ │ Ver: 0x0303│ │ │
│ │ Len: 0x00ff│ [Original HTTP request is now unreadable] │ │
│ └────────────┴─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ TCP Layer adds sequencing
┌─────────────────────────────────────────────────────────────────────────────┐
│ TCP SEGMENT │
│ ┌────────────────────────────┬─────────────────────────────────────────┐ │
│ │ TCP Header │ TLS Record (payload) │ │
│ │ (20 bytes) │ │ │
│ │ │ │ │
│ │ Src Port: 52341 │ [Encrypted data from above] │ │
│ │ Dst Port: 443 │ │ │
│ │ Seq: 1000 │ │ │
│ │ Ack: 5001 │ │ │
│ │ Flags: PSH, ACK │ │ │
│ │ Window: 65535 │ │ │
│ │ Checksum: 0xa1b2 │ │ │
│ └────────────────────────────┴─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ IP Layer adds addressing
┌─────────────────────────────────────────────────────────────────────────────┐
│ IP PACKET │
│ ┌────────────────────────────┬─────────────────────────────────────────┐ │
│ │ IP Header │ TCP Segment (payload) │ │
│ │ (20 bytes) │ │ │
│ │ │ │ │
│ │ Version: 4 │ [TCP header + TLS record] │ │
│ │ IHL: 5 │ │ │
│ │ Total Length: 295 │ │ │
│ │ TTL: 64 │ │ │
│ │ Protocol: 6 (TCP) │ │ │
│ │ Src IP: 192.168.1.100 │ │ │
│ │ Dst IP: 93.184.216.34 │ │ │
│ └────────────────────────────┴─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ Ethernet adds local delivery
┌─────────────────────────────────────────────────────────────────────────────┐
│ ETHERNET FRAME │
│ ┌──────────────┬─────────────────────────────────────────┬─────────────┐ │
│ │ Eth Header │ IP Packet (payload) │ Eth Trailer │ │
│ │ (14 bytes) │ │ (4 bytes) │ │
│ │ │ │ │ │
│ │ Dst MAC │ [IP header + TCP segment] │ FCS │ │
│ │ Src MAC │ │ (checksum) │ │
│ │ EtherType │ │ │ │
│ │ (0x0800=IPv4)│ │ │ │
│ └──────────────┴─────────────────────────────────────────┴─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ Physical Layer
[Electrical signals on the wire]
SUMMARY OF ENCAPSULATION:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Layer │ Protocol │ Header Size │ What It Adds │
│ ─────────────┼──────────┼─────────────┼────────────────────────────── │
│ Application │ HTTP │ Variable │ Request method, headers, body │
│ Presentation │ TLS │ 5+ bytes │ Encryption, integrity (AEAD) │
│ Transport │ TCP │ 20+ bytes │ Ports, sequencing, reliability │
│ Network │ IP │ 20+ bytes │ Source/dest IP, routing info │
│ Data Link │ Ethernet │ 14+4 bytes │ MAC addresses, frame check │
│ Physical │ - │ - │ Bits on the wire │
│ │
│ Total overhead: ~60+ bytes per packet (before HTTP even starts!) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Protocol Evolution: Why HTTP/2 and HTTP/3 Exist
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROTOCOL EVOLUTION COMPARISON │
│ Why the Web Keeps Upgrading Its Protocols │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP/1.1 (1997) - One request at a time per connection
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Client ════════════ TCP Connection 1 ════════════ Server │
│ │ │ │
│ │──── GET /page.html ────────────────────►│ │
│ │◄─── Response ─────────────────────────── │ (blocking) │
│ │──── GET /style.css ────────────────────►│ │
│ │◄─── Response ─────────────────────────── │ (must wait) │
│ │──── GET /script.js ────────────────────►│ │
│ │◄─── Response ─────────────────────────── │ │
│ │
│ Problem: HEAD-OF-LINE BLOCKING - each request waits for previous │
│ Workaround: Open 6+ parallel TCP connections (wasteful!) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP/2 (2015) - Multiplexed streams over single TCP connection
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Client ════════════ TCP Connection ══════════════ Server │
│ │ │ │
│ │══ Stream 1: GET /page.html ════════════►│ │
│ │══ Stream 3: GET /style.css ════════════►│ (parallel!) │
│ │══ Stream 5: GET /script.js ════════════►│ │
│ │ │ │
│ │◄═ Stream 1: [HTML data frames] ═════════│ │
│ │◄═ Stream 3: [CSS data frames] ══════════│ (interleaved) │
│ │◄═ Stream 5: [JS data frames] ═══════════│ │
│ │
│ Improvement: Binary framing, header compression (HPACK), server push │
│ Remaining Problem: TCP packet loss blocks ALL streams (TCP HOL) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
HTTP/3 + QUIC (2022) - Independent streams over UDP
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Client ════════════ QUIC (over UDP) ═════════════ Server │
│ │ │ │
│ │═══ Stream 0: GET /page.html ═══════════►│ │
│ │═══ Stream 4: GET /style.css ═══════════►│ │
│ │═══ Stream 8: GET /script.js ═══════════►│ │
│ │ │ │
│ │ [Packet loss on Stream 4] │ │
│ │ ↓ │ │
│ │◄══ Stream 0: continues unaffected! ═════│ │
│ │◄══ Stream 8: continues unaffected! ═════│ │
│ │◄══ Stream 4: retransmits independently ═│ │
│ │
│ Improvements: No HOL blocking, 0-RTT resumption, connection migration │
│ Built-in encryption (TLS 1.3 mandatory), user-space implementation │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
PERFORMANCE COMPARISON (2025 Real-World Benchmarks):
┌────────────────────────────┬──────────────┬──────────────┬──────────────┐
│ Metric │ HTTP/1.1 │ HTTP/2 │ HTTP/3 │
├────────────────────────────┼──────────────┼──────────────┼──────────────┤
│ Page Load (Good Network) │ 2.8s │ 2.30s │ 2.33s │
│ Page Load (Mobile 4G+15% │ 5.2s │ 3.6s │ 1.6s │
│ packet loss) │ │ │ (55% faster) │
│ Connection Setup (50ms RTT)│ 201ms │ 201ms │ 176ms │
│ │ │ │ (12.4% faster)│
│ Mobile Latency Reduction │ baseline │ -15% │ -30% │
│ Connections Needed │ 6-8 │ 1 │ 1 │
│ HOL Blocking │ Application │ TCP Layer │ None │
│ Connection Setup (ideal) │ 3-4 RTT │ 3-4 RTT │ 1 RTT (0-RTT)│
│ Encryption │ Optional │ Optional │ Mandatory │
│ CPU/Memory Cost (server) │ Low │ Medium │ High │
└────────────────────────────┴──────────────┴──────────────┴──────────────┘
**Key 2025 Findings** ([DebugBear](https://www.debugbear.com/blog/http3-vs-http2-performance), [Akamai 2025](https://www.akamai.com/blog/performance/http3-performance)):
- HTTP/3 shows **strongest gains on mobile/unstable networks** (30% latency reduction)
- On **stable, low-latency connections**, HTTP/2 and HTTP/3 perform similarly
- HTTP/3 **connection setup 45% faster** on networks with 50ms+ RTT
- For **small pages** (<100KB), HTTP/3 provides minimal benefit
- For **large, multi-asset pages**, HTTP/3 shines under packet loss conditions
- **Real-world adoption**: Google, Meta, Apple, Cloudflare, Akamai deploy at scale despite higher server costs
The DNS Hierarchy: The Internet’s Phonebook
DNS is a distributed, hierarchical database. It’s the first step in almost every network connection.
. (root)
|
+-----------+-----------+
| | |
com org net (TLDs)
| | |
+---+---+ example cloudflare (2nd Level)
| | |
google amazon www www (Subdomains)
What you must understand: UDP vs TCP in DNS, Recursion vs Iteration, Resource Records (A, AAAA, CNAME, MX, TXT), and Caching/TTL.
HTTP Evolution: From Text to Binary
HTTP has moved from a simple, synchronous text protocol to a complex, asynchronous binary stream.
HTTP/1.1 (Text-based, Synchronous)
Request: GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n
Response: HTTP/1.1 200 OK\r\nContent-Length: 42\r\n\r\nHello...
Problem: Head-of-line (HOL) blocking at the application level.
HTTP/2 (Binary Framing, Multiplexed)
[Frame Header][DATA/HEADERS/SETTINGS]
Solution: Multiplexing multiple requests over one TCP connection. Remaining Problem: TCP HOL blocking.
HTTP/3 (QUIC - UDP based) Solution: Move reliability and stream management to UDP, eliminating TCP HOL blocking and enabling faster handshakes (0-RTT).
TLS and PKI: The Foundation of Trust
HTTPS is HTTP over TLS. TLS provides Encryption, Integrity, and Authentication.
Client Server
|---------- ClientHello ----------->|
|<--------- ServerHello + Cert -----|
|---------- KeyExchange ------------>|
|<--------- Finished --------------|
What you must understand: Asymmetric vs Symmetric encryption, Certificate Authorities (CAs), Chain of Trust, and the Handshake process.
WebSockets: Bidirectional Real-time
WebSockets start as an HTTP request and “Upgrade” to a persistent, bidirectional TCP connection.
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Caching and CDNs: Edge Distribution
Caching turns repeat requests into local hits, while CDNs place content at edge locations close to users. You need to understand cache keys, freshness, and invalidation because they directly shape latency and cost.
Client -> Edge POP -> Regional Cache -> Origin
| | | |
| HIT | | |
|<--------| | |
| MISS |------------->|------------->
| |<-------------|<-------------
Key ideas: cacheability (Cache-Control, ETag), freshness vs revalidation, and what happens when a cached object is stale or uncacheable.
Prerequisites & Background Knowledge
Before diving into these projects, you should have foundational understanding in the following areas. These projects assume you can program and have basic networking knowledge, but you’ll learn the deep protocol internals as you build.
Essential Prerequisites (Must Have)
Programming Skills:
- Proficiency in at least one systems language: Python, Go, Rust, or C
- Comfort with sockets programming (binding, listening, accepting connections)
- Understanding of binary data manipulation (bytes, bit operations, hex representation)
- Ability to read RFCs and protocol specifications (you’ll spend a lot of time in RFC 1035, RFC 7540, RFC 9000)
- Recommended: If you’ve never worked with sockets, complete a “TCP echo server” tutorial first
Networking Fundamentals:
- TCP/IP stack basics: What layer 3 (IP) and layer 4 (TCP/UDP) do
- How DNS works at a high level (you type a domain, get an IP back)
- HTTP basics: requests, responses, headers, status codes
- TLS/SSL concept: encryption between client and server (not the math, just the purpose)
- Recommended Reading: “Computer Networks, Fifth Edition” by Tanenbaum — Ch. 1 (Introduction), Ch. 5 (Network Layer), Ch. 6 (Transport Layer)
Binary & Encoding Knowledge:
- Hexadecimal representation (0xFF = 255, how to read hex dumps)
- Network byte order (big-endian) vs host byte order
- ASCII vs binary protocols (text-based HTTP/1.1 vs binary HTTP/2)
- Base64 encoding (used in HTTP headers)
- Recommended Reading: “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron — Ch. 2 (Representing and Manipulating Information)
Systems Knowledge:
- Command-line proficiency: Linux/macOS terminal, basic bash
- How to use debugging tools:
strace,tcpdump,netstat,lsof - Understanding of processes and ports
- Recommended Reading: “How Linux Works, 3rd Edition” by Brian Ward — Ch. 1-3
Helpful But Not Required
You’ll learn these during the projects, but having background helps:
Advanced Networking:
- Packet structure: Ethernet frames, IP headers, TCP headers
- How routing and NAT work
- MTU and fragmentation
- Can learn during: Projects 2, 3, 6, 7
- Book Reference: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens — Ch. 2-6
Cryptography Basics:
- Public key cryptography (RSA, ECDSA)
- Symmetric encryption (AES, ChaCha20)
- Hashing and HMAC (SHA-256)
- Digital certificates and Certificate Authorities
- Can learn during: Project 3 (TLS Handshake Explorer)
- Book Reference: “Bulletproof TLS and PKI” by Ivan Ristić — Ch. 1
HTTP/2 & HTTP/3 Concepts:
- Binary framing layer
- Stream multiplexing
- QUIC and UDP reliability
- Can learn during: Projects 5, 7
- Book Reference: “High Performance Browser Networking” by Ilya Grigorik — Ch. 12, 13
Compression & Encoding:
- gzip and brotli compression
- HPACK header compression
- Can learn during: Projects 2, 5
- Book Reference: “HTTP: The Definitive Guide” by David Gourley — Ch. 15
Self-Assessment Questions
Before starting, ask yourself:
- ✅ Can you write a TCP echo server that accepts connections and echoes back data?
- ✅ Do you know what happens when you type
curl https://google.com? (DNS → TCP → TLS → HTTP) - ✅ Can you read a hex dump and understand that
0x48 0x54 0x54 0x50= “HTTP”? - ✅ Have you ever used
tcpdumpor Wireshark to capture network packets? - ✅ Can you explain the difference between UDP and TCP?
If you answered “no” to questions 1-3: Spend 1-2 weeks building a simple HTTP/1.0 server (without frameworks) first. Read “HTTP: The Definitive Guide” Ch. 1-4.
If you answered “yes” to all 5: You’re ready to begin! You have the foundational knowledge to tackle these projects.
If you’re somewhere in between: Start with Project 1 (DNS Resolver) — it’s the gentlest introduction to binary protocols.
Development Environment Setup
To complete these projects, you’ll need:
Required Tools:
- Linux or macOS (Windows WSL2 works too)
- Programming language runtime:
- Python 3.10+ (easiest for beginners)
- Go 1.21+ (best performance, excellent stdlib for networking)
- Rust 1.70+ (memory safety, hardest learning curve)
- C with gcc/clang (maximum control, requires manual memory management)
- openssl command-line tool (for TLS certificate inspection)
- curl (for testing HTTP servers)
- nc (netcat) (for testing TCP connections)
- dig or nslookup (for DNS testing)
Recommended Tools:
- Wireshark or tcpdump (packet capture and analysis) — Essential for Projects 3, 5, 7
- docker and docker-compose (for running test services)
- hexdump or xxd (for viewing binary data)
- strace (for tracing system calls)
- nghttp2 client (
nghttp) for testing HTTP/2 servers - A text editor with hex viewing (VS Code with “Hex Editor” extension, vim with xxd)
For Specific Projects:
- Project 3 (TLS):
openssl s_client,openssl x509 - Project 4 (WebSocket): A browser with DevTools (Chrome/Firefox)
- Project 5 (HTTP/2):
nghttpclient,h2loadbenchmarking tool - Project 6 (CDN):
ab(Apache Bench) orwrkfor load testing - Project 7 (QUIC/HTTP/3):
quicheclient orcurlwith HTTP/3 support
Testing Your Setup:
# Verify core tools
$ which curl openssl dig nc tcpdump
/usr/bin/curl
/usr/bin/openssl
/usr/bin/dig
/usr/bin/nc
/usr/sbin/tcpdump
# Test Python (if using Python)
$ python3 --version
Python 3.11.5
$ python3 -c "import socket; print('Sockets work!')"
Sockets work!
# Test packet capture permissions
$ sudo tcpdump -i lo -c 1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
# Test OpenSSL
$ openssl version
OpenSSL 3.0.11 19 Sep 2023
# Test that you can bind to ports
$ nc -l 8080 &
$ lsof -i :8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 1234 user 3u IPv4 12345 0t0 TCP *:8080 (LISTEN)
$ kill %1
If all commands work, you’re ready!
Time Investment
Realistic time estimates for each project:
| Project | Minimum (experienced) | Realistic (learning) |
|---|---|---|
| 1. DNS Resolver | 3-4 days | 1 week |
| 2. HTTP/1.1 Server | 4-5 days | 1-2 weeks |
| 3. TLS Handshake Explorer | 2-3 days | 1 week |
| 4. WebSocket Chat | 3-4 days | 1 week |
| 5. HTTP/2 Parser | 5-7 days | 2 weeks |
| 6. Mini-CDN | 5-7 days | 2 weeks |
| 7. QUIC/HTTP/3 Client | 4-5 days | 1-2 weeks |
| Total | 4-6 weeks | 8-12 weeks |
Important: These are active coding hours. If you’re working on this part-time (5-10 hours/week), expect 4-6 months.
Important Reality Check
This is hard. You will:
- ✅ Get stuck parsing binary formats and spend hours debugging “off by one” errors
- ✅ Read RFCs that feel like legal documents
- ✅ Capture packets in Wireshark and not understand what you’re seeing
- ✅ Implement a feature, test it, and watch it fail in mysterious ways
- ✅ Question whether you really need to understand this deeply
But when you finish, you’ll be the engineer who:
- ❌ Doesn’t panic when someone says “the DNS is broken”
- ❌ Can debug production TLS certificate issues in minutes
- ❌ Understands why HTTP/2 multiplexing actually improves performance
- ❌ Can read a Wireshark capture and spot the bottleneck
- ❌ Can confidently answer “what happens when you type google.com” in an interview
This is the difference between using the internet and understanding it.
Quick Start: Your First 48 Hours
Feeling overwhelmed by 7 projects? Start here. This is the absolute minimum viable path to get momentum.
Day 1: DNS (4-6 hours)
Goal: Understand one binary protocol from scratch.
What to do:
- Read RFC 1035 Section 4 (DNS Message Format) — just the header and question section
- Code: Write a function that constructs a single DNS query packet for
google.comA record - Send it: Use UDP socket to send to
8.8.8.8:53and print the raw bytes you get back - Debug: Use
tcpdumpto capture your query and compare it to whatdigsends
Success: You sent a UDP packet and got a response. You understand what “binary protocol” means.
Example checkpoint:
import socket
# Construct DNS query for google.com A record
query = b'\x00\x01' # Transaction ID
query += b'\x01\x00' # Flags: standard query
query += b'\x00\x01' # Questions: 1
query += b'\x00\x00' * 3 # Answer/Authority/Additional: 0
# ... (encode domain name)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(query, ('8.8.8.8', 53))
response, _ = sock.recvfrom(512)
print(f"Got {len(response)} bytes back!")
Day 2: HTTP (4-6 hours)
Goal: Build the simplest possible HTTP/1.1 server.
What to do:
- Read RFC 2616 Section 5 (Request) and Section 6 (Response) — skim it
- Code: TCP server that accepts connections and parses the request line (
GET / HTTP/1.1) - Respond: Return
HTTP/1.1 200 OKwith a single HTML page - Test:
curl http://localhost:8080and see your page
Success: You served HTTP without a framework. You understand what “request/response” means at the byte level.
Example checkpoint:
$ curl -v http://localhost:8080
* Connected to localhost (127.0.0.1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 13
<
Hello, World!
After 48 Hours
If you completed both: You have the core skills. Now pick your path:
- Want depth? → Do all 7 projects in order
- Want breadth? → Do Project 3 (TLS), then Project 7 (QUIC)
- Want to ship? → Do Project 6 (CDN) and deploy it
If you got stuck: That’s normal! Post your code in a gist and ask for help. The first 2 projects are the hardest conceptually.
Recommended Learning Paths
Not everyone learns the same way. Choose the path that matches your background and goals:
Path 1: “The Complete Foundation” (Recommended for Career Changers)
Best for: People new to networking or systems programming
Order: 1 → 2 → 3 → 4 → 5 → 6 → 7
Why: This builds knowledge incrementally. DNS (1) teaches binary parsing. HTTP/1.1 (2) teaches text protocols. TLS (3) adds security. WebSocket (4) adds real-time. HTTP/2 (5) adds binary framing. CDN (6) integrates everything. QUIC (7) shows the future.
Time: 10-12 weeks part-time
Key insight: By the time you reach Project 7, you’ll understand why QUIC exists and what problems it solves.
Path 2: “The Security-First Engineer” (For Security/DevSecOps Roles)
Best for: People who need to understand TLS, certificates, and secure communication
Order: 3 → 2 → 1 → 6 → 7 → 4 → 5
Why: Start with TLS (3) to understand encryption and certificates. Then build HTTP/1.1 (2) to see how HTTPS works. Add DNS (1) to understand name resolution attacks. Build a CDN (6) to understand proxy TLS termination. QUIC (7) shows TLS 1.3 integration. WebSocket (4) and HTTP/2 (5) are optional depth.
Time: 8-10 weeks part-time
Key insight: You’ll be able to debug production TLS issues, understand certificate pinning, and explain PKI to your team.
Path 3: “The Performance Engineer” (For Backend/Infra Roles)
Best for: People optimizing web performance, CDNs, or building high-throughput systems
Order: 2 → 5 → 6 → 7 → 1 → 3 → 4
Why: Start with HTTP/1.1 (2) baseline. Jump to HTTP/2 (5) to understand multiplexing and header compression. Build a CDN (6) to see caching strategies. Add QUIC (7) to understand 0-RTT. Fill gaps with DNS (1), TLS (3), and WebSocket (4).
Time: 9-11 weeks part-time
Key insight: You’ll understand why Cloudflare and Fastly architecture decisions matter, and how to benchmark protocol performance.
Path 4: “The Full-Stack Developer” (For Pragmatic Builders)
Best for: People who want to build production-ready applications
Order: 2 → 4 → 6 → 3 → 5 → 7 → 1
Why: Build a working HTTP/1.1 server (2) first. Add WebSocket (4) for real-time features. Deploy a CDN (6) to understand caching. Learn TLS (3) for security. Optimize with HTTP/2 (5) and QUIC (7). DNS (1) is last since you’ll use cloud DNS services.
Time: 8-10 weeks part-time
Key insight: You’ll know exactly how your framework (Express, Flask, FastAPI) works under the hood, and when to bypass it.
Path 5: “The Interview Prep Sprint” (For Job Seekers)
Best for: People preparing for systems design or networking interviews
Order: 1 → 2 → 3 → 6 → (read 5, 7 without coding)
Why: DNS (1) answers “what happens when you type google.com”. HTTP/1.1 (2) is fundamental. TLS (3) is asked in every security question. CDN (6) is a classic design question. Read about HTTP/2 (5) and QUIC (7) to discuss modern protocols.
Time: 4-6 weeks part-time
Key insight: You’ll confidently answer “design a URL shortener”, “design a CDN”, and “explain HTTPS handshake” questions.
Path 6: “The Depth-First Explorer” (For Researchers/Academics)
Best for: People who want to deeply understand one protocol family
Order: Pick ONE: (1 + 2 + 6) OR (3) OR (5 + 7)
Why:
- DNS cluster (1 + 2 + 6): Build a recursive resolver, HTTP server, and caching CDN to understand request routing
- TLS deep-dive (3): Spend 3-4 weeks on TLS alone, implement handshake parsing, certificate validation, cipher suite negotiation
- HTTP evolution (5 + 7): Compare HTTP/2 vs HTTP/3 side-by-side, implement both parsers
Time: 6-8 weeks on one cluster
Key insight: You’ll become the go-to expert for that specific protocol at your company.
How to Choose Your Path
Ask yourself:
-
“Do I have networking fundamentals?” → Yes: Path 3, 4, or 5 No: Path 1 or 2 - “Am I preparing for interviews?” → Path 5 (sprint through the classics)
- “Do I work in security/DevSecOps?” → Path 2 (TLS-first)
- “Do I work in infrastructure/SRE?” → Path 3 (performance-first)
- “Do I just want to understand the web?” → Path 1 (complete foundation)
- “Do I want to specialize?” → Path 6 (depth-first)
Still unsure? Default to Path 1. It’s designed to build knowledge incrementally without gaps.
Concept Summary Table
| Concept Cluster | What You Need to Internalize |
|---|---|
| DNS Wire Format | Every DNS query is a binary packet with fixed offsets for ID, flags, and record counts. |
| HTTP State Machines | Requests and responses are governed by strict parsing rules and connection lifecycles. |
| Multiplexing | How to slice multiple streams into frames and reassemble them without mixing data. |
| TLS Handshake | The exact byte-level sequence that establishes an encrypted tunnel before data flows. |
| QUIC Streams | Why moving reliability to UDP solves TCP head-of-line blocking. |
| WebSocket Framing | How upgrade, masking, and frame boundaries turn HTTP into a full-duplex channel. |
| Caching and CDNs | How cache keys, freshness, and edge placement shape latency and cost. |
Deep Dive Reading by Concept
This section maps each concept to specific book chapters. Read these before or alongside the projects.
Protocol Fundamentals
| Concept | Book & Chapter |
|---|---|
| DNS Internals | TCP/IP Illustrated, Vol 1 by W. Richard Stevens - Ch. 11: “DNS: Domain Name System” |
| HTTP/1.1 Basics | HTTP: The Definitive Guide by David Gourley - Ch. 3: “HTTP Messages” |
| HTTP/2 and QUIC | High Performance Browser Networking by Ilya Grigorik - Ch. 12 (HTTP/2) and Ch. 13 (QUIC) |
| TLS/SSL | Bulletproof TLS and PKI by Ivan Ristic - Ch. 1: “SSL/TLS and Cryptography” |
| WebSockets | High Performance Browser Networking by Ilya Grigorik - Ch. 14: “WebSockets” |
| Caching and CDNs | High Performance Browser Networking by Ilya Grigorik - Ch. 6: “Storage and Caching” |
Essential Reading Order
- Foundation (The Basics):
- HTTP: The Definitive Guide Ch. 1-3 (HTTP Messages and Connections)
- TCP/IP Illustrated Ch. 11 (DNS)
- Security and Real-time:
- Bulletproof TLS and PKI Ch. 1-2
- High Performance Browser Networking Ch. 14 (WebSockets)
- Modern Performance:
- High Performance Browser Networking Ch. 12-13 (HTTP/2 and HTTP/3)
Project 1: Recursive DNS Resolver (From Root to Record)
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Python
- Alternative Programming Languages: Go, Rust, C
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: DNS / Binary Protocols / Distributed Systems
- Software or Tool: UDP Sockets / Wire Format Parsing
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A DNS resolver that starts with the root servers (a.root-servers.net, etc.) and recursively queries its way down through TLD servers (.com, .org) to authoritative name servers, ultimately finding the IP address for any domain—without ever touching 8.8.8.8 or your ISP’s resolver.
Why it teaches Networking: DNS is the first thing that happens in nearly every network interaction. By building a recursive resolver, you’ll understand the hierarchical structure of the internet’s naming system, learn to parse binary wire formats, and see how distributed systems achieve global consensus through delegation.
Core challenges you’ll face:
- Parsing the DNS Wire Format → maps to understanding binary protocol encoding (RFC 1035)
- Name Compression Pointers → maps to the 0xC0 pointer mechanism that references earlier parts of the message
- Following the Referral Chain → maps to understanding delegation and glue records
- Handling CNAME Chains → maps to resolving aliases before the final A/AAAA record
- UDP vs TCP Fallback → maps to understanding truncation and when TCP is required
Key Concepts:
- DNS Message Format: RFC 1035 Section 4 - The 12-byte header and variable sections
- Resource Record Types: TCP/IP Illustrated Ch. 11 - A, AAAA, NS, CNAME, MX, SOA
- Delegation & Glue Records: DNS and BIND Ch. 2 - Albitz & Liu
- Name Compression: RFC 1035 Section 4.1.4 - Pointer mechanism
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Basic Python/Go, understanding of UDP sockets, familiarity with binary/hex representation
Real World Outcome
You’ll have a command-line tool that resolves any domain name by walking the DNS hierarchy from the root servers down. When you run it, you’ll see the complete delegation chain—something even dig doesn’t show by default.
Example Output:
$ ./dns_resolver google.com
[QUERY] Starting resolution for: google.com
[ROOT] Querying a.root-servers.net (198.41.0.4)
[ROOT] Received referral to .com TLD servers
└── NS: a.gtld-servers.net (192.5.6.30)
└── NS: b.gtld-servers.net (192.33.14.30)
[TLD] Querying a.gtld-servers.net for google.com
[TLD] Received referral to google.com authoritative servers
└── NS: ns1.google.com (216.239.32.10)
└── NS: ns2.google.com (216.239.34.10)
[AUTH] Querying ns1.google.com for google.com A record
[AUTH] ✓ Received authoritative answer!
┌─────────────────────────────────────────────┐
│ FINAL ANSWER │
│ google.com → 142.250.80.46 │
│ TTL: 300 seconds │
│ Total queries: 3 │
│ Resolution time: 127ms │
└─────────────────────────────────────────────┘
DNS Resolution Path Visualization:
┌─────────────────┐
│ Root (.) │ "I don't know google.com,
│ 13 root servers │ but .com is handled by..."
└────────┬────────┘
│ NS referral
▼
┌─────────────────┐
│ TLD (.com) │ "I don't know google.com,
│ gtld-servers │ but google.com NS is..."
└────────┬────────┘
│ NS referral
▼
┌─────────────────┐
│ Authoritative │ "Yes! google.com is
│ ns1.google.com │ 142.250.80.46"
└────────┬────────┘
│ A record
▼
┌─────────────────┐
│ ANSWER │
│ 142.250.80.46 │
└─────────────────┘
The Core Question You’re Answering
“How does the internet know where anything is, and why don’t we need a central database of all domain names?”
Before you write any code, sit with this question. The answer reveals one of the most elegant distributed systems ever designed. DNS works through delegation—each level only needs to know about the next level down. The root servers don’t store every domain; they just know who’s responsible for .com, .org, .net, etc. This hierarchical delegation is why DNS scales to billions of records without a central bottleneck.
Concepts You Must Understand First
Stop and research these before coding:
- The DNS Hierarchy
- What are the 13 root server addresses, and why are there exactly 13?
- What’s the difference between a TLD server and an authoritative server?
- What is a “zone” and how does it differ from a “domain”?
- Book Reference: “TCP/IP Illustrated, Vol 1” Ch. 11 - W. Richard Stevens
- DNS Message Binary Format
- What are the 6 fields in the DNS header, and what does each do?
- How are domain names encoded (length-prefixed labels)?
- What is the compression pointer (0xC0) and why was it needed?
- Book Reference: RFC 1035 Section 4
- Record Types and Their Purposes
- What’s the difference between NS, A, AAAA, and CNAME records?
- What are “glue records” and when are they necessary?
- How does MX priority work?
- Book Reference: “DNS and BIND” Ch. 4 - Albitz & Liu
- UDP and DNS
- Why does DNS primarily use UDP instead of TCP?
- When does DNS fall back to TCP (truncation)?
- What port does DNS use?
- Book Reference: “TCP/IP Illustrated, Vol 1” Ch. 11
Questions to Guide Your Design
Before implementing, think through these:
- Message Construction
- How will you generate unique transaction IDs?
- How will you encode a domain name like “www.example.com” into bytes?
- What flags should you set for a recursive query vs an iterative query?
- Response Parsing
- How will you handle compression pointers that reference earlier parts of the message?
- How will you differentiate between an answer and a referral?
- What happens if the Answer section is empty but Authority section has NS records?
- Recursion Logic
- How will you follow NS referrals to the next server?
- What if the NS record has no glue (no accompanying A record)?
- How will you handle CNAME chains (alias → alias → final)?
- Error Handling
- What happens if a server doesn’t respond (timeout)?
- What if you get NXDOMAIN (domain doesn’t exist)?
- How do you handle SERVFAIL responses?
Thinking Exercise
Trace a Resolution by Hand
Before coding, manually trace the resolution of mail.google.com:
- Start at root: What query would you send to 198.41.0.4?
- Root response: What NS records would you expect? Would there be glue records?
- Query TLD: Which server would you query next? What would you ask?
- TLD response: What NS records point to google.com’s servers?
- Query authoritative: What’s the final query?
- Handle CNAME: What if
mail.google.comis a CNAME togooglemail.l.google.com?
Draw the packet structure for step 1:
Header (12 bytes):
ID: ???? (your random ID)
Flags: ???? (what bits for a standard query?)
QDCOUNT: ?
ANCOUNT: ?
NSCOUNT: ?
ARCOUNT: ?
Question Section:
Name: How do you encode "com"?
Type: What type for a referral query?
Class: IN (0x0001)
The Interview Questions They’ll Ask
Prepare to answer these:
- “Walk me through what happens when I type google.com in my browser, starting from DNS.”
- “Why are there exactly 13 root server addresses, and how do they handle the load?”
- “What’s the difference between an authoritative answer and a cached answer?”
- “How does DNS handle load balancing? What about geographic routing?”
- “What security vulnerabilities exist in DNS, and how does DNSSEC address them?”
- “What’s DNS amplification attack and why is UDP problematic for it?”
- “Explain how DNS caching works at different levels (browser, OS, ISP).”
- “What happens when a DNS record’s TTL expires?”
Hints in Layers
Hint 1: Starting Point Begin with a hardcoded list of root server IPs. Send a query for the final domain (not just “com”) to a root server. The root will respond with NS records for the TLD.
Hint 2: Parsing Responses Look at the Answer Count (ANCOUNT) first. If it’s 0 but NSCOUNT > 0, you got a referral. The NS records in the Authority section tell you where to go next. Check the Additional section for glue records (A records for those NS servers).
Hint 3: Following Referrals
function resolve(domain, server):
response = query(server, domain)
if response has answer:
return response.answer
else if response has NS referral:
next_server = get_glue_or_resolve_ns(response)
return resolve(domain, next_server)
Hint 4: Name Compression When you see a byte >= 0xC0, the lower 14 bits are an offset into the message. Jump to that offset, read the name there, then continue from where you were. Watch out for nested pointers!
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| DNS Protocol Overview | “TCP/IP Illustrated, Vol 1” by Stevens | Ch. 11 |
| DNS Message Format | RFC 1035 | Section 4 |
| Practical DNS Operations | “DNS and BIND” by Albitz & Liu | Ch. 1-4 |
| DNS Security (DNSSEC) | “DNS Security” by Afilias | Ch. 3-5 |
| Network Programming | “Unix Network Programming” by Stevens | Ch. 8 (UDP) |
Common Pitfalls & Debugging
When building a DNS resolver, you’ll likely hit these issues. Here’s how to diagnose and fix them:
Problem 1: “My query returns no answer section, just authority section with NS records”
Symptom:
$ python dns_resolver.py example.com
[ROOT] Query sent to 198.41.0.4
[ROOT] Response: ANCOUNT=0, NSCOUNT=13, ARCOUNT=13
No answer! Got NS referrals for .com
-
Why: This is not a problem—this is how DNS works. The root servers don’t store every domain. They’re telling you “I don’t know example.com, but .com is handled by a.gtld-servers.net.” This is a referral, not an answer.
- Fix: Your code needs to:
- Extract NS records from the Authority Section (not Answer Section)
- Look for glue records (A/AAAA records in the Additional Section) that give you IP addresses for those NS records
- Query one of those NS servers next
- Quick test:
# Manually see what root returns $ dig @a.root-servers.net google.com +norecurse ;; AUTHORITY SECTION: com. 172800 IN NS a.gtld-servers.net. # This is a referral, not an error!
Problem 2: “Pointer decompression crashes with index out of bounds”
Symptom:
Exception in decompress_name:
offset = struct.unpack('!H', data[pos:pos+2])[0] & 0x3FFF
IndexError: index out of range
-
Why: DNS uses name compression pointers (
0xC0XX) to avoid repeating domain names in the same message. The pointer0xC00Cmeans “jump to offset 12 and read the name there.” If your offset is wrong or points beyond the packet size, you crash. - Fix: Add bounds checking:
def decompress_name(data, pos): labels = [] jumped = False original_pos = pos while pos < len(data): # <--- Bounds check length = data[pos] if (length & 0xC0) == 0xC0: # Compression pointer if pos + 1 >= len(data): # <--- Check before reading 2 bytes raise ValueError(f"Incomplete pointer at {pos}") ptr_offset = struct.unpack('!H', data[pos:pos+2])[0] & 0x3FFF if ptr_offset >= len(data): # <--- Check pointer validity raise ValueError(f"Pointer {ptr_offset} beyond message size {len(data)}") if not jumped: original_pos = pos + 2 pos = ptr_offset jumped = True elif length == 0: break else: pos += 1 if pos + length > len(data): # <--- Check label length raise ValueError(f"Label extends beyond message") labels.append(data[pos:pos+length].decode()) pos += length return '.'.join(labels), original_pos if jumped else pos + 1 - Quick test:
# Capture a real DNS response and inspect in hex $ dig @8.8.8.8 google.com > /dev/null $ sudo tcpdump -i any port 53 -w dns_capture.pcap -c 2 $ tcpdump -r dns_capture.pcap -x # Look for 0xc0XX bytes in the response — those are pointers!
Problem 3: “I get SERVFAIL or empty response from TLD servers”
Symptom:
$ python dns_resolver.py example.com
[ROOT] Query sent to 198.41.0.4 → Success
[TLD] Query sent to a.gtld-servers.net (192.5.6.30) → SERVFAIL
-
Why: You’re likely sending the wrong question. TLD servers expect you to ask for
example.com, not.com. Or you might be setting the wrong flags (asking for recursion when you should ask iteratively). - Fix: Ensure your query construction is correct:
# WRONG: asking for .com at the TLD server query = build_query(".com", QTYPE_NS) # RIGHT: asking for example.com at the TLD server query = build_query("example.com", QTYPE_A)Also check your flags:
# For iterative queries (what you want): flags = 0x0100 # Standard query, NO recursion desired (RD=0) # NOT: flags = 0x0120 # This asks for recursion, which TLD servers might reject - Quick test:
# See what dig sends in iterative mode $ dig @a.gtld-servers.net google.com +norecurse +trace
Problem 4: “CNAME chains resolve to the wrong IP or loop forever”
Symptom:
$ python dns_resolver.py www.example.com
[AUTH] Got CNAME: www.example.com → cdn.example.net
[AUTH] Querying cdn.example.net... CNAME → cdn2.fastly.net
[AUTH] Querying cdn2.fastly.net... CNAME → cdn.example.net
[ERROR] CNAME loop detected!
-
Why: CNAMEs are aliases. When you query
www.example.com, you might get a CNAME pointing tocdn.example.net. You must then query forcdn.example.netto get the final A record. Some domains have CNAME chains of 2-3 hops. If you don’t handle this, you’ll never get an IP address. - Fix: Implement CNAME following logic with loop detection:
def resolve_with_cname(name, max_hops=10): seen = set() current = name for hop in range(max_hops): if current in seen: raise ValueError(f"CNAME loop detected: {current}") seen.add(current) response = query_dns(current, QTYPE_A) # Check answer section for CNAME or A record for rr in response.answers: if rr.type == QTYPE_CNAME: print(f"[CNAME] {current} → {rr.data}") current = rr.data # Follow the CNAME break elif rr.type == QTYPE_A: return rr.data # Got the IP! else: raise ValueError(f"No CNAME or A record found for {current}") raise ValueError(f"Too many CNAME hops (>{max_hops})") - Quick test:
# See CNAME chains in action $ dig www.github.com +noall +answer www.github.com. 60 IN CNAME github.com. github.com. 60 IN A 140.82.121.3 # Or a longer chain: $ dig www.cnn.com +noall +answer
Problem 5: “DNS works for small domains but fails for long ones”
Symptom:
$ python dns_resolver.py google.com # Works
$ python dns_resolver.py really.long.subdomain.example.com # Fails
socket.error: [Errno 90] Message too long
-
Why: DNS uses UDP, which has a 512-byte limit for queries/responses (RFC 1035). If your query or the response exceeds this, it gets truncated. The TC (Truncated) flag will be set, and you must retry over TCP.
- Fix: Check the TC flag and fall back to TCP:
def query_dns(domain, server): # Try UDP first sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.sendto(build_query(domain), (server, 53)) response, _ = sock.recvfrom(512) # Check TC flag (bit 9 of flags field) flags = struct.unpack('!H', response[2:4])[0] if flags & 0x0200: # TC flag set print("[TCP] Response truncated, retrying over TCP...") sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_tcp.connect((server, 53)) # TCP DNS prepends message with 2-byte length query = build_query(domain) sock_tcp.sendall(struct.pack('!H', len(query)) + query) length_bytes = sock_tcp.recv(2) length = struct.unpack('!H', length_bytes)[0] response = sock_tcp.recv(length) sock_tcp.close() return response - Quick test:
# Force TCP mode with dig $ dig @8.8.8.8 google.com +tcp
Problem 6: “Glue records are missing, can’t find authoritative server IP”
Symptom:
[TLD] Got NS: ns1.example.com, ns2.example.com
[ERROR] No A records for ns1.example.com in Additional section!
Can't query authoritative server.
-
Why: Sometimes TLD servers return NS records like
ns1.example.combut don’t include the IP address (glue record) forns1.example.comin the Additional section. This creates a circular dependency: you need to resolvens1.example.comto queryexample.com, butns1.example.comis underexample.com! - Fix: Recursively resolve the NS hostname:
def get_authoritative_servers(domain, tld_server): response = query_dns(domain, tld_server) ns_names = extract_ns_records(response) # e.g., ["ns1.example.com"] glue_ips = extract_additional_records(response) # e.g., {"ns1.example.com": "1.2.3.4"} if ns_names[0] in glue_ips: return glue_ips[ns_names[0]] # Use glue record else: # No glue! Must resolve NS hostname separately print(f"[GLUE] No glue for {ns_names[0]}, resolving it...") return resolve_recursive(ns_names[0]) # Resolve the NS hostname - Quick test:
# Check if glue records are present $ dig @a.gtld-servers.net example.com +norecurse ;; AUTHORITY SECTION: example.com. 172800 IN NS ns1.example.com. ;; ADDITIONAL SECTION: ns1.example.com. 172800 IN A 93.184.216.34 <-- This is the glue record
Debugging Tools Checklist
When stuck, use these tools:
- tcpdump / Wireshark:
$ sudo tcpdump -i any port 53 -w dns.pcap # Open dns.pcap in Wireshark to see exact bytes - dig with trace mode:
$ dig +trace google.com # Shows the full resolution path - Compare with dig’s query:
$ dig google.com +norecurse +qr # +qr shows both query and response - Hex dump your packet:
print(' '.join(f'{b:02x}' for b in query)) # Compare to dig's output byte-by-byte - Online DNS query visualizer:
- https://messwithdns.net/ — Julia Evans’ interactive DNS tool
- https://dnschecker.org/ — Check global DNS propagation
Project 2: HTTP/1.1 Server from Scratch (The Foundation)
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, Go, Zig
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: HTTP / TCP Sockets / State Machines
- Software or Tool: POSIX Sockets / epoll/kqueue
- Main Book: “HTTP: The Definitive Guide” by David Gourley & Brian Totty
What you’ll build: A compliant HTTP/1.1 server that handles persistent connections, chunked transfer encoding, proper header parsing, and serves static files—all without using any HTTP library. Just raw TCP sockets and your own parser.
Why it teaches Networking: HTTP/1.1 is the Rosetta Stone of the application layer. Every web developer uses it daily, but few understand its actual wire format. Building a server from scratch teaches you about request parsing, state machines, connection management, and the realities of handling malformed input.
Core challenges you’ll face:
- Request Line Parsing → maps to handling variable-length lines, method detection, URI parsing
- Header Parsing with Edge Cases → maps to multi-line headers, case insensitivity, LWS folding
- Persistent Connections (Keep-Alive) → maps to knowing when a request ends and another begins
- Chunked Transfer Encoding → maps to the streaming body format for dynamic content
- Content-Length vs Chunked → maps to two different ways to delimit message bodies
- Error Recovery → maps to handling malformed requests without crashing
Key Concepts:
- HTTP Message Format: RFC 7230 Sections 3-4 - Request/Response syntax
- Persistent Connections: RFC 7230 Section 6.3 - Keep-Alive behavior
- Chunked Transfer: RFC 7230 Section 4.1 - Streaming body encoding
- Status Codes: RFC 7231 Section 6 - Response semantics
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: C programming, TCP socket basics (socket/bind/listen/accept), understanding of blocking I/O
Real World Outcome
You’ll have a web server that can serve static files to any browser. When you run it and navigate to http://localhost:8080, you’ll see your HTML page load—served entirely by code you wrote.
Example Output:
$ ./http_server --port 8080 --root ./public
[INFO] HTTP/1.1 Server starting on port 8080
[INFO] Document root: ./public
[CONN] New connection from 127.0.0.1:52341
[REQ] GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: text/html
Connection: keep-alive
[RESP] 200 OK (text/html, 1247 bytes)
[REQ] GET /style.css HTTP/1.1
Host: localhost:8080
Connection: keep-alive
[RESP] 200 OK (text/css, 892 bytes)
[REQ] GET /favicon.ico HTTP/1.1
[RESP] 404 Not Found
[CONN] Connection closed after 3 requests (keep-alive)
--- Server Statistics ---
Total connections: 1
Total requests: 3
Bytes sent: 2,139
Uptime: 5.2s
HTTP Request/Response Flow:
┌──────────────────────────────────────────────────────────┐
│ TCP Connection │
├──────────────────────────────────────────────────────────┤
│ │
│ Request 1: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ GET /index.html HTTP/1.1\r\n │ │
│ │ Host: localhost:8080\r\n │ │
│ │ Connection: keep-alive\r\n │ │
│ │ \r\n │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Response 1: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ HTTP/1.1 200 OK\r\n │ │
│ │ Content-Type: text/html\r\n │ │
│ │ Content-Length: 1247\r\n │ │
│ │ Connection: keep-alive\r\n │ │
│ │ \r\n │ │
│ │ <!DOCTYPE html>... │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ Request 2: (same connection, no new handshake) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ GET /style.css HTTP/1.1\r\n │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
The Core Question You’re Answering
“What actually happens between my browser sending ‘GET /’ and receiving an HTML page? What’s in those bytes on the wire?”
Before you write any code, sit with this question. HTTP looks simple—it’s text-based and human-readable. But the devil is in the details: How do you know when the headers end? How do you know when the body ends? What if someone sends you 10GB of headers? These edge cases are what separate a toy server from a robust one.
Concepts You Must Understand First
Stop and research these before coding:
- TCP Socket Programming
- What’s the difference between a listening socket and a connected socket?
- What does
accept()return and why? - What’s the difference between
recv()returning 0 vs -1? - Book Reference: “Unix Network Programming, Vol 1” Ch. 4-5 - W. Richard Stevens
- HTTP Message Structure
- What separates the request line from headers? Headers from body?
- What’s the exact format of a request line (method SP URI SP version)?
- How are headers case-insensitive but values case-sensitive?
- Book Reference: “HTTP: The Definitive Guide” Ch. 3 - Gourley & Totty
- Request Parsing State Machine
- What states does your parser need (reading line, reading headers, reading body)?
- How do you handle partial reads (data split across multiple recv() calls)?
- What’s the maximum header size you should accept before rejecting?
- Book Reference: RFC 7230 Section 3
- Persistent Connections
- What’s the default connection behavior in HTTP/1.1 vs 1.0?
- When should you close the connection vs keep it open?
- How do you handle pipelining (multiple requests before any response)?
- Book Reference: RFC 7230 Section 6
Questions to Guide Your Design
Before implementing, think through these:
- Buffer Management
- How will you handle a request that arrives in multiple TCP segments?
- What if the headers span multiple
recv()calls? - How big should your receive buffer be?
- Parsing Strategy
- Will you read line-by-line or parse byte-by-byte?
- How will you handle the CRLF line endings robustly?
- What happens if someone sends
GET /\r\nHost: evil(malformed)?
- Response Generation
- How will you determine Content-Type from file extension?
- How will you handle files larger than available memory?
- Should you buffer the whole response or stream it?
- Concurrency Model
- Will you use one process per connection? One thread? Event loop?
- How will you handle slow clients (slow loris attack)?
- What limits will you enforce (max connections, timeouts)?
Thinking Exercise
Parse a Request by Hand
Given this raw HTTP request (note: \r\n shown explicitly):
GET /api/users?id=42 HTTP/1.1\r\n
Host: api.example.com\r\n
Accept: application/json\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
\r\n
Answer these questions:
- What are the exact byte positions of: method start, method end, URI start, URI end?
- How many headers are there? List each name and value.
- At what byte offset does the message body begin?
- Given
Connection: keep-alive, what should happen after you send your response? - If the next request starts with
POST, how will you know when its body ends?
The Interview Questions They’ll Ask
Prepare to answer these:
- “Explain the differences between HTTP/1.0 and HTTP/1.1.”
- “How does keep-alive work, and what are its performance implications?”
- “What is chunked transfer encoding and when would you use it?”
- “How would you handle a slow client trying to DoS your server?”
- “What’s the difference between Content-Length and Transfer-Encoding: chunked?”
- “How do you prevent buffer overflow attacks in an HTTP parser?”
- “Explain HTTP pipelining and why it’s rarely used.”
- “What status codes would you return for: file not found, bad request, server error?”
Hints in Layers
Hint 1: Basic Structure
Start with a simple loop: accept() → recv() until you see \r\n\r\n → parse request → send hardcoded response → close. Get this working before adding complexity.
Hint 2: State Machine Your parser needs at least these states:
READING_REQUEST_LINE → READING_HEADERS → READING_BODY → COMPLETE
Transition on seeing \r\n (request line done), \r\n\r\n (headers done), Content-Length bytes read (body done).
Hint 3: Keep-Alive Loop
while (connection_alive):
request = parse_request(socket)
response = handle_request(request)
send_response(socket, response)
if request.headers["Connection"] == "close":
break
Hint 4: Content-Type Mapping Create a simple lookup table:
.html → text/html
.css → text/css
.js → application/javascript
.png → image/png
* → application/octet-stream
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| HTTP Message Format | “HTTP: The Definitive Guide” | Ch. 3-4 |
| TCP Socket Programming | “Unix Network Programming, Vol 1” | Ch. 4-6 |
| HTTP Connection Management | RFC 7230 | Section 6 |
| Building Parsers | “Crafting Interpreters” by Nystrom | Ch. 4 (Scanning) |
| C Systems Programming | “The Linux Programming Interface” | Ch. 56-61 |
Common Pitfalls & Debugging
Building an HTTP/1.1 server from scratch surfaces many subtle protocol details. Here’s how to handle common issues:
Problem 1: “Browser shows ‘Connection Reset’ or hangs forever”
Symptom:
$ curl http://localhost:8080
curl: (52) Empty reply from server
# Or browser spins infinitely
-
Why: Your server accepted the connection but didn’t send back a proper HTTP response. Either you’re not writing to the socket, closing it prematurely, or sending malformed HTTP.
- Fix: Ensure you’re sending the complete HTTP response with proper line endings:
const char *response = "HTTP/1.1 200 OK\r\n" // Status line "Content-Type: text/html\r\n" // Headers "Content-Length: 13\r\n" "\r\n" // BLANK LINE (crucial!) "Hello, World!"; // Body write(client_fd, response, strlen(response)); - Key points:
- MUST use
\r\n(CRLF), not\n(LF only). HTTP spec requires CRLF. - MUST have blank line (
\r\n\r\n) between headers and body - Content-Length must match actual body size
- MUST use
- Quick test:
```bash
See raw HTTP response
$ curl -v http://localhost:8080
- Connected to localhost (127.0.0.1) port 8080
GET / HTTP/1.1
< HTTP/1.1 200 OK <– Your response must look like this < Content-Type: text/html < Content-Length: 13 < Hello, World! ```
- Connected to localhost (127.0.0.1) port 8080
Problem 2: “Request parsing fails on real browsers but works with curl”
Symptom:
// Your parser works for:
GET / HTTP/1.1\r\n\r\n
// But fails for real browser requests:
GET / HTTP/1.1\r\n
Host: localhost:8080\r\n
User-Agent: Mozilla/5.0...\r\n
Accept: text/html...\r\n
\r\n
-
Why: Real browsers send many headers. Your parser might be looking for
\r\n\r\nimmediately after the request line, but there are headers in between. - Fix: Parse line-by-line until you hit the blank line:
char buffer[4096]; int n = read(client_fd, buffer, sizeof(buffer)); buffer[n] = '\0'; // Parse request line char *line_end = strstr(buffer, "\r\n"); if (!line_end) { // Handle error } char request_line[256]; int len = line_end - buffer; memcpy(request_line, buffer, len); request_line[len] = '\0'; // Parse method, path, version char method[16], path[256], version[16]; sscanf(request_line, "%s %s %s", method, path, version); // Parse headers (skip for now, just find the blank line) char *body_start = strstr(buffer, "\r\n\r\n"); if (body_start) { body_start += 4; // Skip past the \r\n\r\n // Now body_start points to the request body (if any) } - Quick test:
# Send a request with headers $ printf "GET / HTTP/1.1\r\nHost: localhost\r\nUser-Agent: test\r\n\r\n" | nc localhost 8080
Problem 3: “Server handles one request then exits”
Symptom:
$ curl http://localhost:8080 # Works first time
$ curl http://localhost:8080 # Connection refused
$ ps aux | grep server # Server process is gone
-
Why: You’re calling
accept()once, handling one client, then exiting. You need an accept loop. - Fix: Add an infinite loop around
accept():int server_fd = socket(AF_INET, SOCK_STREAM, 0); // ... bind, listen ... while (1) { // <--- Accept loop int client_fd = accept(server_fd, NULL, NULL); if (client_fd < 0) { perror("accept failed"); continue; } // Handle request handle_request(client_fd); close(client_fd); // Close this client, loop for next one } - Quick test:
# Send multiple requests $ for i in {1..5}; do curl http://localhost:8080; done # All 5 should succeed
Problem 4: “Concurrent requests don’t work - server blocks”
Symptom:
# Terminal 1:
$ curl http://localhost:8080/slow # Takes 5 seconds
# Terminal 2 (while Terminal 1 is waiting):
$ curl http://localhost:8080/fast # Blocked! Waits for Terminal 1 to finish
-
Why: Your accept loop is handling one client at a time synchronously. While serving client 1, client 2 can’t even connect.
- Fix: Use
fork()or threads to handle clients concurrently:while (1) { int client_fd = accept(server_fd, NULL, NULL); if (fork() == 0) { // Child process close(server_fd); // Child doesn't need the listening socket handle_request(client_fd); close(client_fd); exit(0); } else { // Parent process close(client_fd); // Parent doesn't need the client socket } }Or with pthreads:
void *handle_client(void *arg) { int client_fd = *(int *)arg; free(arg); handle_request(client_fd); close(client_fd); return NULL; } while (1) { int client_fd = accept(server_fd, NULL, NULL); int *arg = malloc(sizeof(int)); *arg = client_fd; pthread_t thread; pthread_create(&thread, NULL, handle_client, arg); pthread_detach(thread); } - Quick test:
# Run in parallel $ curl http://localhost:8080 & curl http://localhost:8080 & # Both should return immediately
Problem 5: “POST requests lose data or get corrupted”
Symptom:
$ curl -X POST -d "key=value" http://localhost:8080
# Server receives: "key=" (truncated!)
-
Why: You’re reading a fixed buffer size (e.g., 4096 bytes) but the POST body might be larger, or split across multiple
read()calls. - Fix: Use
Content-Lengthheader to know how much to read:// Parse headers to find Content-Length char *cl_header = strstr(buffer, "Content-Length:"); int content_length = 0; if (cl_header) { sscanf(cl_header, "Content-Length: %d", &content_length); } // Read body char *body_start = strstr(buffer, "\r\n\r\n") + 4; int bytes_already_read = n - (body_start - buffer); char *body = malloc(content_length + 1); memcpy(body, body_start, bytes_already_read); int total_read = bytes_already_read; while (total_read < content_length) { int r = read(client_fd, body + total_read, content_length - total_read); if (r <= 0) break; total_read += r; } body[total_read] = '\0'; - Quick test:
# Send a large POST $ dd if=/dev/zero bs=1K count=10 | curl -X POST -d @- http://localhost:8080 # Server should receive all 10KB
Problem 6: “Segfault or crash on certain URLs”
Symptom:
$ curl http://localhost:8080/../../../../etc/passwd
Segmentation fault (core dumped)
-
Why: Path traversal vulnerability. Your code doesn’t sanitize the request path, allowing attackers to access files outside your document root.
- Fix: Canonicalize the path and check bounds:
#include <linux/limits.h> // For PATH_MAX #include <stdlib.h> // For realpath() char requested_path[PATH_MAX]; snprintf(requested_path, sizeof(requested_path), "/var/www/html%s", path); char canonical[PATH_MAX]; if (!realpath(requested_path, canonical)) { // Path doesn't exist or is malicious send_404(client_fd); return; } // Ensure canonical path is still under document root if (strncmp(canonical, "/var/www/html", 13) != 0) { send_403(client_fd); // Forbidden return; } // Now safe to open the file - Quick test:
# Try path traversal $ curl http://localhost:8080/../../../etc/passwd # Should return 403 Forbidden, NOT crash!
Debugging Tools Checklist
- nc (netcat) for manual requests:
$ nc localhost 8080 GET / HTTP/1.1 Host: localhost # Press Enter twice - should see HTTP response - strace to see system calls:
$ strace -e trace=read,write ./http_server # Shows every read() and write() call - tcpdump to capture HTTP traffic:
$ sudo tcpdump -i lo -A port 8080 # Shows ASCII HTTP messages - Valgrind for memory errors:
$ valgrind --leak-check=full ./http_server # Finds memory leaks and invalid reads/writes - curl verbose mode:
$ curl -v http://localhost:8080 # Shows request and response headers
Project 3: TLS 1.3 Handshake Explorer (The Security Layer)
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Go
- Alternative Programming Languages: Rust, Python
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 4: Expert
- Knowledge Area: TLS / Cryptography / PKI
- Software or Tool: TCP Sockets / Crypto Libraries
- Main Book: “Bulletproof TLS and PKI” by Ivan Ristić
What you’ll build: A TLS 1.3 client that performs the complete handshake, displaying and explaining every message exchanged—ClientHello, ServerHello, encrypted extensions, certificate verification, and the finished messages. You’ll see exactly how a secure connection is established.
Why it teaches Networking: TLS is the invisible armor that protects all modern internet traffic. By building a handshake explorer, you’ll understand key exchange (ECDHE), certificate chains, cipher suites, and why TLS 1.3 is faster and more secure than 1.2. This knowledge is essential for debugging HTTPS issues and understanding security architecture.
Core challenges you’ll face:
- Constructing the ClientHello → maps to understanding TLS extensions, cipher suites, and key shares
- Parsing the ServerHello → maps to extracting the selected cipher suite and server’s key share
- Key Derivation (HKDF) → maps to understanding how shared secrets become encryption keys
- Certificate Chain Verification → maps to PKI, trust anchors, and signature verification
- Encrypted Handshake Messages → maps to TLS 1.3’s unique encrypted extensions phase
- Finished Message (HMAC) → maps to verifying transcript integrity
Key Concepts:
- TLS 1.3 Handshake: RFC 8446 Section 2 - Full handshake flow
- Key Exchange (ECDHE): Understanding Diffie-Hellman with elliptic curves
- Certificate Validation: X.509 certificates and chain of trust
- AEAD Encryption: AES-GCM or ChaCha20-Poly1305 for record protection
Difficulty: Expert Time estimate: 3 weeks Prerequisites: Project 2 (TCP/HTTP basics), basic understanding of public key cryptography, familiarity with Go’s crypto libraries
Real World Outcome
You’ll have a tool that performs a TLS handshake and shows you every step—like Wireshark but with explanations. When you run it against any HTTPS server, you’ll see the cryptographic negotiation in human-readable form.
Example Output:
$ ./tls_explorer google.com:443
[TLS 1.3 Handshake Explorer]
Target: google.com:443
═══════════════════════════════════════════════════════════════
STEP 1: ClientHello (Client → Server)
═══════════════════════════════════════════════════════════════
Protocol Version: TLS 1.2 (0x0303) [Legacy, actual 1.3 in extension]
Random: 5f4dcc3b5aa765d61d8327deb882cf99... (32 bytes)
Session ID: [empty - TLS 1.3 doesn't use sessions like 1.2]
Cipher Suites (3 offered):
├── TLS_AES_256_GCM_SHA384 (0x1302)
├── TLS_AES_128_GCM_SHA256 (0x1301)
└── TLS_CHACHA20_POLY1305_SHA256 (0x1303)
Extensions:
├── server_name: google.com (SNI)
├── supported_versions: TLS 1.3
├── key_share: x25519 (32 bytes)
│ └── Public Key: 7d8a12b4c5e6f7890a1b2c3d4e5f6a7b...
├── signature_algorithms: RSA-PSS, ECDSA-P256
└── supported_groups: x25519, P-256
═══════════════════════════════════════════════════════════════
STEP 2: ServerHello (Server → Client)
═══════════════════════════════════════════════════════════════
Protocol Version: TLS 1.2 (0x0303) [Legacy]
Random: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6... (32 bytes)
Selected Cipher: TLS_AES_256_GCM_SHA384
Extensions:
├── supported_versions: TLS 1.3 (selected!)
└── key_share: x25519
└── Server Public Key: 8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b...
[CRYPTO] Computing shared secret via ECDHE...
[CRYPTO] Shared Secret: 0xf1e2d3c4b5a69788...
[CRYPTO] Deriving handshake keys via HKDF...
├── client_handshake_key: 16 bytes
├── server_handshake_key: 16 bytes
├── client_handshake_iv: 12 bytes
└── server_handshake_iv: 12 bytes
═══════════════════════════════════════════════════════════════
STEP 3: EncryptedExtensions (Server → Client, ENCRYPTED)
═══════════════════════════════════════════════════════════════
[DECRYPT] Using server_handshake_key...
Extensions:
└── application_layer_protocol_negotiation: h2 (HTTP/2)
═══════════════════════════════════════════════════════════════
STEP 4: Certificate (Server → Client, ENCRYPTED)
═══════════════════════════════════════════════════════════════
Certificate Chain (3 certificates):
[0] Leaf Certificate:
Subject: CN=*.google.com
Issuer: CN=GTS CA 1C3, O=Google Trust Services
Valid: 2024-01-15 to 2024-04-08
Public Key: ECDSA P-256
✓ Hostname matches (*.google.com → google.com)
[1] Intermediate CA:
Subject: CN=GTS CA 1C3
Issuer: CN=GTS Root R1
Valid: 2020-08-13 to 2027-09-30
✓ Signs certificate [0]
[2] Root CA (Trust Anchor):
Subject: CN=GTS Root R1
✓ Found in system trust store
═══════════════════════════════════════════════════════════════
STEP 5: CertificateVerify (Server → Client, ENCRYPTED)
═══════════════════════════════════════════════════════════════
Signature Algorithm: ecdsa_secp256r1_sha256
Signed Data: SHA256(transcript so far)
✓ Signature VALID - Server proved possession of private key
═══════════════════════════════════════════════════════════════
STEP 6: Finished (Server → Client, ENCRYPTED)
═══════════════════════════════════════════════════════════════
Verify Data: HMAC(transcript, server_finished_key)
✓ Server Finished message VALID
═══════════════════════════════════════════════════════════════
STEP 7: Finished (Client → Server, ENCRYPTED)
═══════════════════════════════════════════════════════════════
Sending client Finished...
✓ Handshake complete!
═══════════════════════════════════════════════════════════════
HANDSHAKE SUMMARY
═══════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ Connection Established │
│ │
│ Protocol: TLS 1.3 │
│ Cipher: TLS_AES_256_GCM_SHA384 │
│ Key Exchange: x25519 (ECDHE) │
│ Server Auth: ECDSA P-256 │
│ │
│ Handshake: 1-RTT (267ms) │
│ Server: google.com (Certificate verified ✓) │
└─────────────────────────────────────────────────────────────┘
TLS 1.3 Handshake Flow:
Client Server
│ │
│ ──────── ClientHello ────────────────────► │
│ (cipher suites, key_share, extensions) │
│ │
│ ◄──────── ServerHello ──────────────────── │
│ (selected cipher, server key_share) │
│ │
│ [Both compute shared secret via ECDHE] │
│ [Derive handshake traffic keys] │
│ │
│ ◄──── {EncryptedExtensions} ────────────── │
│ ◄──── {Certificate} ────────────────────── │
│ ◄──── {CertificateVerify} ──────────────── │
│ ◄──── {Finished} ───────────────────────── │
│ │
│ ──────── {Finished} ─────────────────────► │
│ │
│ [Derive application traffic keys] │
│ │
│ ════════ APPLICATION DATA ═══════════════ │
│ │
{} = Encrypted with handshake keys
═══ = Encrypted with application keys
The Core Question You’re Answering
“When I see the green padlock in my browser, what actually happened? How can two strangers on the internet establish a shared secret without ever meeting?”
Before you write any code, sit with this question. The answer involves one of the most beautiful ideas in computer science: Diffie-Hellman key exchange. Two parties can agree on a secret number by only exchanging public information, even if an eavesdropper sees everything. TLS 1.3 builds on this with modern cryptography, certificate chains for identity, and AEAD for encryption.
Concepts You Must Understand First
Stop and research these before coding:
- Public Key Cryptography Basics
- What’s the difference between symmetric and asymmetric encryption?
- How does Diffie-Hellman allow two parties to agree on a shared secret?
- What’s the “discrete logarithm problem” that makes DH secure?
- Book Reference: “Serious Cryptography” Ch. 9-10 - Jean-Philippe Aumasson
- TLS Record Protocol
- What’s the structure of a TLS record (type, version, length, data)?
- How do you tell the difference between a handshake record and application data?
- What’s the ContentType byte for each record type?
- Book Reference: RFC 8446 Section 5
- Elliptic Curve Diffie-Hellman (ECDHE)
- What’s X25519 and why is it preferred over traditional DH?
- What does “key share” mean in TLS 1.3?
- How do you compute a shared secret from public keys?
- Book Reference: “Bulletproof TLS and PKI” Ch. 4 - Ivan Ristić
- Certificate Chains and PKI
- What’s the difference between a leaf certificate, intermediate, and root?
- How does a browser decide to trust a certificate?
- What’s the “chain of signatures” concept?
- Book Reference: “Bulletproof TLS and PKI” Ch. 1-2 - Ivan Ristić
Questions to Guide Your Design
Before implementing, think through these:
- ClientHello Construction
- Which cipher suites should you offer? (Hint: TLS 1.3 only has a few)
- Which extensions are mandatory in TLS 1.3?
- How do you generate the X25519 key pair for the key_share extension?
- ServerHello Parsing
- How do you know if the server selected TLS 1.3 vs 1.2?
- What if the server rejects your cipher suites with a “handshake_failure” alert?
- How do you extract the server’s public key share?
- Key Derivation
- What is HKDF and why does TLS 1.3 use it instead of the PRF from TLS 1.2?
- What are the input/output sizes for each derived key?
- What’s the difference between handshake keys and application keys?
- Certificate Verification
- How do you verify a signature on a certificate?
- How do you check the certificate chain leads to a trusted root?
- What hostname checks do you need to perform?
Thinking Exercise
Trace the Key Derivation
TLS 1.3 uses a key schedule based on HKDF. Given:
- Client X25519 private key:
client_private - Client X25519 public key:
client_public - Server X25519 public key:
server_public
Trace these steps:
- Shared Secret:
shared_secret = X25519(client_private, server_public) - Early Secret:
early_secret = HKDF-Extract(0, 0)(no PSK) - Handshake Secret:
handshake_secret = HKDF-Extract(derived_secret, shared_secret) - Client Handshake Key:
c_hs_key = HKDF-Expand-Label(handshake_secret, "c hs traffic", transcript_hash) - Application Secret:
app_secret = HKDF-Extract(derived_secret_2, 0)
Draw the key schedule tree showing how each key is derived.
The Interview Questions They’ll Ask
Prepare to answer these:
- “Walk me through the TLS 1.3 handshake. What’s exchanged in each message?”
- “Why does TLS 1.3 only take 1 RTT while TLS 1.2 takes 2?”
- “What’s the difference between authentication and encryption? How does TLS provide both?”
- “Explain how ECDHE provides forward secrecy.”
- “What happens if a certificate in the chain is expired or revoked?”
- “How does 0-RTT work in TLS 1.3, and what are its security implications?”
- “What’s the purpose of the Finished message?”
- “Why was RSA key exchange removed from TLS 1.3?”
Hints in Layers
Hint 1: Use High-Level Crypto
Don’t implement X25519 or AES-GCM yourself. Use Go’s crypto/tls for primitives, but construct the handshake messages manually. Focus on the protocol, not the algorithms.
Hint 2: TLS Record Structure Every TLS message is wrapped in a record:
struct TLSRecord {
ContentType: 1 byte (22=Handshake, 23=Application)
Version: 2 bytes (0x0303 for TLS 1.2 in record layer)
Length: 2 bytes
Data: [Length bytes]
}
Hint 3: Handshake Message Structure Inside the record, handshake messages have:
struct HandshakeMessage {
Type: 1 byte (1=ClientHello, 2=ServerHello, 11=Certificate, etc.)
Length: 3 bytes (24-bit)
Data: [Length bytes]
}
Hint 4: Extension Parsing Extensions follow TLV format:
Extension:
Type: 2 bytes
Length: 2 bytes
Data: [Length bytes]
Read extensions in a loop until you’ve consumed all bytes.
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| TLS Protocol Deep Dive | “Bulletproof TLS and PKI” by Ivan Ristić | Ch. 1-4 |
| TLS 1.3 Specification | RFC 8446 | Sections 2, 4-5 |
| Cryptographic Primitives | “Serious Cryptography” by Aumasson | Ch. 8-11 |
| Go Crypto Programming | Go standard library documentation | crypto/* packages |
| Certificate Validation | “Real-World Cryptography” by Wong | Ch. 13 |
Common Pitfalls & Debugging
Problem 1: “handshake_failure alert from server”
- Why: Your ClientHello might be missing mandatory TLS 1.3 extensions (key_share, supported_versions, or signature_algorithms)
- Fix: Verify all required extensions are present. Use Wireshark to compare your ClientHello to a browser’s
- Quick test:
openssl s_client -connect example.com:443 -msgto see a working handshake
Problem 2: “Certificate verification failed even though cert looks valid in browser”
- Why: You’re not building the certificate chain correctly or missing intermediate certificates
- Fix: Server sends certificates in order: leaf → intermediate → root. You need to verify each signature against the next cert in chain
- Quick test:
openssl s_client -connect example.com:443 -showcertsto see the full chain
Problem 3: “Key derivation produces wrong keys, can’t decrypt ServerHello”
- Why: Transcript hash is incorrect. The transcript must include exact bytes of each message in order
- Fix: Hash concatenation of raw handshake messages (not including record layer headers). Print transcript_hash and compare to known-good value
- Quick test: Use RFC 8448 test vectors to verify your HKDF implementation
Problem 4: “Finished message verification fails”
- Why: verify_data computation is wrong, or you’re hashing the wrong transcript
- Fix: The Finished hash includes all messages UP TO but NOT INCLUDING the Finished message itself
- Quick test: Double-check you’re using handshake_secret, not application_secret
Problem 5: “Connection works for some sites but not others”
- Why: Different servers negotiate different cipher suites or extensions (ALPN, SNI, etc.)
- Fix: Add better logging to show which cipher suite was selected. Support multiple cipher suites
- Quick test: Compare successful vs failed connections side-by-side in Wireshark
Debugging Tools Checklist
# Capture handshake with Wireshark
sudo tcpdump -i any -w tls.pcap port 443
# Decode TLS handshake with OpenSSL
openssl s_client -connect example.com:443 -msg -debug
# View certificate chain
openssl s_client -connect example.com:443 -showcerts | openssl x509 -text
# Test cipher suite negotiation
openssl s_client -connect example.com:443 -tls1_3 -ciphersuites TLS_AES_128_GCM_SHA256
# Check server TLS configuration
nmap --script ssl-enum-ciphers -p 443 example.com
Project 4: WebSocket Real-Time Chat with Binary Framing
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Node.js
- Alternative Programming Languages: Go, Python
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: WebSockets / Real-time Protocols
- Software or Tool: TCP Sockets / HTTP Upgrade
- Main Book: “High Performance Browser Networking” by Ilya Grigorik
What you’ll build: A real-time chat server that implements the WebSocket protocol (RFC 6455) from scratch, including the HTTP Upgrade handshake and the framing layer (masking, opcode, payload length).
Why it teaches Networking: It bridges the gap between traditional request/response HTTP and persistent bidirectional TCP. You’ll learn how to “hijack” an HTTP connection and transition to a custom binary protocol.
Core challenges you’ll face:
- The Sec-WebSocket-Accept Handshake: Implementing the SHA-1 + Base64 hash required to prove the server understands WebSockets.
- Decoding Masked Frames: Client-to-server frames are masked; you must implement the XOR decoding logic.
- Variable-Length Payload Encoding: Handling 7-bit, 16-bit, and 64-bit length fields in the frame header.
- Control Frames: Implementing Ping/Pong and Close frames for connection health.
Key Concepts:
- WebSocket Framing: RFC 6455 Section 5
- HTTP Upgrade Mechanism: RFC 7230 Section 6.7
- Masking & XOR: Why clients mask data to prevent proxy cache poisoning.
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 2 (HTTP basics).
Real World Outcome
You’ll have a complete WebSocket chat system that implements the protocol from scratch. When you run the server and open multiple browser tabs, you’ll see real-time bidirectional communication without polling—messages appear instantly across all connected clients.
Example Output:
$ node chat_server.js --port 8080
[INFO] WebSocket Server starting on port 8080
═══════════════════════════════════════════════════════════════
HANDSHAKE: New connection from 127.0.0.1:54321
═══════════════════════════════════════════════════════════════
[REQUEST] GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
[COMPUTE] Sec-WebSocket-Accept:
Key + GUID: dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
SHA-1 Hash: 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16...
Base64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
[RESPONSE] HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
✓ WebSocket connection established (Client #1)
═══════════════════════════════════════════════════════════════
FRAME RECEIVED: Client #1
═══════════════════════════════════════════════════════════════
Raw bytes (hex): 81 8d 37 fa 21 3d 5f 9f 4d 59 18 9d 4d 5d 51 d4 4d 59 1e
Frame Header Analysis:
┌─────────────────────────────────────────────────────────────┐
│ Byte 0: 0x81 (1000 0001) │
│ ├── FIN: 1 (Final fragment) │
│ ├── RSV: 000 (No extensions) │
│ └── Opcode: 0001 (Text frame) │
│ │
│ Byte 1: 0x8D (1000 1101) │
│ ├── MASK: 1 (Masked - required for client→server) │
│ └── Payload Length: 13 bytes (7-bit) │
│ │
│ Bytes 2-5: Masking Key: 0x37 0xfa 0x21 0x3d │
│ │
│ Bytes 6-18: Masked Payload │
└─────────────────────────────────────────────────────────────┘
[UNMASK] XOR decoding with masking key...
Byte 0: 0x5f XOR 0x37 = 0x68 ('H')
Byte 1: 0x9f XOR 0xfa = 0x65 ('e')
Byte 2: 0x4d XOR 0x21 = 0x6c ('l')
Byte 3: 0x59 XOR 0x3d = 0x6c ('l')
... (key repeats)
[DECODED] "Hello, world!"
[BROADCAST] Sending to 3 connected clients...
├── Client #1: Sent (13 bytes, unmasked)
├── Client #2: Sent (13 bytes, unmasked)
└── Client #3: Sent (13 bytes, unmasked)
═══════════════════════════════════════════════════════════════
CONTROL FRAME: Ping from Client #2
═══════════════════════════════════════════════════════════════
[FRAME] Opcode: 0x09 (Ping)
[FRAME] Payload: "keepalive" (9 bytes)
[PONG] Responding with identical payload...
Opcode: 0x0A (Pong), Payload: "keepalive"
═══════════════════════════════════════════════════════════════
CONNECTION CLOSE: Client #1
═══════════════════════════════════════════════════════════════
[FRAME] Opcode: 0x08 (Close)
[FRAME] Status Code: 1000 (Normal Closure)
[FRAME] Reason: "Goodbye"
[CLOSE] Sending close acknowledgment...
[CLOSE] TCP connection terminated
--- Server Statistics ---
Active connections: 2
Total messages: 47
Bytes received: 1,284
Bytes sent: 3,852
Uptime: 5m 23s
WebSocket Handshake Flow:
Client Server
│ │
│ ──────── HTTP Request ─────────────────► │
│ GET /chat HTTP/1.1 │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: [random base64] │
│ Sec-WebSocket-Version: 13 │
│ │
│ ◄──────── HTTP Response ─────────────────── │
│ HTTP/1.1 101 Switching Protocols │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: [computed hash] │
│ │
│ ═══════════════════════════════════════ │
│ Protocol switched from HTTP to WebSocket │
│ ═══════════════════════════════════════ │
│ │
│ ──────── WebSocket Frame ─────────────► │
│ (Masked, Client → Server) │
│ │
│ ◄──────── WebSocket Frame ────────────── │
│ (Unmasked, Server → Client) │
│ │
WebSocket Frame Structure (RFC 6455):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
Opcodes:
┌─────────┬────────────────────┬────────────────────────────────┐
│ Opcode │ Type │ Description │
├─────────┼────────────────────┼────────────────────────────────┤
│ 0x0 │ Continuation │ Continuation of fragmented msg │
│ 0x1 │ Text │ UTF-8 encoded text data │
│ 0x2 │ Binary │ Binary data │
│ 0x8 │ Close │ Connection close request │
│ 0x9 │ Ping │ Heartbeat request │
│ 0xA │ Pong │ Heartbeat response │
└─────────┴────────────────────┴────────────────────────────────┘
Payload Length Encoding:
┌──────────────────┬─────────────────────────────────────────────┐
│ 7-bit value │ Meaning │
├──────────────────┼─────────────────────────────────────────────┤
│ 0-125 │ Actual payload length │
│ 126 │ Next 2 bytes are 16-bit length │
│ 127 │ Next 8 bytes are 64-bit length │
└──────────────────┴─────────────────────────────────────────────┘
The Core Question You’re Answering
“How can a server push data to a client without the client asking for it first? And why couldn’t we just keep an HTTP connection open?”
Before you write any code, sit with this question. HTTP is fundamentally request-response: the client speaks first, and the server can only reply. If you want real-time updates (chat messages, stock prices, game state), you’re stuck with hacks: polling every second (wasteful), long-polling (complex and limited), or Server-Sent Events (one-way only).
WebSockets solve this by upgrading an HTTP connection into a persistent, full-duplex channel. Once established, either side can send data at any time. But here’s the key insight: WebSockets aren’t a completely new protocol—they’re HTTP that “transforms” into something else. The handshake is HTTP; the framing is custom binary. Understanding this hybrid nature is what makes the protocol elegant.
Concepts You Must Understand First
Stop and research these before coding:
- The HTTP Upgrade Mechanism
- How does
Connection: Upgradework in HTTP/1.1? - What’s the difference between a protocol upgrade and a redirect?
- Why can’t HTTP/2 use the same upgrade mechanism?
- Book Reference: “HTTP: The Definitive Guide” Ch. 6 - Gourley & Totty
- Book Reference: RFC 7230 Section 6.7 - Upgrade
- How does
- Full-Duplex vs Half-Duplex Communication
- What does “full-duplex” mean at the TCP level vs the application level?
- Why is HTTP/1.1 technically half-duplex even though TCP is full-duplex?
- How do WebSockets enable true bidirectional communication?
- Book Reference: “High Performance Browser Networking” Ch. 17 - Ilya Grigorik
- WebSocket Frame Structure
- What do the FIN, RSV, and opcode bits mean?
- How does the variable-length payload encoding work (7-bit, 16-bit, 64-bit)?
- What’s the difference between data frames and control frames?
- Book Reference: RFC 6455 Sections 5-6
- XOR Masking for Security
- Why must client-to-server frames be masked but server-to-client frames are not?
- What attack does masking prevent (cache poisoning)?
- How do you XOR a payload with a 4-byte repeating key?
- Book Reference: RFC 6455 Section 10.3 (Security Considerations)
- Connection State Management
- What states can a WebSocket connection be in (CONNECTING, OPEN, CLOSING, CLOSED)?
- How do you handle a clean close vs an abrupt disconnect?
- What’s the purpose of the Close frame’s status code and reason?
- Book Reference: “High Performance Browser Networking” Ch. 17 - Ilya Grigorik
Questions to Guide Your Design
Before implementing, think through these:
- Handshake Implementation
- How will you detect that an HTTP request is a WebSocket upgrade request?
- How do you compute
Sec-WebSocket-AcceptfromSec-WebSocket-Key? - What happens if the client sends an invalid key or unsupported version?
- Should you support subprotocols (
Sec-WebSocket-Protocol)?
- Frame Parsing/Encoding
- How will you handle frames that span multiple TCP packets?
- How will you buffer incomplete frames until all data arrives?
- How do you differentiate between a 7-bit, 16-bit, and 64-bit payload length?
- How will you handle fragmented messages (FIN=0)?
- Message Broadcasting
- How will you track all connected clients?
- How do you handle a slow client that can’t keep up with messages?
- Should messages be queued or dropped for slow receivers?
- How will you serialize access to the client list (thread safety)?
- Connection Lifecycle
- How do you detect a dead connection (client disappeared without Close frame)?
- What timeout should you use for Ping/Pong heartbeats?
- How do you gracefully shut down the server without dropping messages?
- What error codes should you use for different failure modes?
Thinking Exercise
Trace a WebSocket Handshake and Frame by Hand
Part 1: The Handshake
Given this client request:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Answer these questions:
- What is the magic GUID that you concatenate with the key?
(Hint: It’s always
258EAFA5-E914-47DA-95CA-C5AB0DC85B11) - What’s the concatenated string before hashing?
- What’s the SHA-1 hash of that string (as 20 raw bytes)?
- What’s the Base64 encoding of those 20 bytes?
- Write the complete HTTP response you would send.
Part 2: Frame Encoding
Encode the text message “Hi” as a WebSocket frame from the server to client:
Step through this:
- What is the FIN bit? (Is this the final fragment?)
- What is the opcode for a text frame?
- What’s the payload length byte? (Is masking required?)
- Write the complete frame in hex bytes.
Expected answer:
Byte 0: 0x81 (FIN=1, opcode=0x01)
Byte 1: 0x02 (MASK=0, length=2)
Byte 2: 0x48 ('H')
Byte 3: 0x69 ('i')
Complete frame: 81 02 48 69
Part 3: XOR Masking
A client sends this frame (hex): 81 82 12 34 56 78 5a 5d
Decode it:
- Parse the header: What’s the opcode? Is it masked? What’s the payload length?
- Extract the masking key (4 bytes).
- Extract the masked payload (2 bytes).
- XOR each payload byte with the corresponding key byte:
payload[0] XOR key[0 % 4]payload[1] XOR key[1 % 4]
- What’s the decoded text?
The Interview Questions They’ll Ask
Prepare to answer these:
- “Explain the WebSocket handshake. Why does it start as HTTP?”
- “What’s the purpose of the Sec-WebSocket-Key and Sec-WebSocket-Accept headers?”
- “Why are client-to-server frames masked but server-to-client frames are not?”
- “How would you implement a ping/pong mechanism for connection health?”
- “What happens if a WebSocket message is too large to fit in a single frame?”
- “How do WebSockets compare to Server-Sent Events? When would you use each?”
- “How would you scale a WebSocket server to handle millions of connections?”
- “What’s the difference between WebSocket subprotocols and extensions?”
Hints in Layers
Hint 1: Starting with the Handshake
The handshake is just HTTP. Read the request, check for Upgrade: websocket, compute the accept hash, and send a 101 Switching Protocols response.
// Computing Sec-WebSocket-Accept
const key = request.headers['sec-websocket-key'];
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const accept = crypto
.createHash('sha1')
.update(key + GUID)
.digest('base64');
Hint 2: Frame Header Parsing Read byte-by-byte, but handle the variable-length cases:
function parseFrame(buffer) {
const byte0 = buffer[0];
const byte1 = buffer[1];
const fin = (byte0 & 0x80) !== 0;
const opcode = byte0 & 0x0F;
const masked = (byte1 & 0x80) !== 0;
let payloadLen = byte1 & 0x7F;
let offset = 2;
if (payloadLen === 126) {
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
payloadLen = buffer.readBigUInt64BE(2);
offset = 10;
}
// ...
}
Hint 3: Masking Implementation The XOR unmasking is straightforward—just remember the key repeats every 4 bytes:
function unmask(payload, maskingKey) {
const unmasked = Buffer.alloc(payload.length);
for (let i = 0; i < payload.length; i++) {
unmasked[i] = payload[i] ^ maskingKey[i % 4];
}
return unmasked;
}
Hint 4: Broadcasting to Multiple Clients Keep a Set of connected sockets and iterate through them:
const clients = new Set();
function broadcast(message) {
const frame = encodeFrame(message);
for (const client of clients) {
if (client.readyState === 'OPEN') {
client.write(frame);
}
}
}
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| WebSocket Protocol Overview | “High Performance Browser Networking” by Ilya Grigorik | Ch. 17 |
| WebSocket Wire Format | RFC 6455 | Sections 5-7 |
| HTTP Upgrade Mechanism | “HTTP: The Definitive Guide” by Gourley & Totty | Ch. 6 |
| HTTP Upgrade Specification | RFC 7230 | Section 6.7 |
| Real-Time Web Architecture | “Building Real-Time Web Applications” by Manning | Ch. 1-3 |
| Node.js TCP Sockets | Node.js Documentation | net module |
| Binary Data in JavaScript | “JavaScript: The Definitive Guide” by Flanagan | Ch. 11 (Typed Arrays) |
Common Pitfalls & Debugging
Problem 1: “Client connects but handshake never completes”
- Why: You’re not sending the HTTP 101 Switching Protocols response correctly, or headers are malformed
- Fix: Verify you send exactly:
HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: [computed_hash]\r\n\r\n - Quick test:
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" http://localhost:8080
Problem 2: “Frames are corrupted or parsing fails randomly”
- Why: You’re not handling variable-length payload length correctly (126/127 special values)
- Fix: If payload_len is 126, read next 2 bytes as uint16. If 127, read next 8 bytes as uint64
- Quick test: Send a message longer than 125 bytes and verify it’s received correctly
Problem 3: “Messages appear as gibberish”
- Why: You forgot to unmask client-to-server frames (XOR with masking key)
- Fix: Check the MASK bit (byte1 & 0x80). If set, read 4-byte mask and XOR payload
- Quick test: Print raw bytes and unmasked bytes side-by-side to verify XOR operation
Problem 4: “Connection closes unexpectedly after a few messages”
- Why: You’re not responding to PING frames with PONG, or not handling CLOSE frames
- Fix: Implement opcode handlers: PING (0x9) → respond with PONG (0xA), CLOSE (0x8) → send CLOSE back and close socket
- Quick test:
wscat -c ws://localhost:8080and watch for ping/pong in debug logs
Problem 5: “Broadcasting doesn’t work or clients receive duplicate messages”
- Why: You’re not tracking client connections properly or not checking socket state before writing
- Fix: Maintain a Set of active clients, remove on disconnect, check
socket.destroyedbefore writing - Quick test: Connect 3 clients, disconnect one, verify remaining 2 still receive messages
Problem 6: “Large messages get truncated or cause crashes”
- Why: You’re assuming entire frame arrives in one TCP packet, but TCP can fragment
- Fix: Buffer incomplete frames and wait for more data. Check if you’ve received payload_len bytes before processing
- Quick test: Send a 10MB message and verify it arrives intact
Debugging Tools Checklist
# Test WebSocket handshake with curl
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" http://localhost:8080
# Interactive WebSocket client
npm install -g wscat
wscat -c ws://localhost:8080
# Capture WebSocket traffic
sudo tcpdump -i lo0 -A port 8080 -w websocket.pcap
# View WebSocket frames in Wireshark (filter: websocket)
wireshark websocket.pcap
# Send binary test data
echo -n "Hello" | websocat ws://localhost:8080
# Verify Sec-WebSocket-Accept calculation
echo -n "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11" | \
openssl dgst -sha1 -binary | base64
# Should output: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Project 5: HTTP/2 Binary Frame Parser & HPACK Decoder
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Rust
- Alternative Programming Languages: Go, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: HTTP/2 / Binary Optimization
- Software or Tool: Byte buffers / Huffman Coding
- Main Book: “Learning HTTP/2” by Stephen Ludin
What you’ll build: A low-level parser that takes raw bytes from an HTTP/2 connection and decodes them into Streams, Frames (HEADERS, DATA, SETTINGS), and decompresses headers using the HPACK algorithm.
Why it teaches Networking: HTTP/2 is a massive departure from HTTP/1.1. It teaches you about multiplexing, flow control, and header compression (HPACK), which are critical for modern web performance.
Core challenges you’ll face:
- HPACK Dynamic Table: Implementing the stateful compression table that both client and server must maintain.
- Huffman Decoding: Decompressing header names and values using the static HTTP/2 Huffman table.
- Stream Multiplexing: Keeping track of multiple independent streams over a single connection.
- Frame Interleaving: Handling cases where a large DATA frame is interrupted by a high-priority HEADERS frame.
Key Concepts:
- HTTP/2 Framing Layer: RFC 7540 Section 4
- HPACK Compression: RFC 7541
- Flow Control: Managing window sizes to prevent one stream from hogging the connection.
Difficulty: Expert Time estimate: 3 weeks Prerequisites: Project 2, experience with bitwise operations.
Real World Outcome
You’ll have a tool h2-dump that can take a raw packet capture of an unencrypted HTTP/2 stream and decode it into human-readable frames, headers, and data. This is invaluable for debugging HTTP/2 performance issues and understanding why certain requests are slow.
Example Output:
$ cat h2_stream.bin | ./h2-dump --verbose
[H2-DUMP] HTTP/2 Binary Frame Analyzer
[INFO] Reading from stdin...
[PREFACE] Detected HTTP/2 connection preface: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
═══════════════════════════════════════════════════════════════
FRAME #1: SETTINGS (Type: 0x04)
═══════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ Frame Header (9 bytes): │
│ Length: 18 (0x000012) │
│ Type: SETTINGS (0x04) │
│ Flags: 0x00 │
│ Stream ID: 0 (connection-level) │
├─────────────────────────────────────────────────────────────┤
│ Settings Payload: │
│ SETTINGS_MAX_CONCURRENT_STREAMS (0x03) = 100 │
│ SETTINGS_INITIAL_WINDOW_SIZE (0x04) = 65535 │
│ SETTINGS_MAX_FRAME_SIZE (0x05) = 16384 │
└─────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════
FRAME #2: HEADERS (Type: 0x01)
═══════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ Frame Header (9 bytes): │
│ Length: 42 (0x00002A) │
│ Type: HEADERS (0x01) │
│ Flags: 0x04 (END_HEADERS) │
│ Stream ID: 1 │
├─────────────────────────────────────────────────────────────┤
│ HPACK Decoded Headers: │
│ [Indexed #2] :method = GET │
│ [Indexed #4] :path = / │
│ [Indexed #7] :scheme = https │
│ [Literal] :authority = example.com │
│ [Literal+Huffman] user-agent = Mozilla/5.0 (decoded) │
│ [Indexed #16] accept-encoding = gzip, deflate │
├─────────────────────────────────────────────────────────────┤
│ Dynamic Table State (after decoding): │
│ [62] :authority = example.com │
│ [63] user-agent = Mozilla/5.0 │
│ Table Size: 119/4096 bytes │
└─────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════
FRAME #3: HEADERS (Type: 0x01)
═══════════════════════════════════════════════════════════════
[Stream 3] HEADERS: :method=GET, :path=/favicon.ico
Flags: END_HEADERS | END_STREAM
HPACK: Reused indexed :authority from dynamic table [62]
═══════════════════════════════════════════════════════════════
FRAME #4: DATA (Type: 0x00)
═══════════════════════════════════════════════════════════════
[Stream 1] DATA: 1024 bytes
Flags: END_STREAM
Preview: "<!DOCTYPE html><html><head>..."
═══════════════════════════════════════════════════════════════
FRAME #5: DATA (Type: 0x00)
═══════════════════════════════════════════════════════════════
[Stream 3] DATA: 512 bytes (End Stream)
═══════════════════════════════════════════════════════════════
STREAM SUMMARY
═══════════════════════════════════════════════════════════════
Stream 1: CLOSED (GET / -> 1024 bytes)
Stream 3: CLOSED (GET /favicon.ico -> 512 bytes)
Total Frames: 5
Header Compression Ratio: 68% reduction (HPACK efficiency)
HTTP/2 Binary Frame Structure:
┌─────────────────────────────────────────────────────────────────────────┐
│ HTTP/2 FRAME FORMAT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ FRAME HEADER (9 bytes fixed) │ │
│ ├──────────────────┬──────────┬───────────┬────────────────────────┤ │
│ │ Length (24 bits)│Type (8b) │ Flags (8b)│ Stream ID (31 bits) │ │
│ │ Payload size │ 0x00-0x09│ Varies │ R + Stream ID │ │
│ ├──────────────────┴──────────┴───────────┴────────────────────────┤ │
│ │ │ │
│ │ FRAME PAYLOAD (Variable) │ │
│ │ Size determined by Length field │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ FRAME TYPES: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 0x00 DATA - Request/response body │ │
│ │ 0x01 HEADERS - HTTP headers (HPACK compressed) │ │
│ │ 0x02 PRIORITY - Stream priority (deprecated in RFC 9113) │ │
│ │ 0x03 RST_STREAM - Abnormal stream termination │ │
│ │ 0x04 SETTINGS - Connection configuration │ │
│ │ 0x05 PUSH_PROMISE - Server push initiation │ │
│ │ 0x06 PING - Connection liveness check │ │
│ │ 0x07 GOAWAY - Graceful connection shutdown │ │
│ │ 0x08 WINDOW_UPDATE - Flow control window adjustment │ │
│ │ 0x09 CONTINUATION - Header block continuation │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
HPACK Compression Architecture:
┌─────────────────────────────────────────────────────────────────────────┐
│ HPACK COMPRESSION CONTEXT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────┐ │
│ │ STATIC TABLE (61 entries) │ │
│ │ Pre-defined headers (RFC 7541 App. A) │ │
│ ├───────────────────────────────────────┤ │
│ │ Index 1: :authority = "" │ │
│ │ Index 2: :method = GET │ │
│ │ Index 3: :method = POST │ │
│ │ Index 4: :path = / │ │
│ │ Index 5: :path = /index.html │ │
│ │ Index 6: :scheme = http │ │
│ │ Index 7: :scheme = https │ │
│ │ ... │ │
│ │ Index 61: www-authenticate = "" │ │
│ └───────────────────────────────────────┘ │
│ │ │
│ ▼ Index 62 onwards │
│ ┌───────────────────────────────────────┐ │
│ │ DYNAMIC TABLE (bounded FIFO) │ │
│ │ Maintained per connection direction │ │
│ ├───────────────────────────────────────┤ │
│ │ Index 62: :authority = example.com │ <── Most recent │
│ │ Index 63: user-agent = Mozilla/5.0 │ │
│ │ Index 64: custom-header = value │ │
│ │ ... │ │
│ │ (Evicted when table exceeds max size) │ <── Oldest evicted │
│ └───────────────────────────────────────┘ │
│ │ │
│ │ Max size set by SETTINGS_HEADER_TABLE_SIZE (default 4096) │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ HPACK ENCODING FORMATS: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Indexed Header Field (1 bit prefix) │ │
│ │ ┌───────┐ │ │
│ │ │1|Index│ → Reference static or dynamic table by index │ │
│ │ └───────┘ │ │
│ │ Example: 0x82 = Index 2 = ":method: GET" │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Literal Header with Incremental Indexing (2 bit prefix) │ │
│ │ ┌─────────┬──────────┬─────────────┐ │ │
│ │ │01|Index | H|Length | Value │ → Add to dynamic table │ │
│ │ └─────────┴──────────┴─────────────┘ │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Literal Header without Indexing (4 bit prefix) │ │
│ │ ┌─────────┬──────────┬─────────────┐ │ │
│ │ │0000|Idx | H|Length | Value │ → Don't add to table │ │
│ │ └─────────┴──────────┴─────────────┘ │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ Literal Header Never Indexed (4 bit prefix) │ │
│ │ ┌─────────┬──────────┬─────────────┐ │ │
│ │ │0001|Idx | H|Length | Value │ → Sensitive, never index │ │
│ │ └─────────┴──────────┴─────────────┘ │ │
│ │ H = Huffman encoding flag (1 = Huffman encoded) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Stream Multiplexing Over Single Connection:
Single TCP Connection
┌──────────────────────────────────────────────────┐
│ │
│ Stream 1 Stream 3 Stream 5 Stream 7 │
│ (Request) (Request) (Request) (Request) │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │HEADER│ │HEADER│ │HEADER│ │HEADER│ │
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │ │
│ ┌──┴───┐ ┌──┴───┐ ┌──┴───┐ ┌──┴───┐ │
│ │ DATA │ │ DATA │ │ DATA │ │ DATA │ │
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ═══════════════════════════════════════ │
│ │H1│D1│H3│D1│H5│D3│D1│H7│D5│D3│D7│...│ │
│ ═══════════════════════════════════════ │
│ Interleaved frames on the wire │
│ │
└──────────────────────────────────────────────────┘
Legend: H = HEADERS frame, D = DATA frame
Numbers = Stream ID
The Core Question You’re Answering
“Why did HTTP evolve from human-readable text to a binary protocol, and what problems does this solve? What makes HPACK compression so much more effective than simple gzip?”
Before you write any code, sit with this question. HTTP/1.1’s text-based format was elegant for debugging with telnet, but it had fundamental inefficiencies:
-
Parsing ambiguity: Where does a header end? You scan for
\r\n. Where do headers end? You scan for\r\n\r\n. This linear scanning is slow and error-prone. -
Header redundancy: Every HTTP request repeats the same headers—
User-Agent,Accept,Cookie—even when they haven’t changed. On a page with 100 resources, you’re sending identical headers 100 times. -
Head-of-line blocking: In HTTP/1.1, if the first request is slow, all subsequent requests on that connection wait. Opening multiple connections was the workaround, but TCP slow-start made that expensive.
HTTP/2’s binary framing solves all three: fixed-size frame headers eliminate parsing ambiguity, HPACK compression eliminates redundancy (90%+ savings on headers), and stream multiplexing eliminates application-level HOL blocking.
Concepts You Must Understand First
Stop and research these before coding:
- HTTP/2 Framing Layer
- What is the structure of the 9-byte frame header?
- What are the different frame types and when is each used?
- What’s the difference between connection-level frames (SETTINGS, PING, GOAWAY) and stream-level frames (HEADERS, DATA)?
- How does the END_HEADERS flag interact with CONTINUATION frames?
- Book Reference: “Learning HTTP/2” by Stephen Ludin & Javier Garza, Ch. 4
- HPACK Header Compression
- What are the 61 entries in the static table, and why were they chosen?
- How does the dynamic table work as a bounded FIFO queue?
- What’s the difference between indexed, literal with indexing, and literal without indexing representations?
- Why does HPACK use integer encoding with variable-length prefixes?
- Book Reference: RFC 7541 (HPACK) Sections 2-4
- Huffman Coding for Header Values
- How does Huffman coding achieve compression (frequent symbols get shorter codes)?
- What is the static Huffman table defined in RFC 7541 Appendix B?
- How do you decode a bit stream when codes have variable lengths?
- What’s the EOS (end-of-string) symbol and why is it needed?
- Book Reference: “Introduction to Algorithms” by Cormen et al., Ch. 16.3 (Huffman Codes)
- Stream Multiplexing and Prioritization
- What makes a stream ID odd vs even? Client-initiated vs server-initiated?
- How does the priority tree work (dependencies and weights)?
- What happens when you close a stream that other streams depend on?
- Book Reference: RFC 7540 Section 5 (Streams and Multiplexing)
- Flow Control Windows
- What’s the purpose of WINDOW_UPDATE frames?
- How does connection-level flow control differ from stream-level?
- What happens when a window goes to zero? How do you recover?
- Book Reference: RFC 7540 Section 5.2 (Flow Control)
Questions to Guide Your Design
Before implementing, think through these:
- Frame Parsing Architecture
- How will you read exactly 9 bytes for the frame header before reading the payload?
- How will you handle frames that span multiple TCP reads?
- What’s your strategy for validating frame types and flags?
- How will you dispatch different frame types to their handlers?
- HPACK Decoder State Management
- How will you represent the dynamic table (array, linked list, ring buffer)?
- How will you handle the table size update triggered by SETTINGS_HEADER_TABLE_SIZE?
- What happens when you need to evict entries to make room for new ones?
- How will you maintain separate encoder and decoder dynamic tables?
- Stream Tracking
- How will you map stream IDs to stream state?
- What states can a stream be in (idle, open, half-closed, closed)?
- How will you detect protocol errors like receiving DATA on a closed stream?
- How will you reassemble a header block split across HEADERS + CONTINUATION frames?
- Error Handling
- What’s the difference between a connection error and a stream error?
- When do you send RST_STREAM vs GOAWAY?
- How do you handle unknown frame types (they might be extensions)?
- What happens if HPACK decoding fails (decompression bomb, invalid index)?
Thinking Exercise
Decode an HTTP/2 Frame by Hand
Given these raw bytes (hex), decode them step by step:
00 00 11 01 04 00 00 00 01 82 86 84 41 8a 08 9d
5c 0b 81 70 dc 78 0f 03
Step 1: Parse the Frame Header (first 9 bytes)
00 00 11 → Length = ?
01 → Type = ?
04 → Flags = ?
00 00 00 01 → Stream ID = ? (ignore the reserved bit)
Step 2: Identify the Frame Type
- What frame type is 0x01?
- What does flag 0x04 mean for this frame type?
Step 3: Decode HPACK Header Block Starting at byte 9 (the payload):
82 → Binary: 1000 0010 → What encoding format? What index?
86 → Binary: 1000 0110 → What header does this represent?
84 → Binary: 1000 0100 → What header?
41 → Binary: 0100 0001 → What encoding format? Index = ?
Step 4: Decode Huffman-Encoded Value
After 41, we have a Huffman-encoded string. The length byte:
8a → Binary: 1000 1010 → H bit = 1 (Huffman), Length = 10 bytes
The next 10 bytes are the Huffman-encoded value. Use RFC 7541 Appendix B to decode.
Step 5: Draw the Dynamic Table After decoding this header block, what entries are in the dynamic table?
Draw it:
Dynamic Table (after decoding):
Index 62: ????? = ?????
Size: ?? bytes / 4096 max
Trace HPACK Compression for a Header
Imagine you need to send these headers:
:method: GET
:path: /api/users
:authority: api.example.com
user-agent: MyClient/1.0
authorization: Bearer abc123token
For each header:
- Is it in the static table? If so, what index?
- Should it be added to the dynamic table?
- What bytes would you emit?
Hint: The authorization header contains sensitive data. What HPACK representation should you use and why?
The Interview Questions They’ll Ask
Prepare to answer these:
-
“Explain how HTTP/2 multiplexing works. How is it different from HTTP/1.1 pipelining?”
-
“What is head-of-line blocking, and how does HTTP/2 solve it at the application layer? Does it still exist somewhere?”
-
“Walk me through how HPACK compression works. Why wasn’t gzip sufficient?”
-
“What’s the difference between the static table and dynamic table in HPACK? Why are there two?”
-
“How does HTTP/2 flow control work? What problem does it solve?”
-
“Why does HTTP/2 require TLS in practice, even though the spec allows unencrypted connections?”
-
“What happens if an HPACK decoder gets out of sync with the encoder? How would you detect and recover?”
-
“Compare HTTP/2’s HPACK with HTTP/3’s QPACK. Why was a new compression algorithm needed?”
Hints in Layers
Hint 1: Frame Header Structure (9 bytes)
struct FrameHeader {
length: u24, // 3 bytes, big-endian
frame_type: u8, // 1 byte: 0x00-0x09 defined, others reserved
flags: u8, // 1 byte: meaning varies by frame type
reserved: u1, // 1 bit, must be 0
stream_id: u31, // 31 bits, big-endian
}
Read the first 9 bytes, parse them, then read exactly length more bytes for the payload. Never read more than length bytes or you’ll corrupt the next frame.
Hint 2: HPACK Indexed vs Literal Headers Check the first bits of each byte in the header block:
1xxxxxxx → Indexed (7-bit index)
01xxxxxx → Literal with Incremental Indexing (6-bit index)
0000xxxx → Literal without Indexing (4-bit index)
0001xxxx → Literal Never Indexed (4-bit index)
001xxxxx → Dynamic Table Size Update (5-bit max-size)
The index tells you which static/dynamic table entry to use for the name. If index is 0, the name follows as a literal string.
Hint 3: Huffman Decoding Approach The Huffman table in RFC 7541 Appendix B maps bit patterns to bytes. Build a lookup tree:
struct HuffmanNode {
left: Option<Box<HuffmanNode>>, // bit 0
right: Option<Box<HuffmanNode>>, // bit 1
symbol: Option<u8>, // leaf node has symbol
}
Read bits from the encoded string, walk the tree, emit symbols when you hit leaves. Handle padding (up to 7 bits of 1s at the end).
Hint 4: Stream State Machine Track each stream’s state:
send HEADERS
idle ─────────────────────────────────► open
│ │
│ recv HEADERS │ send END_STREAM
▼ ▼
open ◄──────────────────────────── half-closed (local)
│ │
│ recv END_STREAM │ recv END_STREAM
▼ │
half-closed (remote) ──────────────────────────┘
│ │
│ send END_STREAM │
▼ ▼
closed ◄───────────────────────────────────┘
Receiving a frame on a stream in the wrong state is a protocol error.
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| HTTP/2 Protocol Overview | “Learning HTTP/2” by Ludin & Garza | Ch. 3-5 |
| HTTP/2 Framing Layer | RFC 7540 | Sections 4-6 |
| HPACK Compression | RFC 7541 | Sections 2-4, Appendix A-B |
| Binary Protocol Design | “High Performance Browser Networking” by Grigorik | Ch. 12 |
| Huffman Coding Theory | “Introduction to Algorithms” by Cormen et al. | Ch. 16.3 |
| Rust Systems Programming | “Programming Rust” by Blandy & Orendorff | Ch. 7-10 |
| Bitwise Operations | “Hacker’s Delight” by Warren | Ch. 2-3 |
Common Pitfalls & Debugging
Problem 1: “Frames are parsed correctly but headers are empty or corrupted”
- Why: Your HPACK decoder is not correctly handling indexed headers from the static table
- Fix: Verify your static table is correctly initialized with all 61 entries from RFC 7541 Appendix A. Index 1 is
:authority, not:method - Quick test: Parse a known HEADERS frame and verify index 2 (
:method: GET) decodes correctly
Problem 2: “Huffman decoding fails or produces garbage”
- Why: You’re not correctly handling the variable-length Huffman codes or padding bits
- Fix: Ensure you read the entire bit stream, not byte-by-byte. The last byte may have padding (all 1s) that must be ignored
- Quick test: Decode the string “www.example.com” from RFC 7541 example (hex:
f1e3 c2e5 f23a 6ba0 ab90 f4ff)
Problem 3: “Dynamic table grows indefinitely or causes out-of-memory”
- Why: You’re not evicting old entries when the table exceeds max size (default 4096 bytes)
- Fix: When adding entry, check if table_size + new_entry_size > max_table_size. If yes, evict oldest entries (FIFO) until it fits
- Quick test: Set max table size to 256 bytes, add 10 large headers, verify table doesn’t exceed limit
Problem 4: “Stream multiplexing doesn’t work, requests are processed sequentially”
- Why: You’re blocking on one stream’s DATA frames instead of interleaving frame processing
- Fix: Read frames in a loop regardless of stream ID. Dispatch each frame to the appropriate stream handler
- Quick test: Send two concurrent requests on streams 1 and 3, verify both are processed in parallel
Problem 5: “SETTINGS frame exchange fails, connection dies immediately”
- Why: You’re not sending the required SETTINGS frame preface or not ACKing server’s SETTINGS
- Fix: After TCP connect and “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n” preface, immediately send SETTINGS frame. When you receive SETTINGS, send SETTINGS with ACK flag
- Quick test:
curl --http2 -v https://example.comand compare your connection sequence
Problem 6: “Flow control errors (FLOW_CONTROL_ERROR) terminate connection”
- Why: You’re sending more DATA than the connection or stream window allows
- Fix: Track window size for each stream and the connection. Decrement on DATA send, increment on WINDOW_UPDATE receipt. Never send if window is 0
- Quick test: Send a large response (10MB), verify you respect initial window size (65535 bytes) and wait for WINDOW_UPDATE
Debugging Tools Checklist
# Capture HTTP/2 traffic (note: encrypted, need TLS keys)
sudo tcpdump -i any -w http2.pcap port 443
# Use h2spec to validate HTTP/2 implementation
go install github.com/summerwind/h2spec/cmd/h2spec@latest
h2spec http2 -h localhost -p 8080
# Test with curl in verbose mode
curl --http2 -v https://localhost:8080
# Decode HPACK header blocks manually
# Use online tool: https://http2.golang.org/hpack/ or
python -c "import hpack; print(hpack.Decoder().decode(b'\x82\x86\x84'))"
# View HTTP/2 frames in Wireshark
# Filter: http2
# Right-click → Protocol Preferences → Reassemble HTTP/2 streaming bodies
# Test HPACK compression ratio
nghttp -nv https://example.com # Shows header compression stats
# Benchmark multiplexing performance
h2load -n 1000 -c 10 https://localhost:8080
Project 6: Mini-CDN (Reverse Proxy with Intelligent Caching)
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Go
- Alternative Programming Languages: Python, Node.js
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 5. The “Industry Disruptor”
- Difficulty: Level 3: Advanced
- Knowledge Area: CDNs / Caching / Proxying
- Software or Tool: HTTP Client & Server
- Main Book: “High Performance Browser Networking” by Ilya Grigorik
What you’ll build: A reverse proxy that sits in front of multiple origin servers, caches responses based on Cache-Control headers, and implements load balancing and basic purging.
Why it teaches Networking: This project moves you from “Server” to “Infrastructure.” You’ll learn how global CDNs like Cloudflare or Akamai work by manipulating headers and managing distributed state.
Core challenges you’ll face:
- Cache Invalidation: Implementing a system to purge specific URLs when the origin updates.
- Header Normalization: Handling
Varyheaders and ensuring the cache key accounts for compressed vs uncompressed content. - Thundering Herd Problem: Ensuring that if 1000 people request a missing file at once, you only make ONE request to the origin.
- Conditional GETs: Implementing
If-Modified-SinceandETagsupport.
Key Concepts:
- HTTP Caching: RFC 7234
- Reverse Proxying: How to preserve the original client IP (
X-Forwarded-For). - Load Balancing Algorithms: Round-robin vs Least-connections.
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Project 2.
Real World Outcome
A proxy minicdn that reduces the load on your origin server by 90% for repeated requests and handles traffic spikes gracefully.
Example Output:
$ ./minicdn --origin http://localhost:9000 --origins http://localhost:9001 \
--cache-dir /tmp/cdn_cache --max-cache-size 1GB
[INFO] Mini-CDN starting...
[INFO] Backend origins: localhost:9000 (primary), localhost:9001 (secondary)
[INFO] Cache directory: /tmp/cdn_cache (max: 1GB)
[INFO] Listening on :8080
===============================================================================
REQUEST FLOW VISUALIZATION
===============================================================================
Request 1: Cache MISS (Cold cache)
-------------------------------------------------------------------------------
[14:23:01.234] [MISS] GET /api/products.json
|-- Cache Key: GET|/api/products.json|gzip
|-- Cache Status: NOT_FOUND
|-- Forwarding to origin: localhost:9000
|-- Origin Response: 200 OK (took 145ms)
|-- Cache-Control: max-age=300, public
|-- ETag: "a1b2c3d4e5f6"
|-- Content-Length: 24,576 bytes
+-- Action: STORED in cache (TTL: 300s)
Request 2: Cache HIT (Warm cache)
-------------------------------------------------------------------------------
[14:23:01.567] [HIT] GET /api/products.json
|-- Cache Key: GET|/api/products.json|gzip
|-- Cache Status: FRESH
|-- TTL Remaining: 299s
|-- Response Time: 2ms
+-- Origin Requests Saved: 1
Request 3: Conditional Request (Revalidation)
-------------------------------------------------------------------------------
[14:28:02.891] [STALE] GET /api/products.json
|-- Cache Key: GET|/api/products.json|gzip
|-- Cache Status: EXPIRED
|-- Sending conditional request to origin
| +-- If-None-Match: "a1b2c3d4e5f6"
|-- Origin Response: 304 Not Modified (took 45ms)
+-- Action: REFRESHED TTL (300s)
Request 4: Cache MISS with Vary Header
-------------------------------------------------------------------------------
[14:28:03.123] [MISS] GET /api/products.json (Accept-Encoding: identity)
|-- Cache Key: GET|/api/products.json|identity <-- Different!
|-- Cache Status: NOT_FOUND (Vary: Accept-Encoding)
|-- Note: Separate cache entry for uncompressed version
+-- Action: STORED as separate variant
Request 5: Thundering Herd Protection
-------------------------------------------------------------------------------
[14:28:04.000] [COALESCE] GET /images/hero.jpg (1000 concurrent requests)
|-- First request triggers origin fetch
|-- 999 requests WAITING (request coalescing active)
|-- Origin Response: 200 OK (took 250ms)
+-- All 1000 clients served from single origin request
===============================================================================
CACHE PURGE OPERATION
===============================================================================
$ curl -X PURGE http://localhost:8080/api/products.json
[14:30:00.000] [PURGE] /api/products.json
|-- Purged 2 variants (gzip, identity)
|-- Total bytes freed: 49,152
+-- Status: SUCCESS
===============================================================================
LOAD BALANCING DEMONSTRATION
===============================================================================
[14:31:00.000] [BALANCE] Origin selection for /api/users
|-- Algorithm: Weighted Round-Robin
|-- localhost:9000 (weight: 3, connections: 12)
|-- localhost:9001 (weight: 1, connections: 4)
+-- Selected: localhost:9000
[14:31:00.050] [FAILOVER] Origin localhost:9000 unhealthy!
|-- Health check failed: Connection refused
|-- Marking as DOWN for 30s
+-- Redirecting traffic to localhost:9001
===============================================================================
STATISTICS SUMMARY (after 1 hour)
===============================================================================
+------------------------------------------------------------------------------+
| CACHE PERFORMANCE |
+------------------------------------------------------------------------------+
| Total Requests: 125,847 |
| Cache Hits: 113,262 (90.0%) |
| Cache Misses: 12,585 (10.0%) |
| Revalidations: 4,231 (3.4%) |
| 304 Not Modified: 3,892 (92% of revalidations) |
| |
| Avg Response Time: |
| Cache HIT: 2.3ms |
| Cache MISS: 147.2ms |
| Revalidation: 43.8ms |
| |
| Bandwidth Saved: 4.2 GB (origin would have served 4.7 GB) |
| Origin Load Reduction: 90% |
+------------------------------------------------------------------------------+
| THUNDERING HERD PROTECTION |
+------------------------------------------------------------------------------+
| Coalesced Requests: 47,832 |
| Origin Requests Prevented: 47,124 |
| Peak Concurrent Waiting: 1,247 requests |
+------------------------------------------------------------------------------+
Mini-CDN Architecture:
+---------------------+
| Incoming |
| Requests |
+---------+-----------+
|
v
+------------------------------------------------------------------------------+
| MINI-CDN PROXY |
+------------------------------------------------------------------------------+
| |
| +-----------------+ +-----------------+ +-----------------+ |
| | Request | | Cache | | Origin | |
| | Handler |----->| Lookup |----->| Pool | |
| | | | | | | |
| | - Parse headers | | - Build key | | - Round-robin | |
| | - Normalize URL | | - Check Vary | | - Health check | |
| | - Validate | | - Check TTL | | - Failover | |
| +-----------------+ +--------+--------+ +--------+--------+ |
| | | |
| +--------v--------+ | |
| | Cache Store | | |
| | |<--------------+ |
| | - LRU eviction | (store response) |
| | - Size limits | |
| | - TTL tracking | |
| | - ETag index | |
| +-----------------+ |
| |
| +----------------------------------------------------------------------+ |
| | REQUEST COALESCING (Thundering Herd Protection) | |
| | | |
| | /img.jpg: | |
| | Request 1 ----> [FETCHING] ----> All get same response | |
| | Request 2 ----> [WAITING] --------------+ | |
| | Request 3 ----> [WAITING] --------------+ | |
| | ... ... | |
| | Request N ----> [WAITING] --------------+ | |
| +----------------------------------------------------------------------+ |
| |
+------------------------------------------------------------------------------+
|
v
+---------------------+
| Origin Servers |
| |
| +---+ +---+ +---+ |
| | A | | B | | C | |
| +---+ +---+ +---+ |
+---------------------+
Cache Key Construction with Vary Header:
Request:
GET /api/data HTTP/1.1
Host: example.com
Accept-Encoding: gzip
Accept-Language: en-US
Response Header:
Vary: Accept-Encoding, Accept-Language
Cache Keys Generated:
+------------------------------------------------------------------------------+
| |
| Key 1: GET|/api/data|Accept-Encoding=gzip|Accept-Language=en-US |
| Key 2: GET|/api/data|Accept-Encoding=br|Accept-Language=en-US |
| Key 3: GET|/api/data|Accept-Encoding=gzip|Accept-Language=fr-FR |
| Key 4: GET|/api/data|Accept-Encoding=identity|Accept-Language=en-US |
| |
| Each combination = separate cache entry! |
| |
+------------------------------------------------------------------------------+
Cache Decision Flow:
Request arrives
|
v
+-------------+
| Build cache |
| key + Vary |
+------+------+
|
v
+-------------+ No +-------------+
| In cache? |---------->| Fetch from |
+------+------+ | origin |
| Yes +------+------+
v |
+-------------+ v
| Expired? | +-------------+
+------+------+ | Store with |
Yes | No | Cache-Ctrl |
| | +-------------+
| v
| [SERVE FROM CACHE]
|
v
+-------------+
| Has ETag? |
+------+------+
Yes | No
| |
| v
| [FETCH FRESH]
|
v
+-------------+
| Send If- |
| None-Match |
+------+------+
|
v
+-------------+ 304 +-------------+
| Response? |----------->| Refresh TTL |
+------+------+ | Serve cache |
| 200 +-------------+
v
+-------------+
| Replace |
| cache entry |
+-------------+
The Core Question You’re Answering
“How do companies like Cloudflare serve billions of requests per second without melting their origin servers? What’s the difference between a simple cache and an intelligent CDN?”
Before you write any code, sit with this question. The answer reveals the critical infrastructure layer that makes the modern web possible. A CDN is not just “a cache”—it’s a distributed system that must handle cache invalidation (the “hardest problem in computer science”), prevent thundering herds, respect HTTP semantics, and make intelligent decisions about what to cache, for how long, and for whom.
The magic is in the details: How do you handle a resource that varies by Accept-Encoding? What happens when 10,000 users request a cold cache entry simultaneously? How do you know when a cached response is still valid without asking the origin every time?
Concepts You Must Understand First
Stop and research these before coding:
- HTTP Caching Headers
- What’s the difference between
Cache-Control: max-age=300andExpires? - What do
public,private,no-cache, andno-storeactually mean? - How does
s-maxagediffer frommax-age? (Critical for shared caches!) - What’s the
Ageheader and who sets it? - Book Reference: “HTTP: The Definitive Guide” Ch. 7 - Gourley & Totty
- Book Reference: RFC 7234 - “HTTP/1.1 Caching”
- What’s the difference between
- Conditional Requests and Revalidation
- What’s an
ETagand how is it generated? - How does
If-None-Matchwork with ETags? - What’s the difference between
If-Modified-SinceandIf-None-Match? - When does the origin return 304 Not Modified vs 200 OK?
- Book Reference: “High Performance Browser Networking” Ch. 10 - Ilya Grigorik
- What’s an
- Cache Key Construction and the Vary Header
- Why is
Vary: Accept-Encodingso common? - How does
Varyaffect cache key construction? - What happens if you ignore
Varyand serve wrong content? - What’s the “cache key explosion” problem?
- Book Reference: RFC 7231 Section 7.1.4 (Vary)
- Why is
- Reverse Proxy vs Forward Proxy
- Who configures a forward proxy? A reverse proxy?
- What headers should a reverse proxy add (
X-Forwarded-For,X-Real-IP)? - How does the proxy handle the
Hostheader? - What’s the security model difference?
- Book Reference: “HTTP: The Definitive Guide” Ch. 6 - Proxies
- The Thundering Herd Problem and Cache Stampede
- What happens when a popular cached item expires?
- How does “request coalescing” solve this?
- What’s a “cache lock” and when would you use it?
- What’s “stale-while-revalidate” and how does it help?
- Book Reference: “High Performance Browser Networking” Ch. 10 - Ilya Grigorik
- Real-World Study: Read about Facebook’s TAO cache or Netflix’s EVCache
Questions to Guide Your Design
Before implementing, think through these:
Cache Storage Architecture
- How will you store cached responses? (Memory? Disk? Both?)
- What’s your eviction policy? (LRU? LFU? Size-based?)
- How will you handle responses larger than available memory?
- Should you store headers separately from bodies?
- How will you index by cache key for O(1) lookup?
Cache Invalidation Strategies
- How will you implement cache purging? (By URL? By pattern? By tag?)
- What happens if purge fails for some cached variants?
- How will you handle
Cache-Control: no-cachevsno-store? - Should you support “soft purge” (mark stale but keep) vs “hard purge” (delete)?
- How will you propagate purges across multiple proxy instances?
Load Balancing Implementation
- What algorithm will you use? (Round-robin? Least connections? Consistent hashing?)
- How will you detect unhealthy backends?
- What’s your health check strategy? (Active polling? Passive failure detection?)
- How will you handle backend recovery?
- Should you support sticky sessions?
Header Manipulation
- Which headers should you strip from cached responses?
- Which headers should you add? (
X-Cache,X-Cache-Lookup,Age, etc.) - How will you handle
ConnectionandKeep-Aliveheaders? - Should you rewrite
Locationheaders in redirects?
Thinking Exercise
Exercise 1: Trace a Cache Miss, Origin Fetch, and Cache Store Sequence
Given this request:
GET /api/products HTTP/1.1
Host: store.example.com
Accept-Encoding: gzip
Accept-Language: en-US
And this origin response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Cache-Control: public, max-age=300, s-maxage=600
ETag: "abc123"
Vary: Accept-Encoding
Content-Length: 4096
Answer these questions:
- What cache key would you construct? Include all components.
- What’s the effective TTL for your shared cache proxy?
- If another request comes with
Accept-Encoding: br, would it be a cache hit? - What should your proxy’s response include that the origin’s didn’t? (Hint:
Age) - After 400 seconds, what should happen on the next request?
Exercise 2: Design Cache Key with Complex Vary
The origin returns:
Vary: Accept-Encoding, Accept-Language, Cookie
Think through:
- How many cache variants could theoretically exist?
- Why is
Vary: Cookiedangerous? What would you do about it? - Design a cache key format that handles this correctly.
- What’s the trade-off between cacheability and correctness here?
Exercise 3: Solve the Thundering Herd
Scenario: Your cache entry for /popular.jpg expires at exactly 14:00:00. At that moment, 5,000 requests arrive within 100ms.
Design a solution:
- Without protection: How many origin requests would fire?
- With request coalescing: Draw the state machine for handling this.
- What data structure would you use to track “in-flight” requests?
- What happens if the origin request fails? How do you handle waiting requests?
- Bonus: How would
stale-while-revalidatechange your approach?
The Interview Questions They’ll Ask
Prepare to answer these:
- “Explain the difference between Cache-Control: no-cache and no-store.”
no-cachemeans “revalidate before using”;no-storemeans “never cache”
- “How would you design a cache key for an endpoint that returns different content based on the user’s Accept-Language header?”
- Include normalized Accept-Language in the key, handle Vary properly
- “What is the thundering herd problem and how would you solve it in a CDN?”
- Explain request coalescing, cache locks, stale-while-revalidate
- “How does an ETag work, and what’s the difference between strong and weak ETags?”
- Strong (
"abc") for byte-for-byte identical; Weak (W/"abc") for semantically equivalent
- Strong (
- “You need to purge a cached resource immediately across 100 edge servers. How would you design this?”
- Discuss push vs pull invalidation, consistency trade-offs, tag-based purging
- “What happens when a CDN receives a request for content it doesn’t have cached?”
- Walk through cache miss flow, origin selection, response caching, header modification
- “How would you handle a resource that should be cached but varies per user?”
- Discuss
Vary: Cookieproblems, edge-side includes, or fragment caching
- Discuss
- “What’s the difference between a forward proxy and a reverse proxy? When would you use each?”
- Forward: client-configured, outbound; Reverse: server-configured, inbound
Hints in Layers
Hint 1: Basic Proxy Pass-Through
Start with a simple proxy that forwards requests and responses without any caching:
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 1. Create request to origin
originURL := "http://localhost:9000" + r.URL.Path
originReq, _ := http.NewRequest(r.Method, originURL, r.Body)
// 2. Copy relevant headers
for k, v := range r.Header {
originReq.Header[k] = v
}
// 3. Add proxy headers
originReq.Header.Set("X-Forwarded-For", r.RemoteAddr)
// 4. Forward to origin
resp, _ := http.DefaultClient.Do(originReq)
// 5. Copy response headers
for k, v := range resp.Header {
w.Header()[k] = v
}
// 6. Copy status and body
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
Get this working first. Verify with curl -v.
Hint 2: Cache-Control Parsing
Parse the Cache-Control header into a struct you can reason about:
type CacheDirectives struct {
MaxAge int // seconds, -1 if not present
SMaxAge int // seconds, -1 if not present (for shared caches)
Public bool
Private bool
NoCache bool
NoStore bool
MustRevalidate bool
}
func parseCacheControl(header string) CacheDirectives {
directives := CacheDirectives{MaxAge: -1, SMaxAge: -1}
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(strings.ToLower(part))
if strings.HasPrefix(part, "max-age=") {
directives.MaxAge, _ = strconv.Atoi(part[8:])
} else if strings.HasPrefix(part, "s-maxage=") {
directives.SMaxAge, _ = strconv.Atoi(part[9:])
} else if part == "public" {
directives.Public = true
}
// ... etc
}
return directives
}
func effectiveTTL(d CacheDirectives) int {
if d.NoStore {
return 0
}
if d.SMaxAge >= 0 {
return d.SMaxAge // s-maxage takes precedence for shared caches
}
if d.MaxAge >= 0 {
return d.MaxAge
}
return 0 // Don't cache by default
}
Hint 3: ETag and Conditional Request Handling
When your cached entry is stale, don’t immediately fetch fresh—use conditional requests:
func fetchFromOrigin(cacheEntry *CacheEntry, originURL string) (*http.Response, error) {
req, _ := http.NewRequest("GET", originURL, nil)
// Add conditional headers if we have a cached version
if cacheEntry != nil {
if cacheEntry.ETag != "" {
req.Header.Set("If-None-Match", cacheEntry.ETag)
}
if !cacheEntry.LastModified.IsZero() {
req.Header.Set("If-Modified-Since",
cacheEntry.LastModified.Format(http.TimeFormat))
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotModified {
// Origin says our cached version is still valid!
// Refresh TTL but serve cached content
cacheEntry.RefreshTTL()
return nil, ErrUseCache
}
return resp, nil // New content, replace cache entry
}
Hint 4: Request Coalescing for Thundering Herd
Use a map of in-flight requests and sync primitives to coalesce:
type InFlightRequest struct {
done chan struct{}
result *CacheEntry
err error
}
type Cache struct {
entries map[string]*CacheEntry
inFlight map[string]*InFlightRequest
mu sync.Mutex
}
func (c *Cache) GetOrFetch(key string, fetcher func() (*CacheEntry, error)) (*CacheEntry, error) {
c.mu.Lock()
// Check if already in cache
if entry, ok := c.entries[key]; ok && !entry.IsExpired() {
c.mu.Unlock()
return entry, nil
}
// Check if request already in flight
if inFlight, ok := c.inFlight[key]; ok {
c.mu.Unlock()
<-inFlight.done // Wait for the existing request
return inFlight.result, inFlight.err
}
// We're the first! Create in-flight entry
inFlight := &InFlightRequest{done: make(chan struct{})}
c.inFlight[key] = inFlight
c.mu.Unlock()
// Actually fetch from origin
entry, err := fetcher()
// Store result and notify waiters
c.mu.Lock()
inFlight.result = entry
inFlight.err = err
if err == nil {
c.entries[key] = entry
}
delete(c.inFlight, key)
close(inFlight.done) // Wake up all waiters
c.mu.Unlock()
return entry, err
}
This pattern ensures that 10,000 concurrent requests for the same key result in exactly 1 origin request.
Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| HTTP Caching Fundamentals | “HTTP: The Definitive Guide” by Gourley & Totty | Ch. 7: Caching |
| Cache-Control Deep Dive | RFC 7234 | Sections 5.2 (Cache-Control), 4.2 (Freshness) |
| Browser Caching Behavior | “High Performance Browser Networking” by Ilya Grigorik | Ch. 10: Primer on Web Performance |
| Proxy Architecture | “HTTP: The Definitive Guide” by Gourley & Totty | Ch. 6: Proxies |
| Conditional Requests | RFC 7232 | Sections 2-3 (Validators, Precondition Headers) |
| Vary Header Semantics | RFC 7231 | Section 7.1.4 |
| Distributed Caching | “Designing Data-Intensive Applications” by Martin Kleppmann | Ch. 5: Replication, Ch. 6: Partitioning |
| CDN Architecture | “Web Scalability for Startup Engineers” by Ejsmont | Ch. 7: Building Scalable Web Caches |
Common Pitfalls & Debugging
Problem 1: “Cache always misses, serving from origin every time”
- Why: Your cache key doesn’t account for important request headers (Vary header), or you’re not parsing Cache-Control correctly
- Fix: Include
Varyheader values in cache key. IfVary: Accept-Encoding, cache separate entries for gzip vs non-gzip - Quick test:
curl -H "Accept-Encoding: gzip" localhost:8080/file.txttwice, second request should be cached
Problem 2: “Stale content served even after origin updates”
- Why: Not implementing cache revalidation with If-None-Match/If-Modified-Since, or ignoring max-age
- Fix: Check entry age against
max-age/s-maxage. If stale, make conditional request with ETag/Last-Modified - Quick test: Update origin file, wait for TTL expiry, verify proxy fetches fresh version
Problem 3: “Thundering herd - origin gets hammered when cache expires”
- Why: Multiple concurrent requests for expired key all trigger origin requests simultaneously
- Fix: Implement request coalescing: track in-flight requests in a map, subsequent requests wait on a channel for the first request to complete
- Quick test: Use
ab -n 100 -c 100 http://localhost:8080/right after cache expiry, verify only 1 origin request
Problem 4: “Private or sensitive data gets cached and leaked”
- Why: Not respecting
Cache-Control: private,no-store, orAuthorizationheader - Fix: Never cache responses with
Authorizationheader unless explicitlyCache-Control: public. Honorprivateandno-store - Quick test: Send request with
Authorization: Bearer token, verify it’s never cached
Problem 5: “Cache grows indefinitely, runs out of memory”
- Why: No eviction policy (LRU, LFU) or max cache size limit
- Fix: Implement LRU eviction: track access times, evict least recently used when size limit exceeded
- Quick test: Set max cache size to 10MB, request 100MB of files, verify cache size stays under limit
Problem 6: “Vary header causes excessive cache fragmentation”
- Why: Caching every combination of Vary headers (User-Agent, Accept-Language) creates too many entries
- Fix: Normalize common Vary headers or only cache on specific Vary values (e.g., Accept-Encoding: gzip vs identity)
- Quick test: Send requests with 100 different User-Agent strings, monitor cache size and hit rate
Debugging Tools Checklist
# Test cache behavior with curl
curl -v http://localhost:8080/file.txt # First request (MISS)
curl -v http://localhost:8080/file.txt # Second request (HIT - check Age header)
# Verify Cache-Control headers
curl -I http://localhost:8080/file.txt | grep -i cache
# Test conditional requests
curl -H "If-None-Match: \"abc123\"" http://localhost:8080/file.txt
# Should return 304 if ETag matches
# Test Vary header handling
curl -H "Accept-Encoding: gzip" http://localhost:8080/file.txt
curl -H "Accept-Encoding: identity" http://localhost:8080/file.txt
# Should be separate cache entries
# Load test to verify thundering herd protection
ab -n 1000 -c 100 http://localhost:8080/expensive-api
# Monitor origin request count (should be ~1 per cache expiry)
# Verify X-Forwarded-For preservation
curl -v http://localhost:8080/echo-ip
# Origin should see proxy's X-Forwarded-For, not proxy's IP
# Cache statistics endpoint (implement this)
curl http://localhost:8080/_stats
# Example output:
# {
# "hits": 1543,
# "misses": 89,
# "hit_rate": 0.945,
# "entries": 127,
# "size_bytes": 4589231
# }
# Purge specific cache entry
curl -X PURGE http://localhost:8080/file.txt
Project 7: QUIC/HTTP/3 “Hello World” Client
- File: APPLICATION_LAYER_NETWORKING_MASTERY.md
- Main Programming Language: Rust (using
quicheorquinn) or Go (usingquic-go) - Alternative Programming Languages: C++ (using
mvfst) - Coolness Level: Level 5: Pure Magic (Super Cool)
- Business Potential: 5. The “Industry Disruptor”
- Difficulty: Level 5: Master
- Knowledge Area: HTTP/3 / QUIC / Modern Web
- Software or Tool: UDP / QUIC Libraries
- Main Book: “QUIC” (Online RFCs and blog posts by Fastly/Cloudflare)
What you’ll build: A client that makes an HTTP/3 request over QUIC. You’ll learn how QUIC combines the handshake and encryption into a single step over UDP.
Why it teaches Networking: This is the bleeding edge. You’ll understand why the industry is moving away from TCP and how QUIC handles reliability, congestion control, and stream multiplexing entirely in user-space.
Core challenges you’ll face:
- UDP-based Reliability: Understanding how QUIC acknowledges packets without the kernel’s help.
- Connection IDs: How QUIC maintains a connection even when your IP changes (e.g., switching from Wi-Fi to 4G).
- 0-RTT Resumption: Implementing the logic to send data in the very first packet of a re-connection.
- QPACK: The new version of HPACK designed for out-of-order delivery.
Key Concepts:
- QUIC Invariants: RFC 8999
- Stream Multiplexing over UDP: Why this prevents head-of-line blocking.
- TLS 1.3 integration in QUIC: RFC 9001
Difficulty: Master Time estimate: 3-4 weeks Prerequisites: Project 3 (TLS) and Project 5 (H2 framing).
Real World Outcome
A tool qget that fetches a page over HTTP/3 and shows the performance improvement over high-loss networks. You’ll see exactly how QUIC combines the TLS handshake with connection establishment, how connection IDs enable seamless network migration, and why HTTP/3 outperforms TCP-based protocols on unreliable networks.
Example Output:
$ ./qget https://cloudflare-quic.com/ --verbose
[QUIC/HTTP3 Client - qget v1.0]
Target: cloudflare-quic.com:443
===============================================================
PHASE 1: Initial Connection (0-RTT Attempt)
===============================================================
[UDP] Binding to local port 54321
[QUIC] Generating Initial Connection ID: 0x1a2b3c4d5e6f7890
[QUIC] Creating Initial packet (Long Header)
Version: 0x00000001 (QUIC v1)
DCID: 0x1a2b3c4d5e6f7890 (8 bytes)
SCID: 0xdeadbeefcafe1234 (8 bytes)
Token: [empty - first connection]
[CRYPTO] Generating TLS 1.3 ClientHello embedded in QUIC...
[CRYPTO] Key Exchange: X25519
[CRYPTO] Cipher Suite: TLS_AES_128_GCM_SHA256
===============================================================
PHASE 2: Server Response (1-RTT Handshake)
===============================================================
[UDP] Received 1200 bytes from 104.16.123.96:443
[QUIC] Parsing Initial packet from server
Version: 0x00000001 (QUIC v1)
DCID: 0xdeadbeefcafe1234 (matches our SCID)
SCID: 0xfedcba9876543210 (server's CID)
[CRYPTO] ServerHello received in CRYPTO frame
[CRYPTO] Selected: TLS_AES_128_GCM_SHA256
[CRYPTO] Computing shared secret via ECDHE...
[CRYPTO] Deriving Initial secrets (HKDF)
|-- client_initial_secret: 32 bytes
|-- server_initial_secret: 32 bytes
+-- handshake_secret: 32 bytes
[QUIC] Switching to Handshake packet space
[CRYPTO] Certificate chain received (3 certs)
|-- [0] CN=cloudflare-quic.com (leaf)
|-- [1] CN=Cloudflare Inc ECC CA-3 (intermediate)
+-- [2] CN=Baltimore CyberTrust Root (root) [OK]
[CRYPTO] CertificateVerify: VALID [OK]
[CRYPTO] Server Finished: VALID [OK]
[CRYPTO] Deriving application secrets...
|-- client_app_key: 16 bytes
|-- server_app_key: 16 bytes
+-- master_secret: 32 bytes
===============================================================
PHASE 3: Connection Established
===============================================================
[QUIC] Handshake complete!
[QUIC] Switching to Short Header packets
New DCID: 0xfedcba98 (4 bytes - server chose shorter)
Packet Number Space: Application Data
[QUIC] Connection Parameters Negotiated:
|-- max_idle_timeout: 30000ms
|-- max_udp_payload_size: 1350 bytes
|-- initial_max_data: 1048576 bytes
|-- initial_max_stream_data_bidi_local: 65536 bytes
|-- initial_max_streams_bidi: 100
+-- active_connection_id_limit: 4
===============================================================
PHASE 4: HTTP/3 Request
===============================================================
[H3] Opening bidirectional stream ID: 0
[H3] Creating HEADERS frame
[QPACK] Encoding request headers:
|-- :method = GET (static table index 17)
|-- :scheme = https (static table index 23)
|-- :authority = cloudflare-quic.com (literal with indexing)
|-- :path = / (static table index 1)
+-- user-agent = qget/1.0 (literal without indexing)
[QPACK] Encoded headers: 47 bytes (vs 89 bytes uncompressed)
[H3] Sending HEADERS frame on Stream 0 (FIN=false)
===============================================================
PHASE 5: HTTP/3 Response
===============================================================
[QUIC] Received Short Header packet (32 bytes protected)
[H3] HEADERS frame received on Stream 0
[QPACK] Decoding response headers:
|-- :status = 200 (static table index 25)
|-- content-type = text/html; charset=utf-8
|-- content-length = 15842
|-- server = cloudflare
+-- alt-svc = h3=":443"
[H3] DATA frames received:
|-- Frame 1: 1200 bytes
|-- Frame 2: 1200 bytes
|-- [...]
+-- Frame 14: 442 bytes (FIN=true)
[H3] Stream 0 complete: 15842 bytes received
===============================================================
CONNECTION SUMMARY
===============================================================
+-------------------------------------------------------------+
| QUIC/HTTP3 Connection Statistics |
+-------------------------------------------------------------+
| Protocol: QUIC v1 + HTTP/3 |
| TLS Version: 1.3 (embedded) |
| Cipher: TLS_AES_128_GCM_SHA256 |
| Key Exchange: X25519 |
| |
| Handshake: 1-RTT (87ms) |
| First Byte: 89ms |
| Total Time: 124ms |
| |
| Packets Sent: 18 |
| Packets Received: 22 |
| Bytes Sent: 1,247 |
| Bytes Received: 17,891 |
| |
| Streams Used: 1 (bidirectional) |
| Connection IDs: 2 (rotated once) |
+-------------------------------------------------------------+
QUIC vs TCP+TLS Performance Comparison:
TCP + TLS 1.3 (HTTP/2):
+---------+---------+---------+---------+---------+
| RTT 1 | RTT 2 | RTT 3 | RTT 4 | Total |
+---------+---------+---------+---------+---------+
|TCP SYN |TLS |TLS |HTTP | |
|TCP SYN- |Client |Finished |Request | |
|ACK |Hello | | | |
| |Server | |Response | |
| |Hello+ | | | |
| |Cert | | | |
+---------+---------+---------+---------+---------+
| 50ms | 50ms | 50ms | 50ms | 200ms |
+---------+---------+---------+---------+---------+
^ TCP handshake + TLS 1.3 (2-RTT) + Request
QUIC (HTTP/3):
+---------+---------+---------+
| RTT 1 | RTT 2 | Total |
+---------+---------+---------+
|QUIC |HTTP/3 | |
|Initial |Response | |
|+ TLS | | |
|Client | | |
|Hello | | |
| | | |
|Server | | |
|Hello+ | | |
|Finished | | |
|+ H3 Req | | |
+---------+---------+---------+
| 50ms | 50ms | 100ms | <-- 50% faster!
+---------+---------+---------+
^ Combined handshake + encrypted request
QUIC 0-RTT Resumption (Returning Visitor):
+---------+---------+
| RTT 1 | Total |
+---------+---------+
|0-RTT | |
|Early | |
|Data + | |
|HTTP Req | |
| | |
|Response | |
+---------+---------+
| 50ms | 50ms | <-- Request in FIRST packet!
+---------+---------+
Why QUIC Avoids Head-of-Line Blocking:
HTTP/2 over TCP (Single TCP Stream):
+--------------------------------------------------------------+
| TCP Connection |
| +---+---+---+---+---+---+---+---+---+---+---+---+---+---+ |
| |S1 |S2 |S1 |S3 |S2 | X |S3 |S1 |S2 |S3 |S1 |S2 |S3 |...| |
| +---+---+---+---+---+-+-+---+---+---+---+---+---+---+---+ |
| | |
| Packet Lost! |
| | |
| v |
| +-----------------------------------------------------+ |
| | ALL streams blocked waiting for retransmission | |
| | Stream 1: BLOCKED ================............ | |
| | Stream 2: BLOCKED ==========.................. | |
| | Stream 3: BLOCKED ========.................... | |
| +-----------------------------------------------------+ |
| Even though packets for S1, S2, S3 arrived after loss, |
| TCP forces in-order delivery, blocking EVERYTHING. |
+--------------------------------------------------------------+
QUIC (Independent Streams):
+--------------------------------------------------------------+
| QUIC Connection (UDP) |
| +--------+ +--------+ +--------+ |
| |Stream 1| |Stream 2| |Stream 3| <-- Independent streams! |
| +---+----+ +---+----+ +---+----+ |
| | | | |
| +--+--+ +--+--+ +--+--+ |
| |Pkt 1| |Pkt 1| |Pkt 1| |
| |Pkt 2| | X | |Pkt 1| <-- Packet lost on Stream 2 |
| |Pkt 3| |Pkt 3| |Pkt 2| |
| +-----+ +-----+ +-----+ |
| | | | |
| v v v |
| +-----------------------------------------------------+ |
| | Only Stream 2 is blocked | |
| | Stream 1: COMPLETE ========================= | |
| | Stream 2: BLOCKED =========............... <-wait | |
| | Stream 3: COMPLETE ========================= | |
| +-----------------------------------------------------+ |
| Streams 1 and 3 complete immediately! Only Stream 2 waits. |
+--------------------------------------------------------------+
Connection Migration with Connection IDs:
Traditional TCP (Connection = 4-tuple):
+---------------------------------------------------------------+
| User on Coffee Shop WiFi |
| Connection: 192.168.1.50:54321 <--> 104.16.123.96:443 |
| |
| [User walks outside] |
| | |
| v |
| User switches to 4G: 10.0.0.25:54321 |
| |
| +===========================================================+ |
| | TCP CONNECTION BROKEN! | |
| | New IP address = New TCP handshake + New TLS handshake | |
| | Lost: All in-flight data, session state, HTTP/2 streams | |
| +===========================================================+ |
+---------------------------------------------------------------+
QUIC (Connection = Connection ID):
+---------------------------------------------------------------+
| User on Coffee Shop WiFi |
| IP: 192.168.1.50:54321 |
| QUIC CID: 0xABCDEF1234567890 |
| |
| [User walks outside] |
| | |
| v |
| User switches to 4G: 10.0.0.25:54321 |
| QUIC CID: 0xABCDEF1234567890 (SAME!) |
| |
| +-----------------------------------------------------------+ |
| | Server receives packet from new IP but recognizes CID! | |
| | | |
| | Server: "CID 0xABCDEF... from new IP? Let me verify..." | |
| | PATH_CHALLENGE --> Client | |
| | Client: PATH_RESPONSE (proves ownership of CID) | |
| | Server: "Verified! Updating path, connection continues." | |
| | | |
| | [OK] Zero RTT for path migration | |
| | [OK] All streams preserved | |
| | [OK] In-flight data delivered | |
| | [OK] No new handshake required | |
| +-----------------------------------------------------------+ |
+---------------------------------------------------------------+
The Core Question You’re Answering
“Why is the internet moving from TCP to UDP, and how can UDP-based protocols be reliable without the kernel’s help?”
Before you write any code, sit with this question. For decades, we’ve been taught that TCP provides reliability and UDP doesn’t. QUIC challenges this fundamental assumption by implementing reliability, ordering, and flow control entirely in user space. The answer reveals why: TCP’s reliability is too rigid. It enforces head-of-line blocking because it guarantees byte-stream ordering across the entire connection. But web pages don’t need this–if a CSS file packet is lost, why should the JavaScript file wait? QUIC’s insight is that reliability should be per-stream, not per-connection. This single change enables massive performance gains on lossy networks (mobile, satellite, congested WiFi).
Concepts You Must Understand First
Stop and research these before coding:
- QUIC Packet Structure (Long vs Short Headers)
- What are the two types of QUIC headers and when is each used?
- What fields are in the Long Header that aren’t in Short Header?
- Why does the Initial packet need protection even before the handshake completes?
- How does Initial packet protection use connection ID-derived keys?
- RFC Reference: RFC 9000 Section 17 - Packet Formats
- Connection IDs and Connection Migration
- Why does QUIC use Connection IDs instead of the IP/port 4-tuple?
- What’s the difference between Source and Destination Connection IDs?
- How does the server validate a client that appears from a new IP address?
- What are PATH_CHALLENGE and PATH_RESPONSE frames?
- RFC Reference: RFC 9000 Section 9 - Connection Migration
- Stream Multiplexing Without Head-of-Line Blocking
- How does QUIC number streams (why are client-initiated odd and server-initiated even)?
- What’s the difference between unidirectional and bidirectional streams?
- How does losing a packet on one stream NOT block other streams?
- What are the flow control mechanisms (stream-level vs connection-level)?
- RFC Reference: RFC 9000 Sections 2-3 - Streams
- 0-RTT and 1-RTT Handshakes
- What makes QUIC handshakes faster than TCP+TLS?
- What is “early data” and when can it be sent?
- What are the security risks of 0-RTT (replay attacks)?
- How does QUIC integrate TLS 1.3 differently than TLS-over-TCP?
- RFC Reference: RFC 9001 - Using TLS to Secure QUIC
- QPACK (HTTP/3’s Header Compression)
- Why couldn’t HTTP/3 use HPACK directly?
- What problem does out-of-order delivery cause for stateful compression?
- How do encoder and decoder streams solve this problem?
- What’s the difference between static and dynamic table entries in QPACK?
- RFC Reference: RFC 9204 - QPACK: Field Compression for HTTP/3
- Integration of TLS 1.3 into QUIC
- How is the TLS handshake carried in CRYPTO frames?
- What are the three packet number spaces (Initial, Handshake, Application)?
- How does QUIC derive encryption keys at each phase?
- Why does QUIC encrypt nearly everything (including packet numbers)?
- RFC Reference: RFC 9001 Section 4 - Carrying TLS Messages
Questions to Guide Your Design
Before implementing, think through these:
Connection Establishment
- How will you send the Initial packet with ClientHello?
- How will you derive the Initial keys from the Destination Connection ID?
- What happens if the server sends a Retry packet?
- How do you handle version negotiation?
Stream Management
- How will you track which streams are open, half-closed, or fully closed?
- How do you handle receiving data on a stream you haven’t opened?
- What limits do you need to enforce (max streams, max data per stream)?
- How do you send flow control updates (MAX_DATA, MAX_STREAM_DATA)?
Packet Loss and Recovery
- How do you detect packet loss (ACK-based vs timeout)?
- How do you distinguish congestion loss from reordering?
- What data structures track sent packets awaiting acknowledgment?
- How do you handle ACK frames (which use a compact encoding)?
HTTP/3 Layer
- What streams are required for HTTP/3 control (stream 0, 2, 4)?
- How do you send a HEADERS frame with QPACK encoding?
- How do you handle DATA frames and flow control interaction?
- What happens if the server sends a GOAWAY frame?
Thinking Exercise
Compare TCP+TLS+HTTP/2 vs QUIC+HTTP/3 Handshakes
Exercise 1: Draw the RTT diagram
For a new connection to https://example.com:
TCP + TLS 1.3 + HTTP/2:
Client Server
|------ TCP SYN ------------------>| RTT 1
|<----- TCP SYN-ACK ---------------|
|------ TCP ACK ------------------>|
|------ TLS ClientHello ---------->| RTT 2
|<----- TLS ServerHello + Cert ----|
|------ TLS Finished ------------->| RTT 3
|<----- TLS Finished --------------|
|------ HTTP/2 HEADERS (GET /) --->| RTT 4
|<----- HTTP/2 Response -----------|
Total: 4 RTTs before response (or 3 with TLS early data)
QUIC + HTTP/3:
Client Server
|------ Initial + ClientHello ---->| RTT 1
|<----- Initial + ServerHello -----|
|<----- Handshake + Cert + Fin ----|
|------ Handshake + Fin ---------->| RTT 2
|------ Short + HTTP/3 Request --->|
|<----- Short + HTTP/3 Response ---|
Total: 2 RTTs (or 1 RTT with 0-RTT early data)
Answer these questions:
- How many RTTs are saved by combining TCP+TLS handshakes?
- When can the HTTP request first be sent in each case?
- What data could be sent in 0-RTT, and what are the risks?
Exercise 2: Draw the packet loss scenario
Given a page with 3 resources (HTML, CSS, JS) each on its own stream:
HTTP/2 over TCP with packet loss:
TCP Sequence: [HTML:1][CSS:1][JS:1][HTML:2][CSS:2][LOST!][JS:2][HTML:3]...
^
TCP blocks HERE
|
v
All streams blocked waiting for [LOST] packet retransmission
QUIC with same packet loss:
QUIC Packets: [Stream 0: HTML][Stream 4: CSS][Stream 8: JS]...
^ [LOST!]
|
+-> Only Stream 0 blocked
Streams 4 and 8 continue receiving!
Diagram what the application sees:
- In HTTP/2: When can each resource be used by the browser?
- In QUIC: When can each resource be used by the browser?
Exercise 3: Trace a 0-RTT resumption
Given:
- Previous session stored
resumption_secret - Server’s
NEW_TOKENfrom previous connection
Draw the packet exchange:
Client Server
|------ [Initial + 0-RTT Data] --->|
| Contains: |
| - ClientHello with early_data
| - HTTP/3 request (protected |
| with early keys) |
| |
|<----- [Initial + Handshake] -----|
| Server processes 0-RTT! |
|<----- [Short + Response] --------|
| |
Response arrives before handshake completes!
Answer:
- How is the 0-RTT data protected if the handshake isn’t complete?
- What happens if the server rejects 0-RTT data?
- Why is 0-RTT data vulnerable to replay attacks?
The Interview Questions They’ll Ask
Prepare to answer these:
-
“Explain why QUIC uses UDP instead of TCP. What problems does this solve?”
-
“What is head-of-line blocking, and how does QUIC eliminate it compared to HTTP/2?”
-
“How does QUIC maintain a connection when the client’s IP address changes (e.g., WiFi to cellular)?”
-
“Walk me through the QUIC handshake. How many round trips does it take for a new connection vs a resumption?”
-
“What are the security risks of 0-RTT data, and how does QUIC mitigate them?”
-
“How does QPACK differ from HPACK? Why couldn’t HTTP/3 use HPACK directly?”
-
“Explain QUIC’s packet number encryption. Why is this necessary when the payload is already encrypted?”
-
“How does QUIC handle packet loss and retransmission differently than TCP? What are the trade-offs?”
Hints in Layers
Hint 1: Using quiche/quinn Libraries Don’t implement QUIC packet protection yourself. Use established libraries:
- Rust:
quinn(high-level) orquiche(lower-level, used by Cloudflare) - Go:
quic-go(widely used, full HTTP/3 support) - C/C++:
quiche(from Cloudflare) ormvfst(from Facebook)
Start by establishing a connection to a known HTTP/3 server like cloudflare-quic.com or quic.rocks.
Hint 2: Connection Setup
// Pseudocode for connection setup
let mut config = Config::new(quiche::PROTOCOL_VERSION)?;
config.set_application_protos(&[b"h3"])?; // HTTP/3
config.set_max_idle_timeout(30_000);
config.set_initial_max_streams_bidi(100);
// For client, generate source connection ID
let scid = generate_random_connection_id(16);
// Create connection
let conn = quiche::connect(
Some("cloudflare-quic.com"), // SNI
&scid,
local_addr,
peer_addr,
&mut config,
)?;
// Send Initial packet
let mut out = [0u8; 1350];
let (write, info) = conn.send(&mut out)?;
socket.send_to(&out[..write], info.to)?;
Hint 3: Stream Creation and Management
// HTTP/3 uses specific stream IDs:
// - Stream 0: Control stream (client-initiated, unidirectional)
// - Stream 2: QPACK encoder stream
// - Stream 6: QPACK decoder stream
// - Stream 0, 4, 8...: Request streams (client-initiated, bidirectional)
// Create HTTP/3 connection over QUIC
let h3_conn = quiche::h3::Connection::with_transport(&mut conn, &h3_config)?;
// Send request on stream
let headers = vec![
quiche::h3::Header::new(b":method", b"GET"),
quiche::h3::Header::new(b":scheme", b"https"),
quiche::h3::Header::new(b":authority", b"cloudflare-quic.com"),
quiche::h3::Header::new(b":path", b"/"),
];
let stream_id = h3_conn.send_request(&mut conn, &headers, true)?; // true = FIN
Hint 4: Sending HTTP/3 Request Frames
// Main event loop
loop {
// Receive incoming packets
let (len, from) = socket.recv_from(&mut buf)?;
// Process with QUIC
let recv_info = quiche::RecvInfo { from };
conn.recv(&mut buf[..len], recv_info)?;
// Process HTTP/3 events
loop {
match h3_conn.poll(&mut conn) {
Ok((stream_id, quiche::h3::Event::Headers { list, has_body })) => {
println!("Response headers on stream {}:", stream_id);
for header in &list {
println!(" {}: {}",
String::from_utf8_lossy(header.name()),
String::from_utf8_lossy(header.value()));
}
}
Ok((stream_id, quiche::h3::Event::Data)) => {
let mut body = vec![0u8; 65535];
while let Ok(len) = h3_conn.recv_body(&mut conn, stream_id, &mut body) {
println!("Body ({} bytes): {}", len,
String::from_utf8_lossy(&body[..len]));
}
}
Ok((_, quiche::h3::Event::Finished)) => break,
Err(quiche::h3::Error::Done) => break,
Err(e) => return Err(e.into()),
}
}
// Send any pending packets
while let Ok((len, info)) = conn.send(&mut out) {
socket.send_to(&out[..len], info.to)?;
}
if conn.is_closed() {
break;
}
}
Books That Will Help
| Topic | Resource | Section |
|---|---|---|
| QUIC Core Protocol | RFC 9000 | Entire spec (essential) |
| QUIC + TLS Integration | RFC 9001 | Sections 1-5 |
| QUIC Loss Detection | RFC 9002 | Congestion control |
| HTTP/3 Specification | RFC 9114 | Entire spec |
| QPACK Header Compression | RFC 9204 | Sections 1-4 |
| QUIC Invariants | RFC 8999 | Version-independent properties |
| QUIC Performance | “HTTP/3 Explained” by Daniel Stenberg | Online: http3-explained.haxx.se |
| Modern Web Protocols | “High Performance Browser Networking” by Ilya Grigorik | Ch. 13 (QUIC) |
| Cloudflare QUIC Blog | Cloudflare Blog | “The Road to QUIC” series |
| Google QUIC Origins | Google Research | “QUIC: A UDP-Based Multiplexed and Secure Transport” |
Online Resources (Essential for QUIC)
Since QUIC is new (RFC finalized 2021), online resources are critical:
| Resource | URL | What You’ll Learn |
|---|---|---|
| http3-explained | https://http3-explained.haxx.se/ | Best beginner-friendly QUIC/H3 guide |
| QUIC Working Group | https://quicwg.org/ | Official specs and errata |
| Cloudflare QUIC | https://blog.cloudflare.com/tag/quic/ | Practical deployment insights |
| quiche Library | https://github.com/cloudflare/quiche | Reference implementation |
| quinn Library | https://github.com/quinn-rs/quinn | Rust async QUIC |
| quic-go | https://github.com/quic-go/quic-go | Go implementation |
Common Pitfalls & Debugging
Problem 1: “Connection fails immediately with version negotiation error”
- Why: Client and server QUIC versions don’t match, or you’re not handling version negotiation packets
- Fix: Ensure you’re using a QUIC version supported by the server (v1, RFC 9000). Check for version negotiation packet (long header with version 0x00000000)
- Quick test:
curl --http3 -v https://cloudflare-quic.comto verify your network allows UDP/443
Problem 2: “Initial packet gets no response from server”
- Why: Firewall blocking UDP/443, incorrect Initial packet format, or invalid crypto
- Fix: Verify UDP/443 is open. Ensure Initial packet has correct long header format, includes TLS ClientHello in CRYPTO frame
- Quick test: Use Wireshark filter
quicto verify Initial packet structure matches RFC 9000 Section 17.2.2
Problem 3: “TLS handshake fails during QUIC connection”
- Why: QUIC integrates TLS 1.3 differently - crypto happens in CRYPTO frames, not records
- Fix: Use QUIC-aware TLS library (quiche, quinn). Don’t try to layer standard TLS over QUIC manually
- Quick test: Verify you’re sending TLS messages in QUIC CRYPTO frames (frame type 0x06), not raw TLS records
Problem 4: “Connection ID mismatch errors”
- Why: Not correctly tracking source/destination connection IDs or not handling NEW_CONNECTION_ID frames
- Fix: Server chooses dest CID, client chooses source CID. Track both and swap appropriately in packet headers
- Quick test: Print CIDs in hex for every packet sent/received, verify they match expectations
Problem 5: “Packets arrive but connection times out”
- Why: Not acknowledging packets (missing ACK frames) or not processing server packets
- Fix: Send ACK frames for every received packet number. Process all incoming frames in event loop
- Quick test: Wireshark filter
quic.frame_type == 0x02to verify ACK frames are being exchanged
Problem 6: “HTTP/3 requests fail after QUIC connection succeeds”
- Why: Missing HTTP/3 SETTINGS frame, incorrect stream types (unidirectional vs bidirectional), or QPACK setup issues
- Fix: After QUIC handshake, immediately send HTTP/3 SETTINGS on control stream (stream ID 2 or 3). Use bidirectional client-initiated streams for requests
- Quick test: Verify you’re sending HTTP/3 SETTINGS frame (type 0x04) before any HTTP/3 HEADERS
Problem 7: “UDP socket receive buffer overflow or packet loss”
- Why: Default UDP receive buffer too small for QUIC’s high-throughput streams
- Fix: Increase UDP socket buffer:
setsockopt(SO_RCVBUF, 2MB)andsetsockopt(SO_SNDBUF, 2MB) - Quick test: Monitor with
netstat -su | grep 'packet receive errors'
Debugging Tools Checklist
# Test QUIC/HTTP/3 support of a server
curl --http3 -v https://cloudflare.com
curl --http3 -v https://google.com
# Capture QUIC traffic with Wireshark
sudo tcpdump -i any -w quic.pcap udp port 443
# Open in Wireshark, filter: quic
# Verify server supports HTTP/3 (check alt-svc header)
curl -I https://cloudflare.com | grep -i alt-svc
# Should show: alt-svc: h3=":443"; ma=86400
# Test with different QUIC implementations
# Using quiche-client (Cloudflare)
cargo install quiche
quiche-client https://cloudflare.com
# Using quinn (Rust async)
# See: https://github.com/quinn-rs/quinn/tree/main/quinn/examples
# Using quic-go (Go)
# See: https://github.com/quic-go/quic-go/tree/master/example
# Analyze QUIC performance vs TCP
h2load --h1 https://example.com # HTTP/1.1 over TLS
h2load --h2 https://example.com # HTTP/2 over TLS
h2load --h3 https://example.com # HTTP/3 over QUIC
# Debug connection IDs and packet numbers
# In your code, log every sent/received packet:
# "SEND: DestCID=abc123 SrcCID=def456 PacketNum=5 Type=Initial"
# "RECV: DestCID=def456 SrcCID=abc123 PacketNum=2 Type=Handshake"
# Check if your ISP/network blocks QUIC
# (Some cellular networks block UDP/443)
nmap -sU -p 443 cloudflare.com
# If filtered, QUIC won't work
# Test 0-RTT resumption (advanced)
# First connection:
quiche-client --dump-session-file session.bin https://cloudflare.com
# Second connection (should be 0-RTT):
quiche-client --session-file session.bin https://cloudflare.com
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor | Reusability |
|---|---|---|---|---|---|
| Recursive DNS Resolver | ⭐⭐ | 1 week | High (Binary, Distributed) | ⭐⭐⭐ | DNS Troubleshooting |
| HTTP/1.1 Server | ⭐⭐⭐ | 2 weeks | High (State Machines, I/O) | ⭐⭐⭐ | Base for proxies |
| TLS Handshake Explorer | ⭐⭐⭐⭐ | 3 weeks | Extreme (Security, PKI) | ⭐⭐⭐⭐ | Security Auditing |
| WebSocket Chat | ⭐⭐ | 1 week | Medium (Binary Framing) | ⭐⭐⭐⭐⭐ | Real-time apps |
| HTTP/2 Frame Parser | ⭐⭐⭐⭐ | 3 weeks | Extreme (Compression, Mux) | ⭐⭐⭐ | Performance tools |
| Mini-CDN Proxy | ⭐⭐⭐ | 2 weeks | High (Caching, Ops) | ⭐⭐⭐⭐ | Personal infra |
| QUIC/H3 Client | ⭐⭐⭐⭐⭐ | 4 weeks | Extreme (Modern Network) | ⭐⭐⭐⭐⭐ | Bleeding edge research |
Recommendation
If you are new to networking, start with Project 1 (DNS Resolver). It’s the most rewarding “Aha!” moment when you realize you don’t need 8.8.8.8 to find an IP address.
If you already know the basics, Project 2 (HTTP/1.1 Server) is the mandatory foundation for everything else in this path.
Final Overall Project: The “HyperProxy” Edge Gateway
What you’ll build: A unified edge gateway that supports HTTP/1.1, HTTP/2, and HTTP/3 on the frontend, and proxies to various backends. It must feature:
- Automatic TLS Termination (via Let’s Encrypt / ACME).
- Protocol Translation: Convert incoming HTTP/3 requests to HTTP/1.1 for legacy backends.
- Intelligent Gzip/Brotli Compression on the fly.
- DDoS Mitigation: Rate limiting and connection tracking.
- Live Metrics Dashboard: Real-time visualization of protocol distribution and cache hit rates.
Why this is the ultimate test: This project forces you to integrate every single concept from the sprint. You’ll handle the extreme complexity of protocol translation, the security requirements of modern TLS, and the performance needs of a production-grade gateway.
Difficulty: Master Time estimate: 2 months
Real World Outcome
You’ll have a production-grade edge gateway that handles all modern HTTP protocols, automatically manages TLS certificates, and provides real-time observability. When you run it, you’ll see protocol translation happening in real-time as clients connect with different protocols and your gateway translates to legacy backends.
Example Output:
$ ./hyperproxy --config hyperproxy.yaml
╔═══════════════════════════════════════════════════════════════╗
║ HyperProxy Edge Gateway ║
╠═══════════════════════════════════════════════════════════════╣
║ Frontends: ║
║ ├── HTTP/1.1 → :80 (redirect to HTTPS) ║
║ ├── HTTPS/H2 → :443 (TLS 1.3, ALPN: h2, http/1.1) ║
║ └── HTTP/3 → :443/udp (QUIC, Alt-Svc advertised) ║
║ ║
║ Backends: ║
║ ├── api.internal:8080 (HTTP/1.1, 3 instances) ║
║ ├── legacy.internal:80 (HTTP/1.0, keepalive disabled) ║
║ └── grpc.internal:9090 (HTTP/2, gRPC passthrough) ║
║ ║
║ TLS Certificates (ACME/Let's Encrypt): ║
║ ├── api.example.com (valid, 45 days remaining) ║
║ ├── app.example.com (valid, 72 days remaining) ║
║ └── *.example.com (wildcard, 30 days remaining) ║
╚═══════════════════════════════════════════════════════════════╝
[INFO] Dashboard available at http://localhost:9090/dashboard
[INFO] Metrics endpoint at http://localhost:9090/metrics (Prometheus)
───────────────────────────────────────────────────────────────────
[14:32:01] REQUEST │ H3 → H1 │ GET /api/users │ 203.0.113.45
[14:32:01] ├── Frontend: HTTP/3 (QUIC, 0-RTT resumption)
[14:32:01] ├── Protocol Translation: H3 → HTTP/1.1
[14:32:01] ├── Backend: api.internal:8080 (pool: 3/10 connections)
[14:32:01] ├── Compression: brotli (1.2KB → 340B, 72% reduction)
[14:32:01] └── Response: 200 OK (23ms total, 18ms backend)
[14:32:02] REQUEST │ H2 → H1 │ POST /legacy/submit │ 198.51.100.22
[14:32:02] ├── Frontend: HTTP/2 (stream #7, multiplexed)
[14:32:02] ├── Protocol Translation: H2 → HTTP/1.1
[14:32:02] ├── Backend: legacy.internal:80 (new connection)
[14:32:02] ├── Rate Limit: 48/100 requests (token bucket)
[14:32:02] └── Response: 201 Created (156ms total)
[14:32:03] RATE_LIMIT │ 192.0.2.100 │ Exceeded 100 req/min
[14:32:03] └── Action: 429 Too Many Requests (60s cooldown)
[14:32:05] CACHE │ HIT │ GET /static/bundle.js │ 203.0.113.45
[14:32:05] ├── Cache Key: /static/bundle.js:gzip
[14:32:05] └── Served: 245KB in 1.2ms (Age: 3600s, TTL: 82800s)
[14:32:10] CERT_RENEWAL │ api.example.com
[14:32:10] ├── ACME Challenge: HTTP-01 (/.well-known/acme-challenge/...)
[14:32:10] ├── Authorization: Validated with Let's Encrypt
[14:32:10] └── Certificate: Renewed (valid for 90 days)
───────────────────────────────────────────────────────────────────
╔═══════════════════════════════════════════════════════════════╗
║ LIVE METRICS (5-second window) ║
╠═══════════════════════════════════════════════════════════════╣
║ Protocol Distribution: ║
║ HTTP/3 (QUIC): ████████████████████░░░░░ 42% (4,200 req) ║
║ HTTP/2: ██████████████░░░░░░░░░░░ 35% (3,500 req) ║
║ HTTP/1.1: █████████░░░░░░░░░░░░░░░░ 23% (2,300 req) ║
║ ║
║ Cache Performance: ║
║ Hit Rate: ████████████████████░░░░░ 82% ║
║ Miss Rate: ████░░░░░░░░░░░░░░░░░░░░░ 18% ║
║ ║
║ Backend Health: ║
║ api.internal [●●●] 3/3 healthy (avg: 15ms) ║
║ legacy.internal [●○○] 1/1 healthy (avg: 120ms) ║
║ grpc.internal [●●] 2/2 healthy (avg: 8ms) ║
║ ║
║ Active Connections: 1,247 │ Requests/sec: 2,000 ║
║ Bandwidth: 45 MB/s in, 180 MB/s out (compression savings) ║
║ Memory: 256 MB │ CPU: 12% (8 cores) ║
╚═══════════════════════════════════════════════════════════════╝
HyperProxy Architecture Overview:
┌─────────────────────────────────────────────────────────────┐
│ HyperProxy Edge Gateway │
│ │
┌─────────────┐ │ ┌─────────────────────────────────────────────────────┐ │
│ Client │ │ │ Frontend Listeners │ │
│ (Browser) │ │ │ │ │
│ │ │ │ :80 ──────► HTTP/1.1 ──────► 301 Redirect │ │
│ HTTP/3 ◄───┼───┼───┼── :443/UDP ─► QUIC/H3 ─────┐ │ │
│ HTTP/2 ◄───┼───┼───┼── :443 ─────► TLS 1.3 ─────┼──► Protocol Router │ │
│ HTTP/1.1 ◄─┼───┼───┼─────────────► HTTP/1.1 ────┘ │ │ │
└─────────────┘ │ │ ▼ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Request Pipeline │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────────┐ │ │
│ │ │ Rate │ ─► │ Cache │ ─► │ Backend │ │ │
│ │ │ Limiter │ │ Lookup │ │ Selector │ │ │
│ │ └─────────┘ └──────────┘ └────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ [429 if [HIT: serve [Translate to │ │
│ │ exceeded] from cache] backend protocol] │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Backend Connection Pools │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ api:8080 │ │ legacy:80 │ │ grpc:9090 │ │ │
│ │ │ HTTP/1.1 │ │ HTTP/1.0 │ │ HTTP/2 │ │ │
│ │ │ Pool: 10 │ │ Pool: 1 │ │ Pool: 5 │ │ │
│ │ │ Keepalive │ │ No-keepalive│ │ Streams │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Observability & Management │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Metrics │ │ ACME │ │ Dashboard │ │ │
│ │ │ (Prometheus)│ │ (Let's Enc) │ │ (Web UI) │ │ │
│ │ │ :9090 │ │ Auto-renew │ │ :9090 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
The Core Question You’re Answering
“How do services like Cloudflare, Fastly, and AWS CloudFront handle millions of concurrent connections across multiple protocols while providing automatic TLS, intelligent caching, and real-time threat mitigation - all with sub-millisecond overhead?”
Before you write any code, sit with this question. The answer reveals the engineering marvel of modern edge infrastructure. These systems must:
- Speak every protocol - Clients might connect via HTTP/1.1, HTTP/2, or HTTP/3, but backends often only speak HTTP/1.1
- Never drop a certificate - Automatic renewal must happen seamlessly without human intervention
- Protect without blocking - Rate limiting must stop attacks while letting legitimate traffic through
- Scale horizontally - The same architecture must work for 100 or 1,000,000 concurrent connections
Your edge gateway is the single point where all internet traffic enters your infrastructure. Get it wrong, and everything is slow, insecure, or unavailable. Get it right, and users experience the modern web as it was meant to be.
Concepts You Must Understand First
Stop and research these before coding. This is the capstone - you need mastery of every previous concept plus these advanced topics:
- Protocol Translation (H1 <-> H2 <-> H3)
- How do you convert HTTP/2’s binary frames into HTTP/1.1 text format?
- What metadata is lost or gained during translation (e.g., HTTP/2 pseudo-headers like
:authority)? - How do you handle HTTP/2’s stream multiplexing when the backend only supports one request per connection?
- How does QUIC’s connection migration affect the translation layer?
- Book Reference: “High Performance Browser Networking” Ch. 12-13 - Ilya Grigorik
- ACME Protocol and Let’s Encrypt Automation
- What are the three ACME challenge types (HTTP-01, DNS-01, TLS-ALPN-01)?
- How do you handle certificate renewal without downtime (hot reload)?
- What’s the certificate chain and how do you serve intermediates correctly?
- How do you implement rate limit backoff for ACME failures?
- Book Reference: RFC 8555 (ACME Protocol), “Bulletproof TLS and PKI” Ch. 5
- Connection Pooling and Keep-Alive Management
- How many connections should you keep open to each backend?
- How do you handle connection affinity for stateful backends?
- What’s the optimal idle timeout before closing pooled connections?
- How do you implement health checks and circuit breakers?
- Book Reference: “Unix Network Programming” Ch. 15 - Stevens, “Release It!” Ch. 5 - Nygard
- Rate Limiting Algorithms
- Token Bucket: How do you allow bursts while maintaining average rate?
- Leaky Bucket: How does it differ and when is each appropriate?
- Sliding Window Log: How do you implement precise per-second limits?
- Distributed Rate Limiting: How do you share state across multiple proxies?
- Book Reference: “Designing Data-Intensive Applications” Ch. 8 - Kleppmann
- Real-Time Metrics Collection and Visualization
- What metrics matter for an edge proxy (latency percentiles, cache hit rate, error rate)?
- How do you implement efficient histograms for latency tracking?
- What’s the overhead of metrics collection and how do you minimize it?
- How do you expose metrics for Prometheus/Grafana consumption?
- Book Reference: “Site Reliability Engineering” Ch. 6 - Google
- Compression Negotiation (Gzip, Brotli, Zstd)
- How do you read the
Accept-Encodingheader and select the best algorithm? - When should you compress (Content-Type, size threshold)?
- How do you handle pre-compressed assets (cache both versions)?
- What’s the CPU vs bandwidth tradeoff for different compression levels?
- Book Reference: “HTTP: The Definitive Guide” Ch. 15 - Gourley
- How do you read the
Questions to Guide Your Design
Before implementing, think through these architectural decisions:
Multi-Protocol Frontend Architecture
- How will you detect the incoming protocol (H1 vs H2 vs H3)?
- Will you use one listener per protocol or multiplex on a single port?
- How do you advertise HTTP/3 support via the
Alt-Svcheader? - How do you handle TLS ALPN negotiation for H2 vs H1.1?
- What’s your strategy for 0-RTT data in QUIC (security implications)?
Backend Connection Management
- How will you implement connection pooling per backend?
- How do you handle backends that close connections unexpectedly?
- What’s your health check interval and failure threshold?
- How do you implement weighted load balancing based on backend capacity?
- What happens when all backends are unhealthy (fail open vs closed)?
Certificate Management
- How will you store certificates and private keys securely?
- How do you handle the initial certificate acquisition (chicken-and-egg)?
- What’s your renewal strategy (days before expiry, retry logic)?
- How do you handle wildcard certificates vs per-domain certificates?
- What’s your fallback if Let’s Encrypt is unavailable (self-signed)?
Rate Limiting Design
- How do you identify clients (IP, API key, user ID)?
- How do you handle IPv6 and CGNAT (many users behind one IP)?
- What’s your rate limit response (429 with
Retry-Afterheader)? - How do you implement tiered limits (free tier vs premium)?
- How do you share rate limit state across multiple proxy instances?
Metrics and Observability
- What granularity of metrics do you need (per-route, per-backend, per-client)?
- How do you handle high cardinality (millions of unique URLs)?
- What’s your logging strategy (structured logs, sampling for high volume)?
- How do you implement request tracing across the proxy?
- What alerts should trigger automatically (5xx spike, latency degradation)?
Thinking Exercise
Design the Request Flow
Before writing code, trace through these scenarios on paper:
Scenario 1: HTTP/3 Client to HTTP/1.1 Backend
Client sends via QUIC/HTTP/3:
:method = GET
:path = /api/users
:authority = api.example.com
accept-encoding: br, gzip
x-request-id: abc123
Your proxy must:
1. Receive QUIC packet, decrypt
2. Parse QPACK-compressed headers
3. Check rate limit for client IP
4. Look up cache (miss)
5. Select backend from pool
6. Translate to HTTP/1.1:
GET /api/users HTTP/1.1
Host: api.example.com
Accept-Encoding: br, gzip
X-Request-Id: abc123
X-Forwarded-For: <client-ip>
X-Forwarded-Proto: https
7. Receive HTTP/1.1 response
8. Compress response body (brotli)
9. Translate back to HTTP/3
10. Update cache if cacheable
11. Update metrics (latency, status code)
Scenario 2: Certificate Renewal Lifecycle
Draw the state machine for certificate management:
┌──────────────┐
│ UNKNOWN │ (startup, no cert)
└──────┬───────┘
│ Check disk/reload
▼
┌──────────────┐
│ MISSING │──────────────────────────────────┐
└──────┬───────┘ │
│ Start ACME │
▼ │
┌──────────────┐ │
│ ORDERING │──► (ACME order creation) │
└──────┬───────┘ │
│ Order ready │
▼ │
┌──────────────┐ │
│ CHALLENGING │──► (HTTP-01/DNS-01 challenge) │
└──────┬───────┘ │
│ Challenge valid │
▼ │
┌──────────────┐ │
│ DOWNLOADING │──► (Fetch signed cert) │
└──────┬───────┘ │
│ Success │
▼ │
┌──────────────┐ renewal window │
│ VALID │──────────────────────────────────┤
└──────┬───────┘ │
│ < 30 days remaining │
▼ │
┌──────────────┐ │
│ RENEWING │──► (Back to ORDERING) │
└──────────────┘ │
│
Failure at any step ──────────────────────────►│
▼
┌──────────────┐
│ FALLBACK │
│ (self-signed)│
└──────────────┘
Scenario 3: Rate Limiting Data Structures
Design the data structures for a token bucket rate limiter:
Per-client state:
client_id: string (IP or API key)
tokens: float (current available tokens)
last_update: timestamp
bucket_capacity: int (max burst size)
refill_rate: float (tokens per second)
Algorithm:
function try_consume(client_id, tokens_needed):
state = get_or_create(client_id)
now = current_time()
elapsed = now - state.last_update
state.tokens = min(
state.bucket_capacity,
state.tokens + elapsed * state.refill_rate
)
state.last_update = now
if state.tokens >= tokens_needed:
state.tokens -= tokens_needed
return ALLOW
else:
return DENY with retry_after = (tokens_needed - state.tokens) / refill_rate
The Interview Questions They’ll Ask
Prepare to answer these - they’re asked at companies building edge infrastructure (Cloudflare, Fastly, AWS, Akamai):
- “Design an edge proxy that can handle 1 million concurrent connections. What are the key bottlenecks?”
- Expected topics: Event loops (epoll/kqueue), connection pooling, memory per connection, kernel tuning
- “How would you implement graceful shutdown for a proxy handling active requests?”
- Expected topics: Drain period, health check failing, connection refusal, timeout handling
- “Explain how you would translate HTTP/2 server push to an HTTP/1.1 backend.”
- Expected topics: Preload hints, Link headers, the limitations of translation
- “What happens when a QUIC client switches from WiFi to cellular mid-request?”
- Expected topics: Connection ID migration, NAT rebinding, zero-RTT resumption
- “How do you prevent cache poisoning attacks in a CDN?”
- Expected topics: Host header validation, cache key normalization,
Varyheader handling
- Expected topics: Host header validation, cache key normalization,
- “Design a rate limiting system that works across 100 edge servers globally.”
- Expected topics: Distributed counters, eventual consistency, local vs global limits
- “How would you implement automatic TLS certificate rotation without dropping connections?”
- Expected topics: Hot reload, file watching, certificate chain ordering, OCSP stapling
- “What’s your strategy for handling slow clients (slow loris attack)?”
- Expected topics: Request timeouts, header timeout, body read timeout, connection limits per IP
- “How do you decide what to cache and for how long?”
- Expected topics: Cache-Control parsing, heuristic freshness, stale-while-revalidate, purging
- “Explain the end-to-end latency breakdown for a request through your proxy.”
- Expected topics: DNS, TCP handshake, TLS handshake, TTFB, backend latency, compression time
Hints in Layers
Hint 1: Start with HTTP/1.1 Proxy Begin with the simplest case: HTTP/1.1 frontend to HTTP/1.1 backend. Get request parsing, response forwarding, and keep-alive working. This is your foundation.
// Pseudo-code for basic proxy
func handleConnection(client net.Conn) {
for {
request := parseHTTP11Request(client)
backend := selectBackend(request.Host)
backendConn := connectionPool.get(backend)
forwardRequest(backendConn, request)
response := readResponse(backendConn)
sendResponse(client, response)
connectionPool.release(backend, backendConn)
}
}
Hint 2: Add TLS Termination
Wrap your listener with TLS. Start with static certificates, then add the ACME client. Use the autocert package in Go or certbot concepts to understand the flow.
// TLS with certificate hot-reload
certManager := &CertManager{
cache: make(map[string]*tls.Certificate),
getFunc: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return getCertificate(hello.ServerName)
},
}
tlsConfig := &tls.Config{GetCertificate: certManager.getCertificate}
listener := tls.NewListener(tcpListener, tlsConfig)
Hint 3: Add HTTP/2 Support Use your language’s HTTP/2 library for the frontend. The key insight: HTTP/2 is still HTTP semantics, just different framing. Your backend logic stays the same.
// HTTP/2 server with h2c (cleartext) option
http2Server := &http2.Server{}
for {
conn, _ := listener.Accept()
go http2Server.ServeConn(conn, &http2.ServeConnOpts{
Handler: proxyHandler,
})
}
Hint 4: Add Caching Layer
Implement RFC 7234 compliant caching. Start simple: cache GET requests with explicit Cache-Control. Handle Vary for content negotiation.
type CacheEntry struct {
Response *http.Response
Body []byte
StoredAt time.Time
TTL time.Duration
VaryHeaders map[string]string
}
func getCacheKey(req *http.Request) string {
// Include Vary headers in key
return req.URL.String() + "|" + req.Header.Get("Accept-Encoding")
}
Hint 5: Add Metrics Dashboard
Use Prometheus client libraries. Expose a /metrics endpoint. Create a simple HTML dashboard that polls these metrics.
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "requests_total"},
[]string{"protocol", "status", "backend"},
)
latencyHistogram = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "request_latency_seconds",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
},
[]string{"protocol", "backend"},
)
)
Books That Will Help
| Topic | Book | Chapter | Why It’s Essential |
|---|---|---|---|
| Protocol Fundamentals | “High Performance Browser Networking” by Ilya Grigorik | Ch. 9-13 | Covers H1, H2, H3 in depth |
| TLS and Certificates | “Bulletproof TLS and PKI” by Ivan Ristic | Ch. 1-5 | ACME, cert chains, security |
| HTTP Caching | “HTTP: The Definitive Guide” by Gourley & Totty | Ch. 7, 15 | RFC 7234 in practical terms |
| Rate Limiting | “Designing Data-Intensive Applications” by Kleppmann | Ch. 8 | Distributed systems patterns |
| Proxy Architecture | “Building Microservices” by Sam Newman | Ch. 4-5 | Service mesh, API gateways |
| Systems Programming | “The Linux Programming Interface” by Kerrisk | Ch. 56-63 | Sockets, I/O multiplexing |
| Production Operations | “Site Reliability Engineering” by Google | Ch. 6, 17 | Monitoring, load balancing |
| Performance | “Systems Performance” by Brendan Gregg | Ch. 10 | Network performance analysis |
| Security | “Web Application Security” by Andrew Hoffman | Ch. 8-12 | DDoS, rate limiting, headers |
| Go Networking | “Network Programming with Go” by Jan Newmarch | Ch. 3-8 | Go-specific implementation |
Learning Milestones
This is the capstone project. Each milestone represents a significant achievement:
Milestone 1: The Basic Proxy (Week 1-2)
Goal: HTTP/1.1 proxy with connection pooling
- Accept HTTP/1.1 connections on port 8080
- Parse requests and forward to a single backend
- Implement connection pooling (10 connections per backend)
- Handle keep-alive correctly on both sides
- Add basic access logging (client IP, method, path, status, latency)
You know you’ve succeeded when: You can proxy requests to a real backend (like httpbin.org) and handle 100 concurrent connections without errors.
Milestone 2: TLS and Certificates (Week 3-4)
Goal: HTTPS frontend with automatic certificates
- Add TLS listener on port 443
- Implement ACME HTTP-01 challenge handling
- Successfully obtain a Let’s Encrypt certificate (use staging first!)
- Implement certificate hot-reload without restarting
- Add HTTP->HTTPS redirect on port 80
You know you’ve succeeded when: You can visit https://your-domain.com and see a valid certificate in your browser, obtained automatically.
Milestone 3: Multi-Protocol Frontend (Week 5-6)
Goal: Support H1, H2, and H3 clients
- Add HTTP/2 support via TLS ALPN negotiation
- Implement basic HTTP/3 listener using quiche or quinn
- Translate all protocols to HTTP/1.1 for backends
- Advertise HTTP/3 via Alt-Svc header
- Verify with curl:
curl --http3 https://your-domain.com
You know you’ve succeeded when: Chrome DevTools shows “h3” in the Protocol column when visiting your site.
Milestone 4: Caching and Rate Limiting (Week 7-8)
Goal: Production-ready traffic management
- Implement RFC 7234 compliant caching
- Add token bucket rate limiting (100 req/min per IP)
- Handle
Varyheader for cache variants (Accept-Encoding) - Implement cache purge API (
POST /admin/purge?url=...) - Add stale-while-revalidate support
You know you’ve succeeded when: Cache hit rate is > 80% for static assets, and you can trigger rate limiting with ab -n 200 -c 10 https://your-domain.com/.
Milestone 5: Observability Dashboard (Week 9-10)
Goal: Real-time visibility into your gateway
- Expose Prometheus metrics at
/metrics - Implement latency histograms (p50, p95, p99)
- Track protocol distribution, cache hit rate, error rate
- Create a simple web dashboard at
/dashboard - Add distributed tracing headers (X-Request-ID passthrough)
You know you’ve succeeded when: You can open your dashboard and watch real-time graphs of traffic flowing through your gateway.
Summary
This learning path covers Application Layer Networking through 7 hands-on projects.
| # | Project Name | Main Language | Difficulty | Time Estimate |
|---|---|---|---|---|
| 1 | Recursive DNS Resolver | Python | Intermediate | 1 week |
| 2 | HTTP/1.1 Server | C | Advanced | 2 weeks |
| 3 | TLS 1.3 Handshake | Go | Expert | 3 weeks |
| 4 | WebSocket Chat | Node.js | Intermediate | 1 week |
| 5 | HTTP/2 Frame Parser | Rust | Expert | 3 weeks |
| 6 | Mini-CDN Proxy | Go | Advanced | 2 weeks |
| 7 | QUIC/H3 Client | Rust/Go | Master | 4 weeks |
Expected Outcomes
After completing these projects, you will:
- Parse and construct binary protocol packets with ease.
- Understand the trade-offs between text-based and binary-based protocols.
- Master the security principles that keep the web safe.
- Be able to debug complex performance issues across the network stack.
- Understand the future of the web (QUIC and HTTP/3) before most of the industry.