mirror of
https://github.com/golang/net.git
synced 2026-04-01 02:47:08 +09:00
The x/net go.mod now depends on go1.23. Change-Id: I24960949d84753f84f75fbdc97b7dfb92191a4d2 Reviewed-on: https://go-review.googlesource.com/c/net/+/664295 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Auto-Submit: Damien Neil <dneil@google.com>
363 lines
8.6 KiB
Go
363 lines
8.6 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"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/quic/qlog"
|
|
)
|
|
|
|
func TestQLogHandshake(t *testing.T) {
|
|
testSides(t, "", func(t *testing.T, side connSide) {
|
|
qr := &qlogRecord{}
|
|
tc := newTestConn(t, side, qr.config)
|
|
tc.handshake()
|
|
tc.conn.Abort(nil)
|
|
tc.wantFrame("aborting connection generates CONN_CLOSE",
|
|
packetType1RTT, debugFrameConnectionCloseTransport{
|
|
code: errNo,
|
|
})
|
|
tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
|
|
tc.advanceToTimer() // let the conn finish draining
|
|
|
|
var src, dst []byte
|
|
if side == clientSide {
|
|
src = testLocalConnID(0)
|
|
dst = testLocalConnID(-1)
|
|
} else {
|
|
src = testPeerConnID(-1)
|
|
dst = testPeerConnID(0)
|
|
}
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "connectivity:connection_started",
|
|
"data": map[string]any{
|
|
"src_cid": hex.EncodeToString(src),
|
|
"dst_cid": hex.EncodeToString(dst),
|
|
},
|
|
}, jsonEvent{
|
|
"name": "connectivity:connection_closed",
|
|
"data": map[string]any{
|
|
"trigger": "clean",
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestQLogPacketFrames(t *testing.T) {
|
|
qr := &qlogRecord{}
|
|
tc := newTestConn(t, clientSide, qr.config)
|
|
tc.handshake()
|
|
tc.conn.Abort(nil)
|
|
tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{})
|
|
tc.advanceToTimer() // let the conn finish draining
|
|
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "transport:packet_sent",
|
|
"data": map[string]any{
|
|
"header": map[string]any{
|
|
"packet_type": "initial",
|
|
"packet_number": 0,
|
|
"dcid": hex.EncodeToString(testLocalConnID(-1)),
|
|
"scid": hex.EncodeToString(testLocalConnID(0)),
|
|
},
|
|
"frames": []any{
|
|
map[string]any{"frame_type": "crypto"},
|
|
},
|
|
},
|
|
}, jsonEvent{
|
|
"name": "transport:packet_received",
|
|
"data": map[string]any{
|
|
"header": map[string]any{
|
|
"packet_type": "initial",
|
|
"packet_number": 0,
|
|
"dcid": hex.EncodeToString(testLocalConnID(0)),
|
|
"scid": hex.EncodeToString(testPeerConnID(0)),
|
|
},
|
|
"frames": []any{map[string]any{"frame_type": "crypto"}},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestQLogConnectionClosedTrigger(t *testing.T) {
|
|
for _, test := range []struct {
|
|
trigger string
|
|
connOpts []any
|
|
f func(*testConn)
|
|
}{{
|
|
trigger: "clean",
|
|
f: func(tc *testConn) {
|
|
tc.handshake()
|
|
tc.conn.Abort(nil)
|
|
},
|
|
}, {
|
|
trigger: "handshake_timeout",
|
|
connOpts: []any{
|
|
func(c *Config) {
|
|
c.HandshakeTimeout = 5 * time.Second
|
|
},
|
|
},
|
|
f: func(tc *testConn) {
|
|
tc.ignoreFrame(frameTypeCrypto)
|
|
tc.ignoreFrame(frameTypeAck)
|
|
tc.ignoreFrame(frameTypePing)
|
|
tc.advance(5 * time.Second)
|
|
},
|
|
}, {
|
|
trigger: "idle_timeout",
|
|
connOpts: []any{
|
|
func(c *Config) {
|
|
c.MaxIdleTimeout = 5 * time.Second
|
|
},
|
|
},
|
|
f: func(tc *testConn) {
|
|
tc.handshake()
|
|
tc.advance(5 * time.Second)
|
|
},
|
|
}, {
|
|
trigger: "error",
|
|
f: func(tc *testConn) {
|
|
tc.handshake()
|
|
tc.writeFrames(packetType1RTT, debugFrameConnectionCloseTransport{
|
|
code: errProtocolViolation,
|
|
})
|
|
tc.conn.Abort(nil)
|
|
},
|
|
}} {
|
|
t.Run(test.trigger, func(t *testing.T) {
|
|
qr := &qlogRecord{}
|
|
tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...)
|
|
test.f(tc)
|
|
fr, ptype := tc.readFrame()
|
|
switch fr := fr.(type) {
|
|
case debugFrameConnectionCloseTransport:
|
|
tc.writeFrames(ptype, fr)
|
|
case nil:
|
|
default:
|
|
t.Fatalf("unexpected frame: %v", fr)
|
|
}
|
|
tc.wantIdle("connection should be idle while closing")
|
|
tc.advance(5 * time.Second) // long enough for the drain timer to expire
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "connectivity:connection_closed",
|
|
"data": map[string]any{
|
|
"trigger": test.trigger,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQLogRecovery(t *testing.T) {
|
|
qr := &qlogRecord{}
|
|
tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
|
|
permissiveTransportParameters, qr.config)
|
|
|
|
// Ignore events from the handshake.
|
|
qr.ev = nil
|
|
|
|
data := make([]byte, 16)
|
|
s.Write(data)
|
|
s.CloseWrite()
|
|
tc.wantFrame("created stream 0",
|
|
packetType1RTT, debugFrameStream{
|
|
id: newStreamID(clientSide, uniStream, 0),
|
|
fin: true,
|
|
data: data,
|
|
})
|
|
tc.writeAckForAll()
|
|
tc.wantIdle("connection should be idle now")
|
|
|
|
// Don't check the contents of fields, but verify that recovery metrics are logged.
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "recovery:metrics_updated",
|
|
"data": map[string]any{
|
|
"bytes_in_flight": nil,
|
|
},
|
|
}, jsonEvent{
|
|
"name": "recovery:metrics_updated",
|
|
"data": map[string]any{
|
|
"bytes_in_flight": 0,
|
|
"congestion_window": nil,
|
|
"latest_rtt": nil,
|
|
"min_rtt": nil,
|
|
"rtt_variance": nil,
|
|
"smoothed_rtt": nil,
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestQLogLoss(t *testing.T) {
|
|
qr := &qlogRecord{}
|
|
tc, s := newTestConnAndLocalStream(t, clientSide, uniStream,
|
|
permissiveTransportParameters, qr.config)
|
|
|
|
// Ignore events from the handshake.
|
|
qr.ev = nil
|
|
|
|
data := make([]byte, 16)
|
|
s.Write(data)
|
|
s.CloseWrite()
|
|
tc.wantFrame("created stream 0",
|
|
packetType1RTT, debugFrameStream{
|
|
id: newStreamID(clientSide, uniStream, 0),
|
|
fin: true,
|
|
data: data,
|
|
})
|
|
|
|
const pto = false
|
|
tc.triggerLossOrPTO(packetType1RTT, pto)
|
|
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "recovery:packet_lost",
|
|
"data": map[string]any{
|
|
"header": map[string]any{
|
|
"packet_number": nil,
|
|
"packet_type": "1RTT",
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestQLogPacketDropped(t *testing.T) {
|
|
qr := &qlogRecord{}
|
|
tc := newTestConn(t, clientSide, permissiveTransportParameters, qr.config)
|
|
tc.handshake()
|
|
|
|
// A garbage-filled datagram with a DCID matching this connection.
|
|
dgram := bytes.Join([][]byte{
|
|
{headerFormShort | fixedBit},
|
|
testLocalConnID(0),
|
|
make([]byte, 100),
|
|
[]byte{1, 2, 3, 4}, // random data, to avoid this looking like a stateless reset
|
|
}, nil)
|
|
tc.endpoint.write(&datagram{
|
|
b: dgram,
|
|
})
|
|
|
|
qr.wantEvents(t, jsonEvent{
|
|
"name": "connectivity:packet_dropped",
|
|
})
|
|
}
|
|
|
|
type nopCloseWriter struct {
|
|
io.Writer
|
|
}
|
|
|
|
func (nopCloseWriter) Close() error { return nil }
|
|
|
|
type jsonEvent map[string]any
|
|
|
|
func (j jsonEvent) String() string {
|
|
b, _ := json.MarshalIndent(j, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
// jsonPartialEqual compares two JSON structures.
|
|
// It ignores fields not set in want (see below for specifics).
|
|
func jsonPartialEqual(got, want any) (equal bool) {
|
|
cmpval := func(v any) any {
|
|
// Map certain types to a common representation.
|
|
switch v := v.(type) {
|
|
case int:
|
|
// JSON uses float64s rather than ints for numbers.
|
|
// Map int->float64 so we can use integers in expectations.
|
|
return float64(v)
|
|
case jsonEvent:
|
|
return (map[string]any)(v)
|
|
case []jsonEvent:
|
|
s := []any{}
|
|
for _, e := range v {
|
|
s = append(s, e)
|
|
}
|
|
return s
|
|
}
|
|
return v
|
|
}
|
|
if want == nil {
|
|
return true // match anything
|
|
}
|
|
got = cmpval(got)
|
|
want = cmpval(want)
|
|
if reflect.TypeOf(got) != reflect.TypeOf(want) {
|
|
return false
|
|
}
|
|
switch w := want.(type) {
|
|
case map[string]any:
|
|
// JSON object: Every field in want must match a field in got.
|
|
g := got.(map[string]any)
|
|
for k := range w {
|
|
if !jsonPartialEqual(g[k], w[k]) {
|
|
return false
|
|
}
|
|
}
|
|
case []any:
|
|
// JSON slice: Every field in want must match a field in got, in order.
|
|
// So want=[2,4] matches got=[1,2,3,4] but not [4,2].
|
|
g := got.([]any)
|
|
for _, ge := range g {
|
|
if jsonPartialEqual(ge, w[0]) {
|
|
w = w[1:]
|
|
if len(w) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
default:
|
|
if !reflect.DeepEqual(got, want) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// A qlogRecord records events.
|
|
type qlogRecord struct {
|
|
ev []jsonEvent
|
|
}
|
|
|
|
func (q *qlogRecord) Write(b []byte) (int, error) {
|
|
// This relies on the property that the Handler always makes one Write call per event.
|
|
if len(b) < 1 || b[0] != 0x1e {
|
|
panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b)))
|
|
}
|
|
var val map[string]any
|
|
if err := json.Unmarshal(b[1:], &val); err != nil {
|
|
panic(fmt.Errorf("log unmarshal failure: %v\n%v", err, string(b)))
|
|
}
|
|
q.ev = append(q.ev, val)
|
|
return len(b), nil
|
|
}
|
|
|
|
func (q *qlogRecord) Close() error { return nil }
|
|
|
|
// config may be passed to newTestConn to configure the conn to use this logger.
|
|
func (q *qlogRecord) config(c *Config) {
|
|
c.QLogLogger = slog.New(qlog.NewJSONHandler(qlog.HandlerOptions{
|
|
Level: QLogLevelFrame,
|
|
NewTrace: func(info qlog.TraceInfo) (io.WriteCloser, error) {
|
|
return q, nil
|
|
},
|
|
}))
|
|
}
|
|
|
|
// wantEvents checks that every event in want occurs in the order specified.
|
|
func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) {
|
|
t.Helper()
|
|
got := q.ev
|
|
if !jsonPartialEqual(got, want) {
|
|
t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want)
|
|
}
|
|
}
|