gw-02 — Analysis

The design questions behind terminating L7, and what to be ready to defend in a review.

Required behaviors of an L7 terminator

  1. Unambiguous framing. Exactly one body-delimitation rule per message. Reject messages that specify both Content-Length and Transfer-Encoding: chunked (smuggling surface), or normalize to one before forwarding.
  2. Correct h2 demux. Frames from interleaved streams must be routed to the right per-stream state machine by Stream Identifier; stream IDs are monotonic and parity-typed (odd=client, even=server).
  3. Flow-control accounting. Track per-stream and per-connection windows; never send DATA beyond the peer's advertised window; emit WINDOW_UPDATE as you consume.
  4. HPACK consistency. Encoder and decoder dynamic tables must stay in lockstep across header rewrites; bound the table size.
  5. Graceful close. Use GOAWAY (h2) to drain rather than abruptly closing the TCP connection mid-stream.

Design decisions in the lab parser

  • Parse, don't fully implement. The steps/ build a read-only h2 frame parser: it decodes the 9-byte header, classifies frame types, and tracks per-stream state — enough to reason about multiplexing, HPACK, and flow control, without re-implementing a production codec. The real thing is golang.org/x/net/http2; the lab's job is to make its mechanics legible.

  • Static-table HPACK first, dynamic table second. We decode using the 61-entry static table and literal headers before adding the dynamic table, because the dynamic table is where the subtle state-sync bugs live and you want to see the table grow.

  • Surface the multiplexing trap experimentally. A step runs many concurrent gRPC calls over one h2 connection through an L4 vs an L7 balancer and shows the load distribution differ — turning the interview talking point into a reproduced result.

Tradeoffs worth flagging

  • L7 termination forfeits zero-copy. The moment you parse, you copy through userspace and hold per-message state. That's the price of routing/retries/auth. For pure pass-through (e.g., opaque TLS passthrough by SNI) you stay L4 and keep splice (gw-07).

  • h2 to the origin is a double-edged sword. One multiplexed connection per origin curbs connection churn (gw-04) and removes h1 HOL — but concentrates load and re-introduces TCP HOL; a single packet loss stalls all streams to that origin. You trade connection efficiency for blast-radius-per-connection.

  • HTTP/3's UDP changes your whole operational model. New LB hashing (connection ID, not 4-tuple), new DDoS posture, new observability (no TCP counters), userspace CPU cost. Often worth it at the client edge (mobile), rarely worth it origin-side first.

  • Trailers complicate everything. gRPC status in trailers means your access logs, metrics, and retry logic must read past the body. A gateway that classifies success by HTTP status alone will misreport gRPC health (gw-06, gw-11).

What production adds beyond this lab

  • A full, fuzzed codec (h2 frame parsers are a rich attack surface: HPACK bombs, CONTINUATION floods, RST_STREAM floods → the "Rapid Reset" CVE-2023-44487 class of DoS).
  • Protocol downgrade/upgrade bridging (h2 client ↔ h1 origin, h3 client ↔ h2 origin) with correct semantics for trailers, 1xx, and Expect/Continue.
  • 0-RTT replay protection for h3 (non-idempotent requests must not be accepted in early data).
  • Per-route protocol policy (force h2 to this cluster, h1 to that one) driven by the control plane (gw-08).