mirror of
https://github.com/golang/net.git
synced 2026-03-31 18:37:08 +09:00
Now that the x/net module requires Go 1.25.0, the go1.25 build constraint is always satisfied. Simplify the code accordingly. Change-Id: I3d6fe4a132a26918455489b998730b494f5273c4 Reviewed-on: https://go-review.googlesource.com/c/net/+/744800 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Nicholas Husin <nsh@golang.org> Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
852 lines
24 KiB
Go
852 lines
24 KiB
Go
// 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"
|
|
"io"
|
|
"log/slog"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/internal/quic/quicwire"
|
|
"golang.org/x/net/quic/qlog"
|
|
)
|
|
|
|
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, clientSide).r
|
|
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{}, clientSide).w
|
|
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) {
|
|
var aes128Keys, aes256Keys, chachaKeys fixedKeys
|
|
aes128Keys.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret"))
|
|
aes256Keys.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret"))
|
|
chachaKeys.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret"))
|
|
for _, test := range []struct {
|
|
desc string
|
|
p longPacket
|
|
k fixedKeys
|
|
}{{
|
|
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) {
|
|
var aes128Keys, aes256Keys, chachaKeys updatingKeyPair
|
|
aes128Keys.r.init(tls.TLS_AES_128_GCM_SHA256, []byte("secret"))
|
|
aes256Keys.r.init(tls.TLS_AES_256_GCM_SHA384, []byte("secret"))
|
|
chachaKeys.r.init(tls.TLS_CHACHA20_POLY1305_SHA256, []byte("secret"))
|
|
aes128Keys.w = aes128Keys.r
|
|
aes256Keys.w = aes256Keys.r
|
|
chachaKeys.w = chachaKeys.r
|
|
aes128Keys.updateAfter = maxPacketNumber
|
|
aes256Keys.updateAfter = maxPacketNumber
|
|
chachaKeys.updateAfter = maxPacketNumber
|
|
connID := make([]byte, connIDLen)
|
|
for i := range connID {
|
|
connID[i] = byte(i)
|
|
}
|
|
for _, test := range []struct {
|
|
desc string
|
|
num packetNumber
|
|
payload []byte
|
|
k updatingKeyPair
|
|
}{{
|
|
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, err := parse1RTTPacket(pkt, &test.k, connIDLen, 0)
|
|
if err != nil {
|
|
t.Errorf("parse1RTTPacket: err=%v, want nil", err)
|
|
}
|
|
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
|
|
j string
|
|
f debugFrame
|
|
b []byte
|
|
truncated []byte
|
|
}{{
|
|
s: "PADDING*1",
|
|
j: `{"frame_type":"padding","length":1}`,
|
|
f: debugFramePadding{
|
|
size: 1,
|
|
},
|
|
b: []byte{
|
|
0x00, // Type (i) = 0x00,
|
|
|
|
},
|
|
}, {
|
|
s: "PING",
|
|
j: `{"frame_type":"ping"}`,
|
|
f: debugFramePing{},
|
|
b: []byte{
|
|
0x01, // TYPE(i) = 0x01
|
|
},
|
|
}, {
|
|
s: "ACK Delay=10 [0,16) [17,32) [48,64)",
|
|
j: `"error: debugFrameAck should not appear as a slog Value"`,
|
|
f: debugFrameAck{
|
|
ackDelay: 10,
|
|
ranges: []i64range[packetNumber]{
|
|
{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: "ACK Delay=10 [0,16) [17,32) ECN=[1,2,3]",
|
|
j: `"error: debugFrameAck should not appear as a slog Value"`,
|
|
f: debugFrameAck{
|
|
ackDelay: 10,
|
|
ranges: []i64range[packetNumber]{
|
|
{0x00, 0x10},
|
|
{0x11, 0x20},
|
|
},
|
|
ecn: ecnCounts{1, 2, 3},
|
|
},
|
|
b: []byte{
|
|
0x03, // TYPE (i) = 0x3
|
|
0x1f, // Largest Acknowledged (i)
|
|
10, // ACK Delay (i)
|
|
0x01, // ACK Range Count (i)
|
|
0x0e, // First ACK Range (i)
|
|
0x00, // Gap (i)
|
|
0x0f, // ACK Range Length (i)
|
|
0x01, // ECT0 Count (i)
|
|
0x02, // ECT1 Count (i)
|
|
0x03, // ECN-CE Count (i)
|
|
},
|
|
truncated: []byte{
|
|
0x03, // TYPE (i) = 0x3
|
|
0x1f, // Largest Acknowledged (i)
|
|
10, // ACK Delay (i)
|
|
0x00, // ACK Range Count (i)
|
|
0x0e, // First ACK Range (i)
|
|
0x01, // ECT0 Count (i)
|
|
0x02, // ECT1 Count (i)
|
|
0x03, // ECN-CE Count (i)
|
|
},
|
|
}, {
|
|
s: "ACK Delay=10 [17,32) ECN=[1,2,3]",
|
|
j: `"error: debugFrameAck should not appear as a slog Value"`,
|
|
f: debugFrameAck{
|
|
ackDelay: 10,
|
|
ranges: []i64range[packetNumber]{
|
|
{0x11, 0x20},
|
|
},
|
|
ecn: ecnCounts{1, 2, 3},
|
|
},
|
|
b: []byte{
|
|
0x03, // TYPE (i) = 0x3
|
|
0x1f, // Largest Acknowledged (i)
|
|
10, // ACK Delay (i)
|
|
0x00, // ACK Range Count (i)
|
|
0x0e, // First ACK Range (i)
|
|
0x01, // ECT0 Count (i)
|
|
0x02, // ECT1 Count (i)
|
|
0x03, // ECN-CE Count (i)
|
|
},
|
|
// Downgrading to a type 0x2 ACK frame is not allowed: "Even if an
|
|
// endpoint does not set an ECT field in packets it sends, the endpoint
|
|
// MUST provide feedback about ECN markings it receives, if these are
|
|
// accessible."
|
|
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.4.1-2
|
|
truncated: nil,
|
|
}, {
|
|
s: "RESET_STREAM ID=1 Code=2 FinalSize=3",
|
|
j: `{"frame_type":"reset_stream","stream_id":1,"final_size":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",
|
|
j: `{"frame_type":"stop_sending","stream_id":1,"error_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",
|
|
j: `{"frame_type":"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",
|
|
j: `{"frame_type":"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",
|
|
j: `{"frame_type":"stream","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",
|
|
j: `{"frame_type":"stream","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",
|
|
j: `{"frame_type":"stream","stream_id":100,"offset":4,"length":3,"fin":true}`,
|
|
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",
|
|
j: `{"frame_type":"stream","stream_id":1,"offset":100,"length":0,"fin":true}`,
|
|
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",
|
|
j: `{"frame_type":"max_data","maximum":10}`,
|
|
f: debugFrameMaxData{
|
|
max: 10,
|
|
},
|
|
b: []byte{
|
|
0x10, // Type (i) = 0x10,
|
|
0x0a, // Maximum Data (i),
|
|
},
|
|
}, {
|
|
s: "MAX_STREAM_DATA ID=1 Max=10",
|
|
j: `{"frame_type":"max_stream_data","stream_id":1,"maximum":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",
|
|
j: `{"frame_type":"max_streams","stream_type":"bidirectional","maximum":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",
|
|
j: `{"frame_type":"max_streams","stream_type":"unidirectional","maximum":1}`,
|
|
f: debugFrameMaxStreams{
|
|
streamType: uniStream,
|
|
max: 1,
|
|
},
|
|
b: []byte{
|
|
0x13, // Type (i) = 0x12..0x13,
|
|
0x01, // Maximum Streams (i),
|
|
},
|
|
}, {
|
|
s: "DATA_BLOCKED Max=1",
|
|
j: `{"frame_type":"data_blocked","limit":1}`,
|
|
f: debugFrameDataBlocked{
|
|
max: 1,
|
|
},
|
|
b: []byte{
|
|
0x14, // Type (i) = 0x14,
|
|
0x01, // Maximum Data (i),
|
|
},
|
|
}, {
|
|
s: "STREAM_DATA_BLOCKED ID=1 Max=2",
|
|
j: `{"frame_type":"stream_data_blocked","stream_id":1,"limit":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",
|
|
j: `{"frame_type":"streams_blocked","stream_type":"bidirectional","limit":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",
|
|
j: `{"frame_type":"streams_blocked","stream_type":"unidirectional","limit":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",
|
|
j: `{"frame_type":"new_connection_id","sequence_number":3,"retire_prior_to":2,"connection_id":"a0a1a2a3","stateless_reset_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",
|
|
j: `{"frame_type":"retire_connection_id","sequence_number":1}`,
|
|
f: debugFrameRetireConnectionID{
|
|
seq: 1,
|
|
},
|
|
b: []byte{
|
|
0x19, // Type (i) = 0x19,
|
|
0x01, // Sequence Number (i),
|
|
},
|
|
}, {
|
|
s: "PATH_CHALLENGE Data=0123456789abcdef",
|
|
j: `{"frame_type":"path_challenge","data":"0123456789abcdef"}`,
|
|
f: debugFramePathChallenge{
|
|
data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
|
|
},
|
|
b: []byte{
|
|
0x1a, // Type (i) = 0x1a,
|
|
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, // Data (64),
|
|
},
|
|
}, {
|
|
s: "PATH_RESPONSE Data=0123456789abcdef",
|
|
j: `{"frame_type":"path_response","data":"0123456789abcdef"}`,
|
|
f: debugFramePathResponse{
|
|
data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
|
|
},
|
|
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"`,
|
|
j: `{"frame_type":"connection_close","error_space":"transport","error_code_value":1,"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"`,
|
|
j: `{"frame_type":"connection_close","error_space":"application","error_code_value":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",
|
|
j: `{"frame_type":"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)
|
|
}
|
|
if got, want := frameJSON(test.f), test.j; got != want {
|
|
t.Errorf("frame.LogValue():\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 TestFrameScaledAck(t *testing.T) {
|
|
for _, test := range []struct {
|
|
j string
|
|
f debugFrameScaledAck
|
|
}{{
|
|
j: `{"frame_type":"ack","acked_ranges":[[0,15],[17],[48,63]],"ack_delay":10.000000}`,
|
|
f: debugFrameScaledAck{
|
|
ackDelay: 10 * time.Millisecond,
|
|
ranges: []i64range[packetNumber]{
|
|
{0x00, 0x10},
|
|
{0x11, 0x12},
|
|
{0x30, 0x40},
|
|
},
|
|
},
|
|
}} {
|
|
if got, want := frameJSON(test.f), test.j; got != want {
|
|
t.Errorf("frame.LogValue():\ngot %q\nwant %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func frameJSON(f slog.LogValuer) string {
|
|
var buf bytes.Buffer
|
|
h := qlog.NewJSONHandler(qlog.HandlerOptions{
|
|
Level: QLogLevelFrame,
|
|
NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) {
|
|
return nopCloseWriter{&buf}, nil
|
|
},
|
|
})
|
|
// Log the frame, and then trim out everything but the frame from the log.
|
|
slog.New(h).Info("message", slog.Any("frame", f))
|
|
_, b, _ := bytes.Cut(buf.Bytes(), []byte(`"frame":`))
|
|
b = bytes.TrimSuffix(b, []byte("}}\n"))
|
|
return string(b)
|
|
}
|
|
|
|
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,
|
|
ranges: []i64range[packetNumber]{
|
|
{0, 1},
|
|
},
|
|
ecn: ecnCounts{1, 2, 3},
|
|
},
|
|
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 quicwire.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, clientSide).r
|
|
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)
|
|
})
|
|
}
|