gw-02 step 01 — Parse the HTTP/2 frame layer
Goal
Decode an HTTP/2 connection at the frame level: the connection preface, the 9-byte frame header, and the common frame types. This makes multiplexing visible — you'll watch frames from multiple streams interleave on one connection.
Background — the preface and frame header
Every h2 connection starts with a fixed client preface then a
SETTINGS frame both ways:
client preface (24 bytes): "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
then: SETTINGS frame (client) and SETTINGS frame (server), each ACKed
Frame header (9 bytes), big-endian:
length: uint24 (payload length, not incl. the 9-byte header)
type: uint8 (0=DATA 1=HEADERS 3=RST_STREAM 4=SETTINGS
6=PING 7=GOAWAY 8=WINDOW_UPDATE ...)
flags: uint8
stream: uint31 (high bit reserved; 0 = connection-level)
Code — src/go/frame.go
package h2
import (
"encoding/binary"
"fmt"
"io"
)
type FrameType uint8
const (
FrameData FrameType = 0x0
FrameHeaders FrameType = 0x1
FramePriority FrameType = 0x2
FrameRSTStream FrameType = 0x3
FrameSettings FrameType = 0x4
FramePushPromise FrameType = 0x5
FramePing FrameType = 0x6
FrameGoAway FrameType = 0x7
FrameWindowUpdate FrameType = 0x8
FrameContinuation FrameType = 0x9
)
func (t FrameType) String() string {
return [...]string{"DATA", "HEADERS", "PRIORITY", "RST_STREAM",
"SETTINGS", "PUSH_PROMISE", "PING", "GOAWAY", "WINDOW_UPDATE",
"CONTINUATION"}[t]
}
// Flag bits (meaning depends on frame type).
const (
FlagEndStream uint8 = 0x1 // DATA, HEADERS
FlagAck uint8 = 0x1 // SETTINGS, PING
FlagEndHeaders uint8 = 0x4 // HEADERS, CONTINUATION
FlagPadded uint8 = 0x8
)
type FrameHeader struct {
Length uint32 // 24-bit
Type FrameType
Flags uint8
StreamID uint32 // 31-bit
}
type Frame struct {
Header FrameHeader
Payload []byte
}
const ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
// ReadFrame reads one frame (9-byte header + payload) from r.
func ReadFrame(r io.Reader) (*Frame, error) {
var hdr [9]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return nil, err
}
length := uint32(hdr[0])<<16 | uint32(hdr[1])<<8 | uint32(hdr[2])
fh := FrameHeader{
Length: length,
Type: FrameType(hdr[3]),
Flags: hdr[4],
StreamID: binary.BigEndian.Uint32(hdr[5:9]) & 0x7fffffff, // mask reserved bit
}
payload := make([]byte, length)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, err
}
return &Frame{Header: fh, Payload: payload}, nil
}
func (f *Frame) String() string {
end := ""
if f.Header.Type == FrameData || f.Header.Type == FrameHeaders {
if f.Header.Flags&FlagEndStream != 0 {
end = " END_STREAM"
}
}
return fmt.Sprintf("stream=%-3d %-13s len=%-5d flags=0x%02x%s",
f.Header.StreamID, f.Header.Type, f.Header.Length, f.Header.Flags, end)
}
A "frame sniffer" main — watch multiplexing happen
src/go/cmd/h2sniff/main.go reads a captured h2 byte stream (e.g. from
a file you dump, or proxy a real curl --http2-prior-knowledge through
an io.TeeReader) and prints frames:
func main() {
r := bufio.NewReader(os.Stdin)
pre := make([]byte, len(h2.ClientPreface))
io.ReadFull(r, pre) // consume the connection preface
for {
fr, err := h2.ReadFrame(r)
if err != nil {
return
}
fmt.Println(fr)
}
}
Generate a capture and feed it in:
# Cleartext h2 (prior knowledge) so you can read frames without TLS:
nghttpd --no-tls 9000 & # or any h2c origin
nghttp -v http://127.0.0.1:9000/ -m 5 # -m 5: 5 concurrent reqs
# nghttp -v already prints frames; the lab's parser reproduces that view
# from raw bytes so you understand the wire, not just the tool's output.
Tasks
- Implement
ReadFrameand the type/flag decoding. - Feed it an h2 capture with multiple concurrent requests (
nghttp -m 8). In the output, find frames with differentstream=values interleaved — that's multiplexing you can point at. - Identify the
SETTINGSexchange at the start and theEND_STREAMflag that closes each request's stream.
Acceptance
- Your sniffer prints a frame-by-frame trace matching
nghttp -vfor the same connection (same stream IDs, types, END_STREAM flags). - You can point to interleaved stream IDs and explain that one TCP connection is carrying N concurrent requests.
Discussion prompts
- Why are client-initiated stream IDs always odd? What are even IDs for?
- A
HEADERSframe withoutEND_HEADERSmust be followed byCONTINUATIONframes — why does that exist, and why is a flood of empty CONTINUATION frames a DoS (the "Rapid Reset" cousin)? - Where, exactly, would an L4 LB have to stop to make per-request routing possible? (It can't — it only sees TCP segments, not frames.)