quic: packet encoding/decoding

Frame encoding is handled by the packetWriter type.
The packetWriter also takes responsibility for recording the contents
of constructed packets in a sentPacket structure.

Frame decoding is handled by consume*Frame functions, which generally
return the frame contents. ACK frames, which have complex contents,
are provided to the caller via callback function.

In addition to the above functions, used in the serving path, this
CL includes per-frame types that implement a common debugFrame
interface.  These types are used for tests and debug logging, but
not in the serving path where we want to avoid allocations from
storing values in an interface.

For golang/go#58547

Change-Id: I03ce11210aa9aa6ac749a5273b2ba9dd9c6989cf
Reviewed-on: https://go-review.googlesource.com/c/net/+/495355
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Damien Neil <dneil@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/quic/frame_debug.go b/internal/quic/frame_debug.go
new file mode 100644
index 0000000..fa9bdca
--- /dev/null
+++ b/internal/quic/frame_debug.go
@@ -0,0 +1,506 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+import (
+	"fmt"
+	"time"
+)
+
+// A debugFrame is a representation of the contents of a QUIC frame,
+// used for debug logs and testing but not the primary serving path.
+type debugFrame interface {
+	String() string
+	write(w *packetWriter) bool
+}
+
+func parseDebugFrame(b []byte) (f debugFrame, n int) {
+	if len(b) == 0 {
+		return nil, -1
+	}
+	switch b[0] {
+	case frameTypePadding:
+		f, n = parseDebugFramePadding(b)
+	case frameTypePing:
+		f, n = parseDebugFramePing(b)
+	case frameTypeAck, frameTypeAckECN:
+		f, n = parseDebugFrameAck(b)
+	case frameTypeResetStream:
+		f, n = parseDebugFrameResetStream(b)
+	case frameTypeStopSending:
+		f, n = parseDebugFrameStopSending(b)
+	case frameTypeCrypto:
+		f, n = parseDebugFrameCrypto(b)
+	case frameTypeNewToken:
+		f, n = parseDebugFrameNewToken(b)
+	case frameTypeStreamBase, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f:
+		f, n = parseDebugFrameStream(b)
+	case frameTypeMaxData:
+		f, n = parseDebugFrameMaxData(b)
+	case frameTypeMaxStreamData:
+		f, n = parseDebugFrameMaxStreamData(b)
+	case frameTypeMaxStreamsBidi, frameTypeMaxStreamsUni:
+		f, n = parseDebugFrameMaxStreams(b)
+	case frameTypeDataBlocked:
+		f, n = parseDebugFrameDataBlocked(b)
+	case frameTypeStreamDataBlocked:
+		f, n = parseDebugFrameStreamDataBlocked(b)
+	case frameTypeStreamsBlockedBidi, frameTypeStreamsBlockedUni:
+		f, n = parseDebugFrameStreamsBlocked(b)
+	case frameTypeNewConnectionID:
+		f, n = parseDebugFrameNewConnectionID(b)
+	case frameTypeRetireConnectionID:
+		f, n = parseDebugFrameRetireConnectionID(b)
+	case frameTypePathChallenge:
+		f, n = parseDebugFramePathChallenge(b)
+	case frameTypePathResponse:
+		f, n = parseDebugFramePathResponse(b)
+	case frameTypeConnectionCloseTransport:
+		f, n = parseDebugFrameConnectionCloseTransport(b)
+	case frameTypeConnectionCloseApplication:
+		f, n = parseDebugFrameConnectionCloseApplication(b)
+	case frameTypeHandshakeDone:
+		f, n = parseDebugFrameHandshakeDone(b)
+	default:
+		return nil, -1
+	}
+	return f, n
+}
+
+// debugFramePadding is a sequence of PADDING frames.
+type debugFramePadding struct {
+	size int
+}
+
+func parseDebugFramePadding(b []byte) (f debugFramePadding, n int) {
+	for n < len(b) && b[n] == frameTypePadding {
+		n++
+	}
+	f.size = n
+	return f, n
+}
+
+func (f debugFramePadding) String() string {
+	return fmt.Sprintf("PADDING*%v", f.size)
+}
+
+func (f debugFramePadding) write(w *packetWriter) bool {
+	if w.avail() == 0 {
+		return false
+	}
+	for i := 0; i < f.size && w.avail() > 0; i++ {
+		w.b = append(w.b, frameTypePadding)
+	}
+	return true
+}
+
+// debugFramePing is a PING frame.
+type debugFramePing struct{}
+
+func parseDebugFramePing(b []byte) (f debugFramePing, n int) {
+	return f, 1
+}
+
+func (f debugFramePing) String() string {
+	return "PING"
+}
+
+func (f debugFramePing) write(w *packetWriter) bool {
+	return w.appendPingFrame()
+}
+
+// debugFrameAck is an ACK frame.
+type debugFrameAck struct {
+	ackDelay time.Duration
+	ranges   []i64range
+}
+
+func parseDebugFrameAck(b []byte) (f debugFrameAck, n int) {
+	f.ranges = nil
+	_, f.ackDelay, n = consumeAckFrame(b, ackDelayExponent, func(start, end packetNumber) {
+		f.ranges = append(f.ranges, i64range{
+			start: int64(start),
+			end:   int64(end),
+		})
+	})
+	// Ranges are parsed smallest to highest; reverse ranges slice to order them high to low.
+	for i := 0; i < len(f.ranges)/2; i++ {
+		j := len(f.ranges) - 1
+		f.ranges[i], f.ranges[j] = f.ranges[j], f.ranges[i]
+	}
+	return f, n
+}
+
+func (f debugFrameAck) String() string {
+	s := fmt.Sprintf("ACK Delay=%v", f.ackDelay)
+	for _, r := range f.ranges {
+		s += fmt.Sprintf(" [%v,%v)", r.start, r.end)
+	}
+	return s
+}
+
+func (f debugFrameAck) write(w *packetWriter) bool {
+	return w.appendAckFrame(rangeset(f.ranges), ackDelayExponent, f.ackDelay)
+}
+
+// debugFrameResetStream is a RESET_STREAM frame.
+type debugFrameResetStream struct {
+	id        streamID
+	code      uint64
+	finalSize int64
+}
+
+func parseDebugFrameResetStream(b []byte) (f debugFrameResetStream, n int) {
+	f.id, f.code, f.finalSize, n = consumeResetStreamFrame(b)
+	return f, n
+}
+
+func (f debugFrameResetStream) String() string {
+	return fmt.Sprintf("RESET_STREAM ID=%v Code=%v FinalSize=%v", f.id, f.code, f.finalSize)
+}
+
+func (f debugFrameResetStream) write(w *packetWriter) bool {
+	return w.appendResetStreamFrame(f.id, f.code, f.finalSize)
+}
+
+// debugFrameStopSending is a STOP_SENDING frame.
+type debugFrameStopSending struct {
+	id   streamID
+	code uint64
+}
+
+func parseDebugFrameStopSending(b []byte) (f debugFrameStopSending, n int) {
+	f.id, f.code, n = consumeStopSendingFrame(b)
+	return f, n
+}
+
+func (f debugFrameStopSending) String() string {
+	return fmt.Sprintf("STOP_SENDING ID=%v Code=%v", f.id, f.code)
+}
+
+func (f debugFrameStopSending) write(w *packetWriter) bool {
+	return w.appendStopSendingFrame(f.id, f.code)
+}
+
+// debugFrameCrypto is a CRYPTO frame.
+type debugFrameCrypto struct {
+	off  int64
+	data []byte
+}
+
+func parseDebugFrameCrypto(b []byte) (f debugFrameCrypto, n int) {
+	f.off, f.data, n = consumeCryptoFrame(b)
+	return f, n
+}
+
+func (f debugFrameCrypto) String() string {
+	return fmt.Sprintf("CRYPTO Offset=%v Length=%v", f.off, len(f.data))
+}
+
+func (f debugFrameCrypto) write(w *packetWriter) bool {
+	b, added := w.appendCryptoFrame(f.off, len(f.data))
+	copy(b, f.data)
+	return added
+}
+
+// debugFrameNewToken is a NEW_TOKEN frame.
+type debugFrameNewToken struct {
+	token []byte
+}
+
+func parseDebugFrameNewToken(b []byte) (f debugFrameNewToken, n int) {
+	f.token, n = consumeNewTokenFrame(b)
+	return f, n
+}
+
+func (f debugFrameNewToken) String() string {
+	return fmt.Sprintf("NEW_TOKEN Token=%x", f.token)
+}
+
+func (f debugFrameNewToken) write(w *packetWriter) bool {
+	return w.appendNewTokenFrame(f.token)
+}
+
+// debugFrameStream is a STREAM frame.
+type debugFrameStream struct {
+	id   streamID
+	fin  bool
+	off  int64
+	data []byte
+}
+
+func parseDebugFrameStream(b []byte) (f debugFrameStream, n int) {
+	f.id, f.off, f.fin, f.data, n = consumeStreamFrame(b)
+	return f, n
+}
+
+func (f debugFrameStream) String() string {
+	fin := ""
+	if f.fin {
+		fin = " FIN"
+	}
+	return fmt.Sprintf("STREAM ID=%v%v Offset=%v Length=%v", f.id, fin, f.off, len(f.data))
+}
+
+func (f debugFrameStream) write(w *packetWriter) bool {
+	b, added := w.appendStreamFrame(f.id, f.off, len(f.data), f.fin)
+	copy(b, f.data)
+	return added
+}
+
+// debugFrameMaxData is a MAX_DATA frame.
+type debugFrameMaxData struct {
+	max int64
+}
+
+func parseDebugFrameMaxData(b []byte) (f debugFrameMaxData, n int) {
+	f.max, n = consumeMaxDataFrame(b)
+	return f, n
+}
+
+func (f debugFrameMaxData) String() string {
+	return fmt.Sprintf("MAX_DATA Max=%v", f.max)
+}
+
+func (f debugFrameMaxData) write(w *packetWriter) bool {
+	return w.appendMaxDataFrame(f.max)
+}
+
+// debugFrameMaxStreamData is a MAX_STREAM_DATA frame.
+type debugFrameMaxStreamData struct {
+	id  streamID
+	max int64
+}
+
+func parseDebugFrameMaxStreamData(b []byte) (f debugFrameMaxStreamData, n int) {
+	f.id, f.max, n = consumeMaxStreamDataFrame(b)
+	return f, n
+}
+
+func (f debugFrameMaxStreamData) String() string {
+	return fmt.Sprintf("MAX_STREAM_DATA ID=%v Max=%v", f.id, f.max)
+}
+
+func (f debugFrameMaxStreamData) write(w *packetWriter) bool {
+	return w.appendMaxStreamDataFrame(f.id, f.max)
+}
+
+// debugFrameMaxStreams is a MAX_STREAMS frame.
+type debugFrameMaxStreams struct {
+	streamType streamType
+	max        int64
+}
+
+func parseDebugFrameMaxStreams(b []byte) (f debugFrameMaxStreams, n int) {
+	f.streamType, f.max, n = consumeMaxStreamsFrame(b)
+	return f, n
+}
+
+func (f debugFrameMaxStreams) String() string {
+	return fmt.Sprintf("MAX_STREAMS Type=%v Max=%v", f.streamType, f.max)
+}
+
+func (f debugFrameMaxStreams) write(w *packetWriter) bool {
+	return w.appendMaxStreamsFrame(f.streamType, f.max)
+}
+
+// debugFrameDataBlocked is a DATA_BLOCKED frame.
+type debugFrameDataBlocked struct {
+	max int64
+}
+
+func parseDebugFrameDataBlocked(b []byte) (f debugFrameDataBlocked, n int) {
+	f.max, n = consumeDataBlockedFrame(b)
+	return f, n
+}
+
+func (f debugFrameDataBlocked) String() string {
+	return fmt.Sprintf("DATA_BLOCKED Max=%v", f.max)
+}
+
+func (f debugFrameDataBlocked) write(w *packetWriter) bool {
+	return w.appendDataBlockedFrame(f.max)
+}
+
+// debugFrameStreamDataBlocked is a STREAM_DATA_BLOCKED frame.
+type debugFrameStreamDataBlocked struct {
+	id  streamID
+	max int64
+}
+
+func parseDebugFrameStreamDataBlocked(b []byte) (f debugFrameStreamDataBlocked, n int) {
+	f.id, f.max, n = consumeStreamDataBlockedFrame(b)
+	return f, n
+}
+
+func (f debugFrameStreamDataBlocked) String() string {
+	return fmt.Sprintf("STREAM_DATA_BLOCKED ID=%v Max=%v", f.id, f.max)
+}
+
+func (f debugFrameStreamDataBlocked) write(w *packetWriter) bool {
+	return w.appendStreamDataBlockedFrame(f.id, f.max)
+}
+
+// debugFrameStreamsBlocked is a STREAMS_BLOCKED frame.
+type debugFrameStreamsBlocked struct {
+	streamType streamType
+	max        int64
+}
+
+func parseDebugFrameStreamsBlocked(b []byte) (f debugFrameStreamsBlocked, n int) {
+	f.streamType, f.max, n = consumeStreamsBlockedFrame(b)
+	return f, n
+}
+
+func (f debugFrameStreamsBlocked) String() string {
+	return fmt.Sprintf("STREAMS_BLOCKED Type=%v Max=%v", f.streamType, f.max)
+}
+
+func (f debugFrameStreamsBlocked) write(w *packetWriter) bool {
+	return w.appendStreamsBlockedFrame(f.streamType, f.max)
+}
+
+// debugFrameNewConnectionID is a NEW_CONNECTION_ID frame.
+type debugFrameNewConnectionID struct {
+	seq           int64
+	retirePriorTo int64
+	connID        []byte
+	token         [16]byte
+}
+
+func parseDebugFrameNewConnectionID(b []byte) (f debugFrameNewConnectionID, n int) {
+	f.seq, f.retirePriorTo, f.connID, f.token, n = consumeNewConnectionIDFrame(b)
+	return f, n
+}
+
+func (f debugFrameNewConnectionID) String() string {
+	return fmt.Sprintf("NEW_CONNECTION_ID Seq=%v Retire=%v ID=%x Token=%x", f.seq, f.retirePriorTo, f.connID, f.token[:])
+}
+
+func (f debugFrameNewConnectionID) write(w *packetWriter) bool {
+	return w.appendNewConnectionIDFrame(f.seq, f.retirePriorTo, f.connID, f.token)
+}
+
+// debugFrameRetireConnectionID is a NEW_CONNECTION_ID frame.
+type debugFrameRetireConnectionID struct {
+	seq           uint64
+	retirePriorTo uint64
+	connID        []byte
+	token         [16]byte
+}
+
+func parseDebugFrameRetireConnectionID(b []byte) (f debugFrameRetireConnectionID, n int) {
+	f.seq, n = consumeRetireConnectionIDFrame(b)
+	return f, n
+}
+
+func (f debugFrameRetireConnectionID) String() string {
+	return fmt.Sprintf("RETIRE_CONNECTION_ID Seq=%v", f.seq)
+}
+
+func (f debugFrameRetireConnectionID) write(w *packetWriter) bool {
+	return w.appendRetireConnectionIDFrame(f.seq)
+}
+
+// debugFramePathChallenge is a PATH_CHALLENGE frame.
+type debugFramePathChallenge struct {
+	data uint64
+}
+
+func parseDebugFramePathChallenge(b []byte) (f debugFramePathChallenge, n int) {
+	f.data, n = consumePathChallengeFrame(b)
+	return f, n
+}
+
+func (f debugFramePathChallenge) String() string {
+	return fmt.Sprintf("PATH_CHALLENGE Data=%016x", f.data)
+}
+
+func (f debugFramePathChallenge) write(w *packetWriter) bool {
+	return w.appendPathChallengeFrame(f.data)
+}
+
+// debugFramePathResponse is a PATH_RESPONSE frame.
+type debugFramePathResponse struct {
+	data uint64
+}
+
+func parseDebugFramePathResponse(b []byte) (f debugFramePathResponse, n int) {
+	f.data, n = consumePathResponseFrame(b)
+	return f, n
+}
+
+func (f debugFramePathResponse) String() string {
+	return fmt.Sprintf("PATH_RESPONSE Data=%016x", f.data)
+}
+
+func (f debugFramePathResponse) write(w *packetWriter) bool {
+	return w.appendPathResponseFrame(f.data)
+}
+
+// debugFrameConnectionCloseTransport is a CONNECTION_CLOSE frame carrying a transport error.
+type debugFrameConnectionCloseTransport struct {
+	code      transportError
+	frameType uint64
+	reason    string
+}
+
+func parseDebugFrameConnectionCloseTransport(b []byte) (f debugFrameConnectionCloseTransport, n int) {
+	f.code, f.frameType, f.reason, n = consumeConnectionCloseTransportFrame(b)
+	return f, n
+}
+
+func (f debugFrameConnectionCloseTransport) String() string {
+	s := fmt.Sprintf("CONNECTION_CLOSE Code=%v", f.code)
+	if f.frameType != 0 {
+		s += fmt.Sprintf(" FrameType=%v", f.frameType)
+	}
+	if f.reason != "" {
+		s += fmt.Sprintf(" Reason=%q", f.reason)
+	}
+	return s
+}
+
+func (f debugFrameConnectionCloseTransport) write(w *packetWriter) bool {
+	return w.appendConnectionCloseTransportFrame(f.code, f.frameType, f.reason)
+}
+
+// debugFrameConnectionCloseApplication is a CONNECTION_CLOSE frame carrying an application error.
+type debugFrameConnectionCloseApplication struct {
+	code   uint64
+	reason string
+}
+
+func parseDebugFrameConnectionCloseApplication(b []byte) (f debugFrameConnectionCloseApplication, n int) {
+	f.code, f.reason, n = consumeConnectionCloseApplicationFrame(b)
+	return f, n
+}
+
+func (f debugFrameConnectionCloseApplication) String() string {
+	s := fmt.Sprintf("CONNECTION_CLOSE AppCode=%v", f.code)
+	if f.reason != "" {
+		s += fmt.Sprintf(" Reason=%q", f.reason)
+	}
+	return s
+}
+
+func (f debugFrameConnectionCloseApplication) write(w *packetWriter) bool {
+	return w.appendConnectionCloseApplicationFrame(f.code, f.reason)
+}
+
+// debugFrameHandshakeDone is a HANDSHAKE_DONE frame.
+type debugFrameHandshakeDone struct{}
+
+func parseDebugFrameHandshakeDone(b []byte) (f debugFrameHandshakeDone, n int) {
+	return f, 1
+}
+
+func (f debugFrameHandshakeDone) String() string {
+	return "HANDSHAKE_DONE"
+}
+
+func (f debugFrameHandshakeDone) write(w *packetWriter) bool {
+	return w.appendHandshakeDoneFrame()
+}
diff --git a/internal/quic/packet_codec_test.go b/internal/quic/packet_codec_test.go
new file mode 100644
index 0000000..ee533c8
--- /dev/null
+++ b/internal/quic/packet_codec_test.go
@@ -0,0 +1,712 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+import (
+	"bytes"
+	"crypto/tls"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestParseLongHeaderPacket(t *testing.T) {
+	// Example Initial packet from:
+	// https://www.rfc-editor.org/rfc/rfc9001.html#section-a.3
+	cid := unhex(`8394c8f03e515708`)
+	_, initialServerKeys := initialKeys(cid)
+	pkt := unhex(`
+		cf000000010008f067a5502a4262b500 4075c0d95a482cd0991cd25b0aac406a
+		5816b6394100f37a1c69797554780bb3 8cc5a99f5ede4cf73c3ec2493a1839b3
+		dbcba3f6ea46c5b7684df3548e7ddeb9 c3bf9c73cc3f3bded74b562bfb19fb84
+		022f8ef4cdd93795d77d06edbb7aaf2f 58891850abbdca3d20398c276456cbc4
+		2158407dd074ee
+	`)
+	want := longPacket{
+		ptype:     packetTypeInitial,
+		version:   1,
+		num:       1,
+		dstConnID: []byte{},
+		srcConnID: unhex(`f067a5502a4262b5`),
+		payload: unhex(`
+			02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
+			88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
+			0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
+			020304
+		`),
+		extra: []byte{},
+	}
+
+	// Parse the packet.
+	got, n := parseLongHeaderPacket(pkt, initialServerKeys, 0)
+	if n != len(pkt) {
+		t.Errorf("parseLongHeaderPacket: n=%v, want %v", n, len(pkt))
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("parseLongHeaderPacket:\n got: %+v\nwant: %+v", got, want)
+	}
+
+	// Skip the packet.
+	if got, want := skipLongHeaderPacket(pkt), len(pkt); got != want {
+		t.Errorf("skipLongHeaderPacket: n=%v, want %v", got, want)
+	}
+
+	// Parse truncated versions of the packet; every attempt should fail.
+	for i := 0; i < len(pkt); i++ {
+		if _, n := parseLongHeaderPacket(pkt[:i], initialServerKeys, 0); n != -1 {
+			t.Fatalf("parse truncated long header packet: n=%v, want -1\ninput: %x", n, pkt[:i])
+		}
+		if n := skipLongHeaderPacket(pkt[:i]); n != -1 {
+			t.Errorf("skip truncated long header packet: n=%v, want -1", n)
+		}
+	}
+
+	// Parse with the wrong keys.
+	_, invalidKeys := initialKeys([]byte{})
+	if _, n := parseLongHeaderPacket(pkt, invalidKeys, 0); n != -1 {
+		t.Fatalf("parse long header packet with wrong keys: n=%v, want -1", n)
+	}
+}
+
+func TestRoundtripEncodeLongPacket(t *testing.T) {
+	aes128Keys, _ := newKeys(tls.TLS_AES_128_GCM_SHA256, []byte("secret"))
+	aes256Keys, _ := newKeys(tls.TLS_AES_256_GCM_SHA384, []byte("secret"))
+	chachaKeys, _ := newKeys(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret"))
+	for _, test := range []struct {
+		desc string
+		p    longPacket
+		k    keys
+	}{{
+		desc: "Initial, 1-byte number, AES128",
+		p: longPacket{
+			ptype:     packetTypeInitial,
+			version:   0x11223344,
+			num:       0, // 1-byte encodeing
+			dstConnID: []byte{1, 2, 3, 4},
+			srcConnID: []byte{5, 6, 7, 8},
+			payload:   []byte("payload"),
+			extra:     []byte("token"),
+		},
+		k: aes128Keys,
+	}, {
+		desc: "0-RTT, 2-byte number, AES256",
+		p: longPacket{
+			ptype:     packetType0RTT,
+			version:   0x11223344,
+			num:       0x100, // 2-byte encoding
+			dstConnID: []byte{1, 2, 3, 4},
+			srcConnID: []byte{5, 6, 7, 8},
+			payload:   []byte("payload"),
+		},
+		k: aes256Keys,
+	}, {
+		desc: "0-RTT, 3-byte number, AES256",
+		p: longPacket{
+			ptype:     packetType0RTT,
+			version:   0x11223344,
+			num:       0x10000, // 2-byte encoding
+			dstConnID: []byte{1, 2, 3, 4},
+			srcConnID: []byte{5, 6, 7, 8},
+			payload:   []byte{0},
+		},
+		k: aes256Keys,
+	}, {
+		desc: "Handshake, 4-byte number, ChaCha20Poly1305",
+		p: longPacket{
+			ptype:     packetTypeHandshake,
+			version:   0x11223344,
+			num:       0x1000000, // 4-byte encoding
+			dstConnID: []byte{1, 2, 3, 4},
+			srcConnID: []byte{5, 6, 7, 8},
+			payload:   []byte("payload"),
+		},
+		k: chachaKeys,
+	}} {
+		t.Run(test.desc, func(t *testing.T) {
+			var w packetWriter
+			w.reset(1200)
+			w.startProtectedLongHeaderPacket(0, test.p)
+			w.b = append(w.b, test.p.payload...)
+			w.finishProtectedLongHeaderPacket(0, test.k, test.p)
+			pkt := w.datagram()
+
+			got, n := parseLongHeaderPacket(pkt, test.k, 0)
+			if n != len(pkt) {
+				t.Errorf("parseLongHeaderPacket: n=%v, want %v", n, len(pkt))
+			}
+			if !reflect.DeepEqual(got, test.p) {
+				t.Errorf("Round-trip encode/decode did not preserve packet.\nsent: %+v\n got: %+v\nwire: %x", test.p, got, pkt)
+			}
+		})
+	}
+}
+
+func TestRoundtripEncodeShortPacket(t *testing.T) {
+	aes128Keys, _ := newKeys(tls.TLS_AES_128_GCM_SHA256, []byte("secret"))
+	aes256Keys, _ := newKeys(tls.TLS_AES_256_GCM_SHA384, []byte("secret"))
+	chachaKeys, _ := newKeys(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret"))
+	connID := make([]byte, connIDLen)
+	for i := range connID {
+		connID[i] = byte(i)
+	}
+	for _, test := range []struct {
+		desc    string
+		num     packetNumber
+		payload []byte
+		k       keys
+	}{{
+		desc:    "1-byte number, AES128",
+		num:     0, // 1-byte encoding,
+		payload: []byte("payload"),
+		k:       aes128Keys,
+	}, {
+		desc:    "2-byte number, AES256",
+		num:     0x100, // 2-byte encoding
+		payload: []byte("payload"),
+		k:       aes256Keys,
+	}, {
+		desc:    "3-byte number, ChaCha20Poly1305",
+		num:     0x10000, // 3-byte encoding
+		payload: []byte("payload"),
+		k:       chachaKeys,
+	}, {
+		desc:    "4-byte number, ChaCha20Poly1305",
+		num:     0x1000000, // 4-byte encoding
+		payload: []byte{0},
+		k:       chachaKeys,
+	}} {
+		t.Run(test.desc, func(t *testing.T) {
+			var w packetWriter
+			w.reset(1200)
+			w.start1RTTPacket(test.num, 0, connID)
+			w.b = append(w.b, test.payload...)
+			w.finish1RTTPacket(test.num, 0, connID, test.k)
+			pkt := w.datagram()
+			p, n := parse1RTTPacket(pkt, test.k, 0)
+			if n != len(pkt) {
+				t.Errorf("parse1RTTPacket: n=%v, want %v", n, len(pkt))
+			}
+			if p.num != test.num || !bytes.Equal(p.payload, test.payload) {
+				t.Errorf("Round-trip encode/decode did not preserve packet.\nsent: num=%v, payload={%x}\ngot: num=%v, payload={%x}", test.num, test.payload, p.num, p.payload)
+			}
+		})
+	}
+}
+
+func TestFrameEncodeDecode(t *testing.T) {
+	for _, test := range []struct {
+		s         string
+		f         debugFrame
+		b         []byte
+		truncated []byte
+	}{{
+		s: "PADDING*1",
+		f: debugFramePadding{
+			size: 1,
+		},
+		b: []byte{
+			0x00, // Type (i) = 0x00,
+
+		},
+	}, {
+		s: "PING",
+		f: debugFramePing{},
+		b: []byte{
+			0x01, // TYPE(i) = 0x01
+		},
+	}, {
+		s: "ACK Delay=80µs [0,16) [17,32) [48,64)",
+		f: debugFrameAck{
+			ackDelay: (10 << ackDelayExponent) * time.Microsecond,
+			ranges: []i64range{
+				{0x00, 0x10},
+				{0x11, 0x20},
+				{0x30, 0x40},
+			},
+		},
+		b: []byte{
+			0x02, // TYPE (i) = 0x02
+			0x3f, // Largest Acknowledged (i)
+			10,   // ACK Delay (i)
+			0x02, // ACK Range Count (i)
+			0x0f, // First ACK Range (i)
+			0x0f, // Gap (i)
+			0x0e, // ACK Range Length (i)
+			0x00, // Gap (i)
+			0x0f, // ACK Range Length (i)
+		},
+		truncated: []byte{
+			0x02, // TYPE (i) = 0x02
+			0x3f, // Largest Acknowledged (i)
+			10,   // ACK Delay (i)
+			0x01, // ACK Range Count (i)
+			0x0f, // First ACK Range (i)
+			0x0f, // Gap (i)
+			0x0e, // ACK Range Length (i)
+		},
+	}, {
+		s: "RESET_STREAM ID=1 Code=2 FinalSize=3",
+		f: debugFrameResetStream{
+			id:        1,
+			code:      2,
+			finalSize: 3,
+		},
+		b: []byte{
+			0x04, // TYPE(i) = 0x04
+			0x01, // Stream ID (i),
+			0x02, // Application Protocol Error Code (i),
+			0x03, // Final Size (i),
+		},
+	}, {
+		s: "STOP_SENDING ID=1 Code=2",
+		f: debugFrameStopSending{
+			id:   1,
+			code: 2,
+		},
+		b: []byte{
+			0x05, // TYPE(i) = 0x05
+			0x01, // Stream ID (i),
+			0x02, // Application Protocol Error Code (i),
+		},
+	}, {
+		s: "CRYPTO Offset=1 Length=2",
+		f: debugFrameCrypto{
+			off:  1,
+			data: []byte{3, 4},
+		},
+		b: []byte{
+			0x06,       // Type (i) = 0x06,
+			0x01,       // Offset (i),
+			0x02,       // Length (i),
+			0x03, 0x04, // Crypto Data (..),
+		},
+		truncated: []byte{
+			0x06, // Type (i) = 0x06,
+			0x01, // Offset (i),
+			0x01, // Length (i),
+			0x03,
+		},
+	}, {
+		s: "NEW_TOKEN Token=0304",
+		f: debugFrameNewToken{
+			token: []byte{3, 4},
+		},
+		b: []byte{
+			0x07,       // Type (i) = 0x07,
+			0x02,       // Token Length (i),
+			0x03, 0x04, // Token (..),
+		},
+	}, {
+		s: "STREAM ID=1 Offset=0 Length=0",
+		f: debugFrameStream{
+			id:   1,
+			fin:  false,
+			off:  0,
+			data: []byte{},
+		},
+		b: []byte{
+			0x0a, // Type (i) = 0x08..0x0f,
+			0x01, // Stream ID (i),
+			// [Offset (i)],
+			0x00, // [Length (i)],
+			// Stream Data (..),
+		},
+	}, {
+		s: "STREAM ID=100 Offset=4 Length=3",
+		f: debugFrameStream{
+			id:   100,
+			fin:  false,
+			off:  4,
+			data: []byte{0xa0, 0xa1, 0xa2},
+		},
+		b: []byte{
+			0x0e,       // Type (i) = 0x08..0x0f,
+			0x40, 0x64, // Stream ID (i),
+			0x04,             // [Offset (i)],
+			0x03,             // [Length (i)],
+			0xa0, 0xa1, 0xa2, // Stream Data (..),
+		},
+		truncated: []byte{
+			0x0e,       // Type (i) = 0x08..0x0f,
+			0x40, 0x64, // Stream ID (i),
+			0x04,       // [Offset (i)],
+			0x02,       // [Length (i)],
+			0xa0, 0xa1, // Stream Data (..),
+		},
+	}, {
+		s: "STREAM ID=100 FIN Offset=4 Length=3",
+		f: debugFrameStream{
+			id:   100,
+			fin:  true,
+			off:  4,
+			data: []byte{0xa0, 0xa1, 0xa2},
+		},
+		b: []byte{
+			0x0f,       // Type (i) = 0x08..0x0f,
+			0x40, 0x64, // Stream ID (i),
+			0x04,             // [Offset (i)],
+			0x03,             // [Length (i)],
+			0xa0, 0xa1, 0xa2, // Stream Data (..),
+		},
+		truncated: []byte{
+			0x0e,       // Type (i) = 0x08..0x0f,
+			0x40, 0x64, // Stream ID (i),
+			0x04,       // [Offset (i)],
+			0x02,       // [Length (i)],
+			0xa0, 0xa1, // Stream Data (..),
+		},
+	}, {
+		s: "STREAM ID=1 FIN Offset=100 Length=0",
+		f: debugFrameStream{
+			id:   1,
+			fin:  true,
+			off:  100,
+			data: []byte{},
+		},
+		b: []byte{
+			0x0f,       // Type (i) = 0x08..0x0f,
+			0x01,       // Stream ID (i),
+			0x40, 0x64, // [Offset (i)],
+			0x00, // [Length (i)],
+			// Stream Data (..),
+		},
+	}, {
+		s: "MAX_DATA Max=10",
+		f: debugFrameMaxData{
+			max: 10,
+		},
+		b: []byte{
+			0x10, // Type (i) = 0x10,
+			0x0a, // Maximum Data (i),
+		},
+	}, {
+		s: "MAX_STREAM_DATA ID=1 Max=10",
+		f: debugFrameMaxStreamData{
+			id:  1,
+			max: 10,
+		},
+		b: []byte{
+			0x11, // Type (i) = 0x11,
+			0x01, // Stream ID (i),
+			0x0a, // Maximum Stream Data (i),
+		},
+	}, {
+		s: "MAX_STREAMS Type=bidi Max=1",
+		f: debugFrameMaxStreams{
+			streamType: bidiStream,
+			max:        1,
+		},
+		b: []byte{
+			0x12, //   Type (i) = 0x12..0x13,
+			0x01, // Maximum Streams (i),
+		},
+	}, {
+		s: "MAX_STREAMS Type=uni Max=1",
+		f: debugFrameMaxStreams{
+			streamType: uniStream,
+			max:        1,
+		},
+		b: []byte{
+			0x13, //   Type (i) = 0x12..0x13,
+			0x01, // Maximum Streams (i),
+		},
+	}, {
+		s: "DATA_BLOCKED Max=1",
+		f: debugFrameDataBlocked{
+			max: 1,
+		},
+		b: []byte{
+			0x14, // Type (i) = 0x14,
+			0x01, // Maximum Data (i),
+		},
+	}, {
+		s: "STREAM_DATA_BLOCKED ID=1 Max=2",
+		f: debugFrameStreamDataBlocked{
+			id:  1,
+			max: 2,
+		},
+		b: []byte{
+			0x15, // Type (i) = 0x15,
+			0x01, // Stream ID (i),
+			0x02, // Maximum Stream Data (i),
+		},
+	}, {
+		s: "STREAMS_BLOCKED Type=bidi Max=1",
+		f: debugFrameStreamsBlocked{
+			streamType: bidiStream,
+			max:        1,
+		},
+		b: []byte{
+			0x16, // Type (i) = 0x16..0x17,
+			0x01, // Maximum Streams (i),
+		},
+	}, {
+		s: "STREAMS_BLOCKED Type=uni Max=1",
+		f: debugFrameStreamsBlocked{
+			streamType: uniStream,
+			max:        1,
+		},
+		b: []byte{
+			0x17, // Type (i) = 0x16..0x17,
+			0x01, // Maximum Streams (i),
+		},
+	}, {
+		s: "NEW_CONNECTION_ID Seq=3 Retire=2 ID=a0a1a2a3 Token=0102030405060708090a0b0c0d0e0f10",
+		f: debugFrameNewConnectionID{
+			seq:           3,
+			retirePriorTo: 2,
+			connID:        []byte{0xa0, 0xa1, 0xa2, 0xa3},
+			token:         [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
+		},
+		b: []byte{
+			0x18,                   // Type (i) = 0x18,
+			0x03,                   // Sequence Number (i),
+			0x02,                   // Retire Prior To (i),
+			0x04,                   // Length (8),
+			0xa0, 0xa1, 0xa2, 0xa3, // Connection ID (8..160),
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // Stateless Reset Token (128),
+		},
+	}, {
+		s: "RETIRE_CONNECTION_ID Seq=1",
+		f: debugFrameRetireConnectionID{
+			seq: 1,
+		},
+		b: []byte{
+			0x19, // Type (i) = 0x19,
+			0x01, // Sequence Number (i),
+		},
+	}, {
+		s: "PATH_CHALLENGE Data=0123456789abcdef",
+		f: debugFramePathChallenge{
+			data: 0x0123456789abcdef,
+		},
+		b: []byte{
+			0x1a,                                           // Type (i) = 0x1a,
+			0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // Data (64),
+		},
+	}, {
+		s: "PATH_RESPONSE Data=0123456789abcdef",
+		f: debugFramePathResponse{
+			data: 0x0123456789abcdef,
+		},
+		b: []byte{
+			0x1b,                                           // Type (i) = 0x1b,
+			0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // Data (64),
+		},
+	}, {
+		s: `CONNECTION_CLOSE Code=INTERNAL_ERROR FrameType=2 Reason="oops"`,
+		f: debugFrameConnectionCloseTransport{
+			code:      1,
+			frameType: 2,
+			reason:    "oops",
+		},
+		b: []byte{
+			0x1c,               // Type (i) = 0x1c..0x1d,
+			0x01,               // Error Code (i),
+			0x02,               // [Frame Type (i)],
+			0x04,               // Reason Phrase Length (i),
+			'o', 'o', 'p', 's', // Reason Phrase (..),
+		},
+	}, {
+		s: `CONNECTION_CLOSE AppCode=1 Reason="oops"`,
+		f: debugFrameConnectionCloseApplication{
+			code:   1,
+			reason: "oops",
+		},
+		b: []byte{
+			0x1d,               // Type (i) = 0x1c..0x1d,
+			0x01,               // Error Code (i),
+			0x04,               // Reason Phrase Length (i),
+			'o', 'o', 'p', 's', // Reason Phrase (..),
+		},
+	}, {
+		s: "HANDSHAKE_DONE",
+		f: debugFrameHandshakeDone{},
+		b: []byte{
+			0x1e, // Type (i) = 0x1e,
+		},
+	}} {
+		var w packetWriter
+		w.reset(1200)
+		w.start1RTTPacket(0, 0, nil)
+		w.pktLim = w.payOff + len(test.b)
+		if added := test.f.write(&w); !added {
+			t.Errorf("encoding %v with %v bytes available: write unexpectedly failed", test.f, len(test.b))
+		}
+		if got, want := w.payload(), test.b; !bytes.Equal(got, want) {
+			t.Errorf("encoding %v:\ngot  {%x}\nwant {%x}", test.f, got, want)
+		}
+		gotf, n := parseDebugFrame(test.b)
+		if n != len(test.b) || !reflect.DeepEqual(gotf, test.f) {
+			t.Errorf("decoding {%x}:\ndecoded %v bytes, want %v\ngot:  %v\nwant: %v", test.b, n, len(test.b), gotf, test.f)
+		}
+		if got, want := test.f.String(), test.s; got != want {
+			t.Errorf("frame.String():\ngot  %q\nwant %q", got, want)
+		}
+
+		// Try encoding the frame into too little space.
+		// Most frames will result in an error; some (like STREAM frames) will truncate
+		// the data written.
+		w.reset(1200)
+		w.start1RTTPacket(0, 0, nil)
+		w.pktLim = w.payOff + len(test.b) - 1
+		if added := test.f.write(&w); added {
+			if test.truncated == nil {
+				t.Errorf("encoding %v with %v-1 bytes available: write unexpectedly succeeded", test.f, len(test.b))
+			} else if got, want := w.payload(), test.truncated; !bytes.Equal(got, want) {
+				t.Errorf("encoding %v with %v-1 bytes available:\ngot  {%x}\nwant {%x}", test.f, len(test.b), got, want)
+			}
+		}
+
+		// Try parsing truncated data.
+		for i := 0; i < len(test.b); i++ {
+			f, n := parseDebugFrame(test.b[:i])
+			if n >= 0 {
+				t.Errorf("decoding truncated frame {%x}:\ngot: %v\nwant error", test.b[:i], f)
+			}
+		}
+	}
+}
+
+func TestFrameDecode(t *testing.T) {
+	for _, test := range []struct {
+		desc string
+		want debugFrame
+		b    []byte
+	}{{
+		desc: "STREAM frame with LEN bit unset",
+		want: debugFrameStream{
+			id:   1,
+			fin:  false,
+			data: []byte{0x01, 0x02, 0x03},
+		},
+		b: []byte{
+			0x08, // Type (i) = 0x08..0x0f,
+			0x01, // Stream ID (i),
+			// [Offset (i)],
+			// [Length (i)],
+			0x01, 0x02, 0x03, // Stream Data (..),
+		},
+	}, {
+		desc: "ACK frame with ECN counts",
+		want: debugFrameAck{
+			ackDelay: (10 << ackDelayExponent) * time.Microsecond,
+			ranges: []i64range{
+				{0, 1},
+			},
+		},
+		b: []byte{
+			0x03,             // TYPE (i) = 0x02..0x03
+			0x00,             // Largest Acknowledged (i)
+			10,               // ACK Delay (i)
+			0x00,             // ACK Range Count (i)
+			0x00,             // First ACK Range (i)
+			0x01, 0x02, 0x03, // [ECN Counts (..)],
+		},
+	}} {
+		got, n := parseDebugFrame(test.b)
+		if n != len(test.b) || !reflect.DeepEqual(got, test.want) {
+			t.Errorf("decoding {%x}:\ndecoded %v bytes, want %v\ngot:  %v\nwant: %v", test.b, n, len(test.b), got, test.want)
+		}
+	}
+}
+
+func TestFrameDecodeErrors(t *testing.T) {
+	for _, test := range []struct {
+		name string
+		b    []byte
+	}{{
+		name: "ACK [-1,0]",
+		b: []byte{
+			0x02, // TYPE (i) = 0x02
+			0x00, // Largest Acknowledged (i)
+			0x00, // ACK Delay (i)
+			0x00, // ACK Range Count (i)
+			0x01, // First ACK Range (i)
+		},
+	}, {
+		name: "ACK [-1,16]",
+		b: []byte{
+			0x02, // TYPE (i) = 0x02
+			0x10, // Largest Acknowledged (i)
+			0x00, // ACK Delay (i)
+			0x00, // ACK Range Count (i)
+			0x11, // First ACK Range (i)
+		},
+	}, {
+		name: "ACK [-1,0],[1,2]",
+		b: []byte{
+			0x02, // TYPE (i) = 0x02
+			0x02, // Largest Acknowledged (i)
+			0x00, // ACK Delay (i)
+			0x01, // ACK Range Count (i)
+			0x00, // First ACK Range (i)
+			0x01, // Gap (i)
+			0x01, // ACK Range Length (i)
+		},
+	}, {
+		name: "NEW_TOKEN with zero-length token",
+		b: []byte{
+			0x07, // Type (i) = 0x07,
+			0x00, // Token Length (i),
+		},
+	}, {
+		name: "MAX_STREAMS with too many streams",
+		b: func() []byte {
+			// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.11-5.2.1
+			return appendVarint([]byte{frameTypeMaxStreamsBidi}, (1<<60)+1)
+		}(),
+	}, {
+		name: "NEW_CONNECTION_ID too small",
+		b: []byte{
+			0x18, // Type (i) = 0x18,
+			0x03, // Sequence Number (i),
+			0x02, // Retire Prior To (i),
+			0x00, // Length (8),
+			// Connection ID (8..160),
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // Stateless Reset Token (128),
+		},
+	}, {
+		name: "NEW_CONNECTION_ID too large",
+		b: []byte{
+			0x18, // Type (i) = 0x18,
+			0x03, // Sequence Number (i),
+			0x02, // Retire Prior To (i),
+			21,   // Length (8),
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+			11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, // Connection ID (8..160),
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // Stateless Reset Token (128),
+		},
+	}, {
+		name: "NEW_CONNECTION_ID sequence smaller than retire",
+		b: []byte{
+			0x18,       // Type (i) = 0x18,
+			0x02,       // Sequence Number (i),
+			0x03,       // Retire Prior To (i),
+			0x02,       // Length (8),
+			0xff, 0xff, // Connection ID (8..160),
+			1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // Stateless Reset Token (128),
+		},
+	}} {
+		f, n := parseDebugFrame(test.b)
+		if n >= 0 {
+			t.Errorf("%v: no error when parsing invalid frame {%x}\ngot: %v", test.name, test.b, f)
+		}
+	}
+}
+
+func FuzzParseLongHeaderPacket(f *testing.F) {
+	cid := unhex(`0000000000000000`)
+	_, initialServerKeys := initialKeys(cid)
+	f.Fuzz(func(t *testing.T, in []byte) {
+		parseLongHeaderPacket(in, initialServerKeys, 0)
+	})
+}
+
+func FuzzFrameDecode(f *testing.F) {
+	f.Fuzz(func(t *testing.T, in []byte) {
+		parseDebugFrame(in)
+	})
+}
diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go
new file mode 100644
index 0000000..d06b601
--- /dev/null
+++ b/internal/quic/packet_parser.go
@@ -0,0 +1,524 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+import (
+	"time"
+)
+
+// parseLongHeaderPacket parses a QUIC long header packet.
+//
+// It does not parse Version Negotiation packets.
+//
+// On input, pkt contains a long header packet (possibly followed by more packets),
+// k the decryption keys for the packet, and pnumMax the largest packet number seen
+// in the number space of this packet.
+//
+// parseLongHeaderPacket returns the parsed packet with protection removed
+// and its length in bytes.
+//
+// It returns an empty packet and -1 if the packet could not be parsed.
+func parseLongHeaderPacket(pkt []byte, k keys, pnumMax packetNumber) (p longPacket, n int) {
+	if len(pkt) < 5 || !isLongHeader(pkt[0]) {
+		return longPacket{}, -1
+	}
+
+	// Header Form (1) = 1,
+	// Fixed Bit (1) = 1,
+	// Long Packet Type (2),
+	// Type-Specific Bits (4),
+	b := pkt
+	p.ptype = getPacketType(b)
+	if p.ptype == packetTypeInvalid {
+		return longPacket{}, -1
+	}
+	b = b[1:]
+	// Version (32),
+	p.version, n = consumeUint32(b)
+	if n < 0 {
+		return longPacket{}, -1
+	}
+	b = b[n:]
+	if p.version == 0 {
+		// Version Negotiation packet; not handled here.
+		return longPacket{}, -1
+	}
+
+	// Destination Connection ID Length (8),
+	// Destination Connection ID (0..160),
+	p.dstConnID, n = consumeUint8Bytes(b)
+	if n < 0 || len(p.dstConnID) > 20 {
+		return longPacket{}, -1
+	}
+	b = b[n:]
+
+	// Source Connection ID Length (8),
+	// Source Connection ID (0..160),
+	p.srcConnID, n = consumeUint8Bytes(b)
+	if n < 0 || len(p.dstConnID) > 20 {
+		return longPacket{}, -1
+	}
+	b = b[n:]
+
+	switch p.ptype {
+	case packetTypeInitial:
+		// Token Length (i),
+		// Token (..),
+		p.extra, n = consumeVarintBytes(b)
+		if n < 0 {
+			return longPacket{}, -1
+		}
+		b = b[n:]
+	case packetTypeRetry:
+		// Retry Token (..),
+		// Retry Integrity Tag (128),
+		p.extra = b
+		return p, len(pkt)
+	}
+
+	// Length (i),
+	payLen, n := consumeVarint(b)
+	if n < 0 {
+		return longPacket{}, -1
+	}
+	b = b[n:]
+	if uint64(len(b)) < payLen {
+		return longPacket{}, -1
+	}
+
+	// Packet Number (8..32),
+	// Packet Payload (..),
+	pnumOff := len(pkt) - len(b)
+	pkt = pkt[:pnumOff+int(payLen)]
+
+	if k.initialized() {
+		var err error
+		p.payload, p.num, err = k.unprotect(pkt, pnumOff, pnumMax)
+		if err != nil {
+			return longPacket{}, -1
+		}
+		// Reserved bits should always be zero, but this is handled
+		// as a protocol-level violation by the caller rather than a parse error.
+		p.reservedBits = pkt[0] & reservedBits
+	}
+	return p, len(pkt)
+}
+
+// skipLongHeaderPacket returns the length of the long header packet at the start of pkt,
+// or -1 if the buffer does not contain a valid packet.
+func skipLongHeaderPacket(pkt []byte) int {
+	// Header byte, 4 bytes of version.
+	n := 5
+	if len(pkt) <= n {
+		return -1
+	}
+	// Destination connection ID length, destination connection ID.
+	n += 1 + int(pkt[n])
+	if len(pkt) <= n {
+		return -1
+	}
+	// Source connection ID length, source connection ID.
+	n += 1 + int(pkt[n])
+	if len(pkt) <= n {
+		return -1
+	}
+	if getPacketType(pkt) == packetTypeInitial {
+		// Token length, token.
+		_, nn := consumeVarintBytes(pkt[n:])
+		if nn < 0 {
+			return -1
+		}
+		n += nn
+	}
+	// Length, packet number, payload.
+	_, nn := consumeVarintBytes(pkt[n:])
+	if nn < 0 {
+		return -1
+	}
+	n += nn
+	if len(pkt) < n {
+		return -1
+	}
+	return n
+}
+
+// parse1RTTPacket parses a QUIC 1-RTT (short header) packet.
+//
+// On input, pkt contains a short header packet, k the decryption keys for the packet,
+// and pnumMax the largest packet number seen in the number space of this packet.
+func parse1RTTPacket(pkt []byte, k keys, pnumMax packetNumber) (p shortPacket, n int) {
+	var err error
+	p.payload, p.num, err = k.unprotect(pkt, 1+connIDLen, pnumMax)
+	if err != nil {
+		return shortPacket{}, -1
+	}
+	// Reserved bits should always be zero, but this is handled
+	// as a protocol-level violation by the caller rather than a parse error.
+	p.reservedBits = pkt[0] & reservedBits
+	return p, len(pkt)
+}
+
+// Consume functions return n=-1 on conditions which result in FRAME_ENCODING_ERROR,
+// which includes both general parse failures and specific violations of frame
+// constraints.
+
+func consumeAckFrame(frame []byte, ackDelayExponent uint8, f func(start, end packetNumber)) (largest packetNumber, ackDelay time.Duration, n int) {
+	b := frame[1:] // type
+
+	largestAck, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+
+	ackDelayScaled, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+	ackDelay = time.Duration(ackDelayScaled*(1<<ackDelayExponent)) * time.Microsecond
+
+	ackRangeCount, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+
+	rangeMax := packetNumber(largestAck)
+	for i := uint64(0); ; i++ {
+		rangeLen, n := consumeVarint(b)
+		if n < 0 {
+			return 0, 0, -1
+		}
+		b = b[n:]
+		rangeMin := rangeMax - packetNumber(rangeLen)
+		if rangeMin < 0 || rangeMin > rangeMax {
+			return 0, 0, -1
+		}
+		f(rangeMin, rangeMax+1)
+
+		if i == ackRangeCount {
+			break
+		}
+
+		gap, n := consumeVarint(b)
+		if n < 0 {
+			return 0, 0, -1
+		}
+		b = b[n:]
+
+		rangeMax = rangeMin - packetNumber(gap) - 2
+	}
+
+	if frame[0] != frameTypeAckECN {
+		return packetNumber(largestAck), ackDelay, len(frame) - len(b)
+	}
+
+	ect0Count, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+	ect1Count, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+	ecnCECount, n := consumeVarint(b)
+	if n < 0 {
+		return 0, 0, -1
+	}
+	b = b[n:]
+
+	// TODO: Make use of ECN feedback.
+	// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.3.2
+	_ = ect0Count
+	_ = ect1Count
+	_ = ecnCECount
+
+	return packetNumber(largestAck), ackDelay, len(frame) - len(b)
+}
+
+func consumeResetStreamFrame(b []byte) (id streamID, code uint64, finalSize int64, n int) {
+	n = 1
+	idInt, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, 0, -1
+	}
+	n += nn
+	code, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, 0, -1
+	}
+	n += nn
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, 0, -1
+	}
+	n += nn
+	finalSize = int64(v)
+	return streamID(idInt), code, finalSize, n
+}
+
+func consumeStopSendingFrame(b []byte) (id streamID, code uint64, n int) {
+	n = 1
+	idInt, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	code, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	return streamID(idInt), code, n
+}
+
+func consumeCryptoFrame(b []byte) (off int64, data []byte, n int) {
+	n = 1
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, nil, -1
+	}
+	off = int64(v)
+	n += nn
+	data, nn = consumeVarintBytes(b[n:])
+	if nn < 0 {
+		return 0, nil, -1
+	}
+	n += nn
+	return off, data, n
+}
+
+func consumeNewTokenFrame(b []byte) (token []byte, n int) {
+	n = 1
+	data, nn := consumeVarintBytes(b[n:])
+	if nn < 0 {
+		return nil, -1
+	}
+	if len(data) == 0 {
+		return nil, -1
+	}
+	n += nn
+	return data, n
+}
+
+func consumeStreamFrame(b []byte) (id streamID, off int64, fin bool, data []byte, n int) {
+	fin = (b[0] & 0x01) != 0
+	n = 1
+	idInt, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, false, nil, -1
+	}
+	n += nn
+	if b[0]&0x04 != 0 {
+		v, nn := consumeVarint(b[n:])
+		if nn < 0 {
+			return 0, 0, false, nil, -1
+		}
+		n += nn
+		off = int64(v)
+	}
+	if b[0]&0x02 != 0 {
+		data, nn = consumeVarintBytes(b[n:])
+		if nn < 0 {
+			return 0, 0, false, nil, -1
+		}
+		n += nn
+	} else {
+		data = b[n:]
+		n += len(data)
+	}
+	return streamID(idInt), off, fin, data, n
+}
+
+func consumeMaxDataFrame(b []byte) (max int64, n int) {
+	n = 1
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, -1
+	}
+	n += nn
+	return int64(v), n
+}
+
+func consumeMaxStreamDataFrame(b []byte) (id streamID, max int64, n int) {
+	n = 1
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	id = streamID(v)
+	v, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	max = int64(v)
+	return id, max, n
+}
+
+func consumeMaxStreamsFrame(b []byte) (typ streamType, max int64, n int) {
+	switch b[0] {
+	case frameTypeMaxStreamsBidi:
+		typ = bidiStream
+	case frameTypeMaxStreamsUni:
+		typ = uniStream
+	default:
+		return 0, 0, -1
+	}
+	n = 1
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	if v > 1<<60 {
+		return 0, 0, -1
+	}
+	return typ, int64(v), n
+}
+
+func consumeStreamDataBlockedFrame(b []byte) (id streamID, max int64, n int) {
+	n = 1
+	v, nn := consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	id = streamID(v)
+	max, nn = consumeVarintInt64(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	return id, max, n
+}
+
+func consumeDataBlockedFrame(b []byte) (max int64, n int) {
+	n = 1
+	max, nn := consumeVarintInt64(b[n:])
+	if nn < 0 {
+		return 0, -1
+	}
+	n += nn
+	return max, n
+}
+
+func consumeStreamsBlockedFrame(b []byte) (typ streamType, max int64, n int) {
+	if b[0] == frameTypeStreamsBlockedBidi {
+		typ = bidiStream
+	} else {
+		typ = uniStream
+	}
+	n = 1
+	max, nn := consumeVarintInt64(b[n:])
+	if nn < 0 {
+		return 0, 0, -1
+	}
+	n += nn
+	return typ, max, n
+}
+
+func consumeNewConnectionIDFrame(b []byte) (seq, retire int64, connID []byte, resetToken [16]byte, n int) {
+	n = 1
+	var nn int
+	seq, nn = consumeVarintInt64(b[n:])
+	if nn < 0 {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	n += nn
+	retire, nn = consumeVarintInt64(b[n:])
+	if nn < 0 {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	n += nn
+	if seq < retire {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	connID, nn = consumeVarintBytes(b[n:])
+	if nn < 0 {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	if len(connID) < 1 || len(connID) > 20 {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	n += nn
+	if len(b[n:]) < len(resetToken) {
+		return 0, 0, nil, [16]byte{}, -1
+	}
+	copy(resetToken[:], b[n:])
+	n += len(resetToken)
+	return seq, retire, connID, resetToken, n
+}
+
+func consumeRetireConnectionIDFrame(b []byte) (seq uint64, n int) {
+	n = 1
+	var nn int
+	seq, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, -1
+	}
+	n += nn
+	return seq, n
+}
+
+func consumePathChallengeFrame(b []byte) (data uint64, n int) {
+	n = 1
+	var nn int
+	data, nn = consumeUint64(b[n:])
+	if nn < 0 {
+		return 0, -1
+	}
+	n += nn
+	return data, n
+}
+
+func consumePathResponseFrame(b []byte) (data uint64, n int) {
+	return consumePathChallengeFrame(b) // identical frame format
+}
+
+func consumeConnectionCloseTransportFrame(b []byte) (code transportError, frameType uint64, reason string, n int) {
+	n = 1
+	var nn int
+	var codeInt uint64
+	codeInt, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, "", -1
+	}
+	code = transportError(codeInt)
+	n += nn
+	frameType, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, 0, "", -1
+	}
+	n += nn
+	reasonb, nn := consumeVarintBytes(b[n:])
+	if nn < 0 {
+		return 0, 0, "", -1
+	}
+	n += nn
+	reason = string(reasonb)
+	return code, frameType, reason, n
+}
+
+func consumeConnectionCloseApplicationFrame(b []byte) (code uint64, reason string, n int) {
+	n = 1
+	var nn int
+	code, nn = consumeVarint(b[n:])
+	if nn < 0 {
+		return 0, "", -1
+	}
+	n += nn
+	reasonb, nn := consumeVarintBytes(b[n:])
+	if nn < 0 {
+		return 0, "", -1
+	}
+	n += nn
+	reason = string(reasonb)
+	return code, reason, n
+}
diff --git a/internal/quic/packet_writer.go b/internal/quic/packet_writer.go
new file mode 100644
index 0000000..1f9e30f
--- /dev/null
+++ b/internal/quic/packet_writer.go
@@ -0,0 +1,548 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package quic
+
+import (
+	"encoding/binary"
+	"time"
+)
+
+// A packetWriter constructs QUIC datagrams.
+//
+// A datagram consists of one or more packets.
+// A packet consists of a header followed by one or more frames.
+//
+// Packets are written in three steps:
+// - startProtectedLongHeaderPacket or start1RTT packet prepare the packet;
+// - append*Frame appends frames to the payload; and
+// - finishProtectedLongHeaderPacket or finish1RTT finalize the packet.
+//
+// The start functions are efficient, so we can start speculatively
+// writing a packet before we know whether we have any frames to
+// put in it. The finish functions will abandon the packet if the
+// payload contains no data.
+type packetWriter struct {
+	dgramLim int // max datagram size
+	pktLim   int // max packet size
+	pktOff   int // offset of the start of the current packet
+	payOff   int // offset of the payload of the current packet
+	b        []byte
+	sent     *sentPacket
+}
+
+// reset prepares to write a datagram of at most lim bytes.
+func (w *packetWriter) reset(lim int) {
+	if cap(w.b) < lim {
+		w.b = make([]byte, 0, lim)
+	}
+	w.dgramLim = lim
+	w.b = w.b[:0]
+}
+
+// datagram returns the current datagram.
+func (w *packetWriter) datagram() []byte {
+	return w.b
+}
+
+// payload returns the payload of the current packet.
+func (w *packetWriter) payload() []byte {
+	return w.b[w.payOff:]
+}
+
+func (w *packetWriter) abandonPacket() {
+	w.b = w.b[:w.payOff]
+	w.sent.reset()
+}
+
+// startProtectedLongHeaderPacket starts writing an Initial, 0-RTT, or Handshake packet.
+func (w *packetWriter) startProtectedLongHeaderPacket(pnumMaxAcked packetNumber, p longPacket) {
+	if w.sent == nil {
+		w.sent = newSentPacket()
+	}
+	w.pktOff = len(w.b)
+	hdrSize := 1 // packet type
+	hdrSize += 4 // version
+	hdrSize += 1 + len(p.dstConnID)
+	hdrSize += 1 + len(p.srcConnID)
+	switch p.ptype {
+	case packetTypeInitial:
+		hdrSize += sizeVarint(uint64(len(p.extra))) + len(p.extra)
+	}
+	hdrSize += 2 // length, hardcoded to a 2-byte varint
+	pnumOff := len(w.b) + hdrSize
+	hdrSize += packetNumberLength(p.num, pnumMaxAcked)
+	payOff := len(w.b) + hdrSize
+	// Check if we have enough space to hold the packet, including the header,
+	// header protection sample (RFC 9001, section 5.4.2), and encryption overhead.
+	if pnumOff+4+headerProtectionSampleSize+aeadOverhead >= w.dgramLim {
+		// Set the limit on the packet size to be the current write buffer length,
+		// ensuring that any writes to the payload fail.
+		w.payOff = len(w.b)
+		w.pktLim = len(w.b)
+		return
+	}
+	w.payOff = payOff
+	w.pktLim = w.dgramLim - aeadOverhead
+	// We hardcode the payload length field to be 2 bytes, which limits the payload
+	// (including the packet number) to 16383 bytes (the largest 2-byte QUIC varint).
+	//
+	// Most networks don't support datagrams over 1472 bytes, and even Ethernet
+	// jumbo frames are generally only about 9000 bytes.
+	if lim := pnumOff + 16383 - aeadOverhead; lim < w.pktLim {
+		w.pktLim = lim
+	}
+	w.b = w.b[:payOff]
+}
+
+// finishProtectedLongHeaderPacket finishes writing an Initial, 0-RTT, or Handshake packet,
+// canceling the packet if it contains no payload.
+// It returns a sentPacket describing the packet, or nil if no packet was written.
+func (w *packetWriter) finishProtectedLongHeaderPacket(pnumMaxAcked packetNumber, k keys, p longPacket) *sentPacket {
+	if len(w.b) == w.payOff {
+		// The payload is empty, so just abandon the packet.
+		w.b = w.b[:w.pktOff]
+		return nil
+	}
+	pnumLen := packetNumberLength(p.num, pnumMaxAcked)
+	plen := w.padPacketLength(pnumLen)
+	hdr := w.b[:w.pktOff]
+	var typeBits byte
+	switch p.ptype {
+	case packetTypeInitial:
+		typeBits = longPacketTypeInitial
+	case packetType0RTT:
+		typeBits = longPacketType0RTT
+	case packetTypeHandshake:
+		typeBits = longPacketTypeHandshake
+	case packetTypeRetry:
+		typeBits = longPacketTypeRetry
+	}
+	hdr = append(hdr, headerFormLong|fixedBit|typeBits|byte(pnumLen-1))
+	hdr = binary.BigEndian.AppendUint32(hdr, p.version)
+	hdr = appendUint8Bytes(hdr, p.dstConnID)
+	hdr = appendUint8Bytes(hdr, p.srcConnID)
+	switch p.ptype {
+	case packetTypeInitial:
+		hdr = appendVarintBytes(hdr, p.extra) // token
+	}
+
+	// Packet length, always encoded as a 2-byte varint.
+	hdr = append(hdr, 0x40|byte(plen>>8), byte(plen))
+
+	pnumOff := len(hdr)
+	hdr = appendPacketNumber(hdr, p.num, pnumMaxAcked)
+
+	return w.protect(hdr[w.pktOff:], p.num, pnumOff, k)
+}
+
+// start1RTTPacket starts writing a 1-RTT (short header) packet.
+func (w *packetWriter) start1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte) {
+	if w.sent == nil {
+		w.sent = newSentPacket()
+	}
+	w.pktOff = len(w.b)
+	hdrSize := 1 // packet type
+	hdrSize += len(dstConnID)
+	// Ensure we have enough space to hold the packet, including the header,
+	// header protection sample (RFC 9001, section 5.4.2), and encryption overhead.
+	if len(w.b)+hdrSize+4+headerProtectionSampleSize+aeadOverhead >= w.dgramLim {
+		w.payOff = len(w.b)
+		w.pktLim = len(w.b)
+		return
+	}
+	hdrSize += packetNumberLength(pnum, pnumMaxAcked)
+	w.payOff = len(w.b) + hdrSize
+	w.pktLim = w.dgramLim - aeadOverhead
+	w.b = w.b[:w.payOff]
+}
+
+// finish1RTTPacket finishes writing a 1-RTT packet,
+// canceling the packet if it contains no payload.
+// It returns a sentPacket describing the packet, or nil if no packet was written.
+func (w *packetWriter) finish1RTTPacket(pnum, pnumMaxAcked packetNumber, dstConnID []byte, k keys) *sentPacket {
+	if len(w.b) == w.payOff {
+		// The payload is empty, so just abandon the packet.
+		w.b = w.b[:w.pktOff]
+		return nil
+	}
+	// TODO: Spin
+	// TODO: Key phase
+	pnumLen := packetNumberLength(pnum, pnumMaxAcked)
+	hdr := w.b[:w.pktOff]
+	hdr = append(hdr, 0x40|byte(pnumLen-1))
+	hdr = append(hdr, dstConnID...)
+	pnumOff := len(hdr)
+	hdr = appendPacketNumber(hdr, pnum, pnumMaxAcked)
+	w.padPacketLength(pnumLen)
+	return w.protect(hdr[w.pktOff:], pnum, pnumOff, k)
+}
+
+// padPacketLength pads out the payload of the current packet to the minimum size,
+// and returns the combined length of the packet number and payload (used for the Length
+// field of long header packets).
+func (w *packetWriter) padPacketLength(pnumLen int) int {
+	plen := len(w.b) - w.payOff + pnumLen + aeadOverhead
+	// "To ensure that sufficient data is available for sampling, packets are
+	// padded so that the combined lengths of the encoded packet number and
+	// protected payload is at least 4 bytes longer than the sample required
+	// for header protection."
+	// https://www.rfc-editor.org/rfc/rfc9001.html#section-5.4.2
+	for plen < 4+headerProtectionSampleSize {
+		w.b = append(w.b, 0)
+		plen++
+	}
+	return plen
+}
+
+// protect applies packet protection and finishes the current packet.
+func (w *packetWriter) protect(hdr []byte, pnum packetNumber, pnumOff int, k keys) *sentPacket {
+	k.protect(hdr, w.b[w.pktOff+len(hdr):], pnumOff-w.pktOff, pnum)
+	w.b = w.b[:len(w.b)+aeadOverhead]
+	w.sent.size = len(w.b) - w.pktOff
+	w.sent.num = pnum
+	sent := w.sent
+	w.sent = nil
+	return sent
+}
+
+// avail reports how many more bytes may be written to the current packet.
+func (w *packetWriter) avail() int {
+	return w.pktLim - len(w.b)
+}
+
+// appendPaddingTo appends PADDING frames until the total datagram size
+// (including AEAD overhead of the current packet) is n.
+func (w *packetWriter) appendPaddingTo(n int) {
+	n -= aeadOverhead
+	lim := w.pktLim
+	if n < lim {
+		lim = n
+	}
+	if len(w.b) >= lim {
+		return
+	}
+	for len(w.b) < lim {
+		w.b = append(w.b, frameTypePadding)
+	}
+	// Packets are considered in flight when they contain a PADDING frame.
+	// https://www.rfc-editor.org/rfc/rfc9002.html#section-2-3.6.1
+	w.sent.inFlight = true
+}
+
+func (w *packetWriter) appendPingFrame() (added bool) {
+	if len(w.b) >= w.pktLim {
+		return false
+	}
+	w.b = append(w.b, frameTypePing)
+	w.sent.appendAckElicitingFrame(frameTypePing)
+	return true
+}
+
+// appendAckFrame appends an ACK frame to the payload.
+// It includes at least the most recent range in the rangeset
+// (the range with the largest packet numbers),
+// followed by as many additional ranges as fit within the packet.
+//
+// We always place ACK frames at the start of packets,
+// we limit the number of ack ranges retained, and
+// we set a minimum packet payload size.
+// As a result, appendAckFrame will rarely if ever drop ranges
+// in practice.
+//
+// In the event that ranges are dropped, the impact is limited
+// to the peer potentially failing to receive an acknowledgement
+// for an older packet during a period of high packet loss or
+// reordering. This may result in unnecessary retransmissions.
+func (w *packetWriter) appendAckFrame(seen rangeset, ackDelayExponent uint8, delay time.Duration) (added bool) {
+	if len(seen) == 0 {
+		return false
+	}
+	var (
+		largest    = uint64(seen.max())
+		mdelay     = uint64(delay.Microseconds() / (1 << ackDelayExponent))
+		firstRange = uint64(seen[len(seen)-1].size() - 1)
+	)
+	if w.avail() < 1+sizeVarint(largest)+sizeVarint(mdelay)+1+sizeVarint(firstRange) {
+		return false
+	}
+	w.b = append(w.b, frameTypeAck)
+	w.b = appendVarint(w.b, largest)
+	w.b = appendVarint(w.b, mdelay)
+	// The range count is technically a varint, but we'll reserve a single byte for it
+	// and never add more than 62 ranges (the maximum varint that fits in a byte).
+	rangeCountOff := len(w.b)
+	w.b = append(w.b, 0)
+	w.b = appendVarint(w.b, firstRange)
+	rangeCount := byte(0)
+	for i := len(seen) - 2; i >= 0; i-- {
+		gap := uint64(seen[i+1].start - seen[i].end - 1)
+		size := uint64(seen[i].size() - 1)
+		if w.avail() < sizeVarint(gap)+sizeVarint(size) || rangeCount > 62 {
+			break
+		}
+		w.b = appendVarint(w.b, gap)
+		w.b = appendVarint(w.b, size)
+		rangeCount++
+	}
+	w.b[rangeCountOff] = rangeCount
+	w.sent.appendNonAckElicitingFrame(frameTypeAck)
+	w.sent.appendInt(uint64(seen.max()))
+	return true
+}
+
+func (w *packetWriter) appendNewTokenFrame(token []byte) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(len(token)))+len(token) {
+		return false
+	}
+	w.b = append(w.b, frameTypeNewToken)
+	w.b = appendVarintBytes(w.b, token)
+	return true
+}
+
+func (w *packetWriter) appendResetStreamFrame(id streamID, code uint64, finalSize int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(id))+sizeVarint(code)+sizeVarint(uint64(finalSize)) {
+		return false
+	}
+	w.b = append(w.b, frameTypeResetStream)
+	w.b = appendVarint(w.b, uint64(id))
+	w.b = appendVarint(w.b, code)
+	w.b = appendVarint(w.b, uint64(finalSize))
+	w.sent.appendAckElicitingFrame(frameTypeResetStream)
+	w.sent.appendInt(uint64(id))
+	return true
+}
+
+func (w *packetWriter) appendStopSendingFrame(id streamID, code uint64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(id))+sizeVarint(code) {
+		return false
+	}
+	w.b = append(w.b, frameTypeStopSending)
+	w.b = appendVarint(w.b, uint64(id))
+	w.b = appendVarint(w.b, code)
+	w.sent.appendAckElicitingFrame(frameTypeStopSending)
+	w.sent.appendInt(uint64(id))
+	return true
+}
+
+// appendCryptoFrame appends a CRYPTO frame.
+// It returns a []byte into which the data should be written and whether a frame was added.
+// The returned []byte may be smaller than size if the packet cannot hold all the data.
+func (w *packetWriter) appendCryptoFrame(off int64, size int) (_ []byte, added bool) {
+	max := w.avail()
+	max -= 1                        // frame type
+	max -= sizeVarint(uint64(off))  // offset
+	max -= sizeVarint(uint64(size)) // maximum length
+	if max <= 0 {
+		return nil, false
+	}
+	if max < size {
+		size = max
+	}
+	w.b = append(w.b, frameTypeCrypto)
+	w.b = appendVarint(w.b, uint64(off))
+	w.b = appendVarint(w.b, uint64(size))
+	start := len(w.b)
+	w.b = w.b[:start+size]
+	w.sent.appendAckElicitingFrame(frameTypeCrypto)
+	w.sent.appendOffAndSize(off, size)
+	return w.b[start:][:size], true
+}
+
+// appendStreamFrame appends a STREAM frame.
+// It returns a []byte into which the data should be written and whether a frame was added.
+// The returned []byte may be smaller than size if the packet cannot hold all the data.
+func (w *packetWriter) appendStreamFrame(id streamID, off int64, size int, fin bool) (_ []byte, added bool) {
+	typ := uint8(frameTypeStreamBase | streamLenBit)
+	max := w.avail()
+	max -= 1 // frame type
+	max -= sizeVarint(uint64(id))
+	if off != 0 {
+		max -= sizeVarint(uint64(off))
+		typ |= streamOffBit
+	}
+	max -= sizeVarint(uint64(size)) // maximum length
+	if max < 0 || (max == 0 && size > 0) {
+		return nil, false
+	}
+	if max < size {
+		size = max
+	} else if fin {
+		typ |= streamFinBit
+	}
+	w.b = append(w.b, typ)
+	w.b = appendVarint(w.b, uint64(id))
+	if off != 0 {
+		w.b = appendVarint(w.b, uint64(off))
+	}
+	w.b = appendVarint(w.b, uint64(size))
+	start := len(w.b)
+	w.b = w.b[:start+size]
+	if fin {
+		w.sent.appendAckElicitingFrame(frameTypeStreamBase | streamFinBit)
+	} else {
+		w.sent.appendAckElicitingFrame(frameTypeStreamBase)
+	}
+	w.sent.appendInt(uint64(id))
+	w.sent.appendOffAndSize(off, size)
+	return w.b[start:][:size], true
+}
+
+func (w *packetWriter) appendMaxDataFrame(max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(max)) {
+		return false
+	}
+	w.b = append(w.b, frameTypeMaxData)
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(frameTypeMaxData)
+	return true
+}
+
+func (w *packetWriter) appendMaxStreamDataFrame(id streamID, max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(id))+sizeVarint(uint64(max)) {
+		return false
+	}
+	w.b = append(w.b, frameTypeMaxStreamData)
+	w.b = appendVarint(w.b, uint64(id))
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(frameTypeMaxStreamData)
+	w.sent.appendInt(uint64(id))
+	return true
+}
+
+func (w *packetWriter) appendMaxStreamsFrame(streamType streamType, max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(max)) {
+		return false
+	}
+	var typ byte
+	if streamType == bidiStream {
+		typ = frameTypeMaxStreamsBidi
+	} else {
+		typ = frameTypeMaxStreamsUni
+	}
+	w.b = append(w.b, typ)
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(typ)
+	return true
+}
+
+func (w *packetWriter) appendDataBlockedFrame(max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(max)) {
+		return false
+	}
+	w.b = append(w.b, frameTypeDataBlocked)
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(frameTypeDataBlocked)
+	return true
+}
+
+func (w *packetWriter) appendStreamDataBlockedFrame(id streamID, max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(id))+sizeVarint(uint64(max)) {
+		return false
+	}
+	w.b = append(w.b, frameTypeStreamDataBlocked)
+	w.b = appendVarint(w.b, uint64(id))
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(frameTypeStreamDataBlocked)
+	w.sent.appendInt(uint64(id))
+	return true
+}
+
+func (w *packetWriter) appendStreamsBlockedFrame(typ streamType, max int64) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(max)) {
+		return false
+	}
+	var ftype byte
+	if typ == bidiStream {
+		ftype = frameTypeStreamsBlockedBidi
+	} else {
+		ftype = frameTypeStreamsBlockedUni
+	}
+	w.b = append(w.b, ftype)
+	w.b = appendVarint(w.b, uint64(max))
+	w.sent.appendAckElicitingFrame(ftype)
+	return true
+}
+
+func (w *packetWriter) appendNewConnectionIDFrame(seq, retirePriorTo int64, connID []byte, token [16]byte) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(seq))+sizeVarint(uint64(retirePriorTo))+1+len(connID)+len(token) {
+		return false
+	}
+	w.b = append(w.b, frameTypeNewConnectionID)
+	w.b = appendVarint(w.b, uint64(seq))
+	w.b = appendVarint(w.b, uint64(retirePriorTo))
+	w.b = appendUint8Bytes(w.b, connID)
+	w.b = append(w.b, token[:]...)
+	w.sent.appendAckElicitingFrame(frameTypeNewConnectionID)
+	w.sent.appendInt(uint64(seq))
+	return true
+}
+
+func (w *packetWriter) appendRetireConnectionIDFrame(seq uint64) (added bool) {
+	if w.avail() < 1+sizeVarint(seq) {
+		return false
+	}
+	w.b = append(w.b, frameTypeRetireConnectionID)
+	w.b = appendVarint(w.b, seq)
+	w.sent.appendAckElicitingFrame(frameTypeRetireConnectionID)
+	return true
+}
+
+func (w *packetWriter) appendPathChallengeFrame(data uint64) (added bool) {
+	if w.avail() < 1+8 {
+		return false
+	}
+	w.b = append(w.b, frameTypePathChallenge)
+	w.b = binary.BigEndian.AppendUint64(w.b, data)
+	w.sent.appendAckElicitingFrame(frameTypePathChallenge)
+	return true
+}
+
+func (w *packetWriter) appendPathResponseFrame(data uint64) (added bool) {
+	if w.avail() < 1+8 {
+		return false
+	}
+	w.b = append(w.b, frameTypePathResponse)
+	w.b = binary.BigEndian.AppendUint64(w.b, data)
+	w.sent.appendAckElicitingFrame(frameTypePathResponse)
+	return true
+}
+
+// appendConnectionCloseTransportFrame appends a CONNECTION_CLOSE frame
+// carrying a transport error code.
+func (w *packetWriter) appendConnectionCloseTransportFrame(code transportError, frameType uint64, reason string) (added bool) {
+	if w.avail() < 1+sizeVarint(uint64(code))+sizeVarint(frameType)+sizeVarint(uint64(len(reason)))+len(reason) {
+		return false
+	}
+	w.b = append(w.b, frameTypeConnectionCloseTransport)
+	w.b = appendVarint(w.b, uint64(code))
+	w.b = appendVarint(w.b, frameType)
+	w.b = appendVarintBytes(w.b, []byte(reason))
+	// We don't record CONNECTION_CLOSE frames in w.sent, since they are never acked or
+	// detected as lost.
+	return true
+}
+
+// appendConnectionCloseTransportFrame appends a CONNECTION_CLOSE frame
+// carrying an application protocol error code.
+func (w *packetWriter) appendConnectionCloseApplicationFrame(code uint64, reason string) (added bool) {
+	if w.avail() < 1+sizeVarint(code)+sizeVarint(uint64(len(reason)))+len(reason) {
+		return false
+	}
+	w.b = append(w.b, frameTypeConnectionCloseApplication)
+	w.b = appendVarint(w.b, code)
+	w.b = appendVarintBytes(w.b, []byte(reason))
+	// We don't record CONNECTION_CLOSE frames in w.sent, since they are never acked or
+	// detected as lost.
+	return true
+}
+
+func (w *packetWriter) appendHandshakeDoneFrame() (added bool) {
+	if w.avail() < 1 {
+		return false
+	}
+	w.b = append(w.b, frameTypeHandshakeDone)
+	w.sent.appendAckElicitingFrame(frameTypeHandshakeDone)
+	return true
+}