Build a Service Mesh (Envoy / Istio style) (13 scenes)
Scene 03 · L4 vs L7 — bytes or requests
An L4 proxy forwards opaque TCP bytes; an L7 proxy parses HTTP and can act on path, method, and headers. The mesh is L7 for everything that follows.
Previously

A sidecar that owns 'policy' has to know what a request IS before it can apply any policy to it — that is the L4/L7 split.

Scene 03
L4 vs L7 — bytes or requests
Diagram
A single sidecar proxy in the middle with a 2-position slider. On the left ('L4'), a proxy that just forwards bytes without opening them is operating at the transport layer — that's an **L4 proxy**: it sees source/destination IP and port but cannot read what's inside the bytes. On the right ('L7'), a proxy that opens the envelope and reads method/path/headers is operating at the application layer — that's an **L7 proxy**: it terminates the HTTP stream and parses method, path, and headers, so it can apply rules to those fields. Below the proxy, a capability table shows which routing tasks each mode supports.
L4: the proxy moves bytes; only IP/port are legible.L4 PROXY (BYTES)SRC10.0.2.14:51022DST10.0.7.31:8080Sidecar (proxy)sees: TCP bytes + IPs0x4A 7F E2 …opaque bytes — proxy cannot parseROUTING TASKL4L7forward bytesroute on /users/* path prefixroute on x-canary: true header
L4 proxy: sees IPs, not bytes' meaning →
L7 sees path + headers · L4 sees only IP/port
The same request enters the same sidecar twice. First as L4: only the IP/port labels light up; the payload is a featureless byte bar. Flip the slider and the proxy's interior changes — the parsed HTTP envelope (method, path, headers) becomes visible.
Implementation
L4Proxy.serve()
transport-layer pipe — bytes pass through opaquely
1def serve():
2 conn = accept(':8080')
3 upstream = dial('backend:8080')
4 # splice both directions; never look inside
5 pipe(conn, upstream)
6 pipe(upstream, conn)
7 # we don't even know if it's HTTP
L7Proxy.serve()
application-layer parse — bytes become an HTTP request
1def serve():
2 conn = accept(':8080')
3 req = http.parse(conn) # method, path, headers
4 route = match(routes, req)
5 upstream = dial(route.cluster)
6 resp = upstream.send(req)
7 http.write(conn, resp)
L7Proxy.match(routes, req)
the rules that only exist once HTTP is parsed
1def match(routes, req):
2 for r in routes:
3 if r.path and not req.path.startswith(r.path):
4 continue
5 if r.header:
6 name, want = r.header
7 if req.headers.get(name) != want:
8 continue
9 return r # first match wins
10 return default_route