quic: support Retry
Add a RequireAddressValidation configuration setting to enable
sending Retry packets on the server.
Support receiving Retry packets on the client.
RFC 9000, Section 8.1.2.
For golang/go#58547
Change-Id: Ia78b9594a03ce1b1143b95cb3c1ef4c38b2b39ef
Reviewed-on: https://go-review.googlesource.com/c/net/+/535237
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/quic/config.go b/internal/quic/config.go
index b390d69..99ef68f 100644
--- a/internal/quic/config.go
+++ b/internal/quic/config.go
@@ -47,6 +47,14 @@
// If zero, the default value of 1MiB is used.
// If negative, the limit is zero.
MaxConnReadBufferSize int64
+
+ // RequireAddressValidation may be set to true to enable address validation
+ // of client connections prior to starting the handshake.
+ //
+ // Enabling this setting reduces the amount of work packets with spoofed
+ // source address information can cause a server to perform,
+ // at the cost of increased handshake latency.
+ RequireAddressValidation bool
}
func configDefault(v, def, limit int64) int64 {
diff --git a/internal/quic/conn.go b/internal/quic/conn.go
index ea03bbf..4acf5dd 100644
--- a/internal/quic/conn.go
+++ b/internal/quic/conn.go
@@ -48,6 +48,9 @@
crypto [numberSpaceCount]cryptoStream
tls *tls.QUICConn
+ // retryToken is the token provided by the peer in a Retry packet.
+ retryToken []byte
+
// handshakeConfirmed is set when the handshake is confirmed.
// For server connections, it tracks sending HANDSHAKE_DONE.
handshakeConfirmed sentVal
@@ -83,7 +86,7 @@
timeNow() time.Time
}
-func newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) {
+func newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort, config *Config, l *Listener) (*Conn, error) {
c := &Conn{
side: side,
listener: l,
@@ -104,17 +107,21 @@
l.testHooks.newConn(c)
}
- var originalDstConnID []byte
+ // initialConnID is the connection ID used to generate Initial packet protection keys.
+ var initialConnID []byte
if c.side == clientSide {
if err := c.connIDState.initClient(c); err != nil {
return nil, err
}
initialConnID, _ = c.connIDState.dstConnID()
} else {
+ initialConnID = originalDstConnID
+ if retrySrcConnID != nil {
+ initialConnID = retrySrcConnID
+ }
if err := c.connIDState.initServer(c, initialConnID); err != nil {
return nil, err
}
- originalDstConnID = initialConnID
}
// The smallest allowed maximum QUIC datagram size is 1200 bytes.
@@ -125,10 +132,10 @@
c.streamsInit()
c.lifetimeInit()
- // TODO: retry_source_connection_id
if err := c.startTLS(now, initialConnID, transportParameters{
initialSrcConnID: c.connIDState.srcConnID(),
originalDstConnID: originalDstConnID,
+ retrySrcConnID: retrySrcConnID,
ackDelayExponent: ackDelayExponent,
maxUDPPayloadSize: maxUDPPayloadSize,
maxAckDelay: maxAckDelay,
@@ -195,7 +202,8 @@
// receiveTransportParameters applies transport parameters sent by the peer.
func (c *Conn) receiveTransportParameters(p transportParameters) error {
- if err := c.connIDState.validateTransportParameters(c.side, p); err != nil {
+ isRetry := c.retryToken != nil
+ if err := c.connIDState.validateTransportParameters(c.side, isRetry, p); err != nil {
return err
}
c.streams.outflow.setMaxData(p.initialMaxData)
@@ -220,9 +228,11 @@
return err
}
}
-
- // TODO: Many more transport parameters to come.
-
+ // TODO: max_idle_timeout
+ // TODO: stateless_reset_token
+ // TODO: max_udp_payload_size
+ // TODO: disable_active_migration
+ // TODO: preferred_address
return nil
}
diff --git a/internal/quic/conn_id.go b/internal/quic/conn_id.go
index 045e646..ff7e2d1 100644
--- a/internal/quic/conn_id.go
+++ b/internal/quic/conn_id.go
@@ -28,6 +28,9 @@
retireRemotePriorTo int64 // largest Retire Prior To value sent by the peer
peerActiveConnIDLimit int64 // peer's active_connection_id_limit transport parameter
+ originalDstConnID []byte // expected original_destination_connection_id param
+ retrySrcConnID []byte // expected retry_source_connection_id param
+
needSend bool
}
@@ -78,6 +81,7 @@
seq: -1,
cid: remid,
})
+ s.originalDstConnID = remid
const retired = false
c.listener.connIDsChanged(c, retired, s.local[:])
return nil
@@ -163,27 +167,21 @@
// validateTransportParameters verifies the original_destination_connection_id and
// initial_source_connection_id transport parameters match the expected values.
-func (s *connIDState) validateTransportParameters(side connSide, p transportParameters) error {
+func (s *connIDState) validateTransportParameters(side connSide, isRetry bool, p transportParameters) error {
// TODO: Consider returning more detailed errors, for debugging.
- switch side {
- case clientSide:
- // Verify original_destination_connection_id matches
- // the transient remote connection ID we chose.
- if len(s.remote) == 0 || s.remote[0].seq != -1 {
- return localTransportError(errInternal)
- }
- if !bytes.Equal(s.remote[0].cid, p.originalDstConnID) {
- return localTransportError(errTransportParameter)
- }
- // Remove the transient remote connection ID.
- // We have no further need for it.
- s.remote = append(s.remote[:0], s.remote[1:]...)
- case serverSide:
- if p.originalDstConnID != nil {
- // Clients do not send original_destination_connection_id.
- return localTransportError(errTransportParameter)
- }
+ // Verify original_destination_connection_id matches
+ // the transient remote connection ID we chose (client)
+ // or is empty (server).
+ if !bytes.Equal(s.originalDstConnID, p.originalDstConnID) {
+ return localTransportError(errTransportParameter)
}
+ s.originalDstConnID = nil // we have no further need for this
+ // Verify retry_source_connection_id matches the value from
+ // the server's Retry packet (when one was sent), or is empty.
+ if !bytes.Equal(p.retrySrcConnID, s.retrySrcConnID) {
+ return localTransportError(errTransportParameter)
+ }
+ s.retrySrcConnID = nil // we have no further need for this
// Verify initial_source_connection_id matches the first remote connection ID.
if len(s.remote) == 0 || s.remote[0].seq != 0 {
return localTransportError(errInternal)
@@ -203,13 +201,10 @@
// We're a client connection processing the first Initial packet
// from the server. Replace the transient remote connection ID
// with the Source Connection ID from the packet.
- // Leave the transient ID the list for now, since we'll need it when
- // processing the transport parameters.
- s.remote[0].retired = true
- s.remote = append(s.remote, connID{
+ s.remote[0] = connID{
seq: 0,
cid: cloneBytes(srcConnID),
- })
+ }
}
case ptype == packetTypeInitial && c.side == serverSide:
if len(s.remote) == 0 {
@@ -232,6 +227,14 @@
}
}
+func (s *connIDState) handleRetryPacket(srcConnID []byte) {
+ if len(s.remote) != 1 || s.remote[0].seq != -1 {
+ panic("BUG: handling retry with non-transient remote conn id")
+ }
+ s.retrySrcConnID = cloneBytes(srcConnID)
+ s.remote[0].cid = s.retrySrcConnID
+}
+
func (s *connIDState) handleNewConnID(seq, retire int64, cid []byte, resetToken [16]byte) error {
if len(s.remote[0].cid) == 0 {
// "An endpoint that is sending packets with a zero-length
diff --git a/internal/quic/conn_id_test.go b/internal/quic/conn_id_test.go
index 44755ec..784c5e2 100644
--- a/internal/quic/conn_id_test.go
+++ b/internal/quic/conn_id_test.go
@@ -48,9 +48,6 @@
t.Errorf("local ids: %v, want %v", fmtConnIDList(got), fmtConnIDList(wantLocal))
}
wantRemote := []connID{{
- cid: testLocalConnID(-1),
- seq: -1,
- }, {
cid: testPeerConnID(0),
seq: 0,
}}
diff --git a/internal/quic/conn_recv.go b/internal/quic/conn_recv.go
index 9b1ba1a..e789ae0 100644
--- a/internal/quic/conn_recv.go
+++ b/internal/quic/conn_recv.go
@@ -34,6 +34,9 @@
n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf)
case packetType1RTT:
n = c.handle1RTT(now, buf)
+ case packetTypeRetry:
+ c.handleRetry(now, buf)
+ return
case packetTypeVersionNegotiation:
c.handleVersionNegotiation(now, buf)
return
@@ -128,6 +131,42 @@
return len(buf)
}
+func (c *Conn) handleRetry(now time.Time, pkt []byte) {
+ if c.side != clientSide {
+ return // clients don't send Retry packets
+ }
+ // "After the client has received and processed an Initial or Retry packet
+ // from the server, it MUST discard any subsequent Retry packets that it receives."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
+ if !c.keysInitial.canRead() {
+ return // discarded Initial keys, connection is already established
+ }
+ if c.acks[initialSpace].seen.numRanges() != 0 {
+ return // processed at least one packet
+ }
+ if c.retryToken != nil {
+ return // received a Retry already
+ }
+ // "Clients MUST discard Retry packets that have a Retry Integrity Tag
+ // that cannot be validated."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2
+ p, ok := parseRetryPacket(pkt, c.connIDState.originalDstConnID)
+ if !ok {
+ return
+ }
+ // "A client MUST discard a Retry packet with a zero-length Retry Token field."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2
+ if len(p.token) == 0 {
+ return
+ }
+ c.retryToken = cloneBytes(p.token)
+ c.connIDState.handleRetryPacket(p.srcConnID)
+ // We need to resend any data we've already sent in Initial packets.
+ // We must not reuse already sent packet numbers.
+ c.loss.discardPackets(initialSpace, c.handleAckOrLoss)
+ // TODO: Discard 0-RTT packets as well, once we support 0-RTT.
+}
+
var errVersionNegotiation = errors.New("server does not support QUIC version 1")
func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) {
diff --git a/internal/quic/conn_send.go b/internal/quic/conn_send.go
index 00b02c2..efeb04f 100644
--- a/internal/quic/conn_send.go
+++ b/internal/quic/conn_send.go
@@ -68,6 +68,7 @@
num: pnum,
dstConnID: dstConnID,
srcConnID: c.connIDState.srcConnID(),
+ extra: c.retryToken,
}
c.w.startProtectedLongHeaderPacket(pnumMaxAcked, p)
c.appendFrames(now, initialSpace, pnum, limit)
diff --git a/internal/quic/conn_test.go b/internal/quic/conn_test.go
index ea47b0b..cfb0d06 100644
--- a/internal/quic/conn_test.go
+++ b/internal/quic/conn_test.go
@@ -218,6 +218,7 @@
listener.now,
side,
initialConnID,
+ nil,
netip.MustParseAddrPort("127.0.0.1:443"))
if err != nil {
t.Fatal(err)
@@ -545,7 +546,13 @@
if d == nil {
return nil
}
- tc.sentPackets = d.packets
+ for _, p := range d.packets {
+ if len(p.frames) == 0 {
+ tc.lastPacket = p
+ continue
+ }
+ tc.sentPackets = append(tc.sentPackets, p)
+ }
}
p := tc.sentPackets[0]
tc.sentPackets = tc.sentPackets[1:]
@@ -638,6 +645,12 @@
w.reset(1200)
var pnumMaxAcked packetNumber
switch p.ptype {
+ case packetTypeRetry:
+ return encodeRetryPacket(p.originalDstConnID, retryPacket{
+ srcConnID: p.srcConnID,
+ dstConnID: p.dstConnID,
+ token: p.token,
+ })
case packetType1RTT:
w.start1RTTPacket(p.num, pnumMaxAcked, p.dstConnID)
default:
@@ -717,6 +730,19 @@
}
ptype := getPacketType(buf)
switch ptype {
+ case packetTypeRetry:
+ retry, ok := parseRetryPacket(buf, tl.lastInitialDstConnID)
+ if !ok {
+ t.Fatalf("could not parse %v packet", ptype)
+ }
+ return &testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ dstConnID: retry.dstConnID,
+ srcConnID: retry.srcConnID,
+ token: retry.token,
+ }},
+ }
case packetTypeInitial, packetTypeHandshake:
var k fixedKeys
if tc == nil {
diff --git a/internal/quic/listener.go b/internal/quic/listener.go
index 9f14b09..aa25839 100644
--- a/internal/quic/listener.go
+++ b/internal/quic/listener.go
@@ -24,6 +24,7 @@
config *Config
udpConn udpConn
testHooks listenerTestHooks
+ retry retryState
acceptQueue queue[*Conn] // new inbound connections
@@ -74,10 +75,10 @@
if err != nil {
return nil, err
}
- return newListener(udpConn, config, nil), nil
+ return newListener(udpConn, config, nil)
}
-func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) *Listener {
+func newListener(udpConn udpConn, config *Config, hooks listenerTestHooks) (*Listener, error) {
l := &Listener{
config: config,
udpConn: udpConn,
@@ -86,8 +87,13 @@
acceptQueue: newQueue[*Conn](),
closec: make(chan struct{}),
}
+ if config.RequireAddressValidation {
+ if err := l.retry.init(); err != nil {
+ return nil, err
+ }
+ }
go l.listen()
- return l
+ return l, nil
}
// LocalAddr returns the local network address.
@@ -142,7 +148,7 @@
}
addr := u.AddrPort()
addr = netip.AddrPortFrom(addr.Addr().Unmap(), addr.Port())
- c, err := l.newConn(time.Now(), clientSide, nil, addr)
+ c, err := l.newConn(time.Now(), clientSide, nil, nil, addr)
if err != nil {
return nil, err
}
@@ -153,13 +159,13 @@
return c, nil
}
-func (l *Listener) newConn(now time.Time, side connSide, initialConnID []byte, peerAddr netip.AddrPort) (*Conn, error) {
+func (l *Listener) newConn(now time.Time, side connSide, originalDstConnID, retrySrcConnID []byte, peerAddr netip.AddrPort) (*Conn, error) {
l.connsMu.Lock()
defer l.connsMu.Unlock()
if l.closing {
return nil, errors.New("listener closed")
}
- c, err := newConn(now, side, initialConnID, peerAddr, l.config, l)
+ c, err := newConn(now, side, originalDstConnID, retrySrcConnID, peerAddr, l.config, l)
if err != nil {
return nil, err
}
@@ -300,8 +306,19 @@
} else {
now = time.Now()
}
+ var originalDstConnID, retrySrcConnID []byte
+ if l.config.RequireAddressValidation {
+ var ok bool
+ retrySrcConnID = p.dstConnID
+ originalDstConnID, ok = l.validateInitialAddress(now, p, m.addr)
+ if !ok {
+ return
+ }
+ } else {
+ originalDstConnID = p.dstConnID
+ }
var err error
- c, err := l.newConn(now, serverSide, p.dstConnID, m.addr)
+ c, err := l.newConn(now, serverSide, originalDstConnID, retrySrcConnID, m.addr)
if err != nil {
// The accept queue is probably full.
// We could send a CONNECTION_CLOSE to the peer to reject the connection.
@@ -320,6 +337,28 @@
m.recycle()
}
+func (l *Listener) sendConnectionClose(in genericLongPacket, addr netip.AddrPort, code transportError) {
+ keys := initialKeys(in.dstConnID, serverSide)
+ var w packetWriter
+ p := longPacket{
+ ptype: packetTypeInitial,
+ version: quicVersion1,
+ num: 0,
+ dstConnID: in.srcConnID,
+ srcConnID: in.dstConnID,
+ }
+ const pnumMaxAcked = 0
+ w.reset(minimumClientInitialDatagramSize)
+ w.startProtectedLongHeaderPacket(pnumMaxAcked, p)
+ w.appendConnectionCloseTransportFrame(code, 0, "")
+ w.finishProtectedLongHeaderPacket(pnumMaxAcked, keys.w, p)
+ buf := w.datagram()
+ if len(buf) == 0 {
+ return
+ }
+ l.sendDatagram(buf, addr)
+}
+
func (l *Listener) sendDatagram(p []byte, addr netip.AddrPort) error {
_, err := l.udpConn.WriteToUDPAddrPort(p, addr)
return err
diff --git a/internal/quic/listener_test.go b/internal/quic/listener_test.go
index 77362aa..346f81c 100644
--- a/internal/quic/listener_test.go
+++ b/internal/quic/listener_test.go
@@ -114,7 +114,11 @@
idlec: make(chan struct{}),
conns: make(map[*Conn]*testConn),
}
- tl.l = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl))
+ var err error
+ tl.l, err = newListener((*testListenerUDPConn)(tl), config, (*testListenerHooks)(tl))
+ if err != nil {
+ t.Fatal(err)
+ }
t.Cleanup(tl.cleanup)
return tl
}
@@ -237,6 +241,13 @@
}
}
+// wantIdle indicates that we expect the Listener to not send any more datagrams.
+func (tl *testListener) wantIdle(expectation string) {
+ if got := tl.readDatagram(); got != nil {
+ tl.t.Fatalf("expect: %v\nunexpectedly got: %v", expectation, got)
+ }
+}
+
func (tl *testListener) newClientTLS(srcConnID, dstConnID []byte) []byte {
peerProvidedParams := defaultTransportParameters()
peerProvidedParams.initialSrcConnID = srcConnID
diff --git a/internal/quic/loss.go b/internal/quic/loss.go
index 152815a..c0f915b 100644
--- a/internal/quic/loss.go
+++ b/internal/quic/loss.go
@@ -281,6 +281,19 @@
c.cc.packetBatchEnd(now, space, &c.rtt, c.maxAckDelay)
}
+// discardPackets declares that packets within a number space will not be delivered
+// and that data contained in them should be resent.
+// For example, after receiving a Retry packet we discard already-sent Initial packets.
+func (c *lossState) discardPackets(space numberSpace, lossf func(numberSpace, *sentPacket, packetFate)) {
+ for i := 0; i < c.spaces[space].size; i++ {
+ sent := c.spaces[space].nth(i)
+ sent.lost = true
+ c.cc.packetDiscarded(sent)
+ lossf(numberSpace(space), sent, packetLost)
+ }
+ c.spaces[space].clean()
+}
+
// discardKeys is called when dropping packet protection keys for a number space.
func (c *lossState) discardKeys(now time.Time, space numberSpace) {
// https://www.rfc-editor.org/rfc/rfc9002.html#section-6.4
diff --git a/internal/quic/packet.go b/internal/quic/packet.go
index 7d69f96..df589cc 100644
--- a/internal/quic/packet.go
+++ b/internal/quic/packet.go
@@ -97,6 +97,9 @@
streamFinBit = 0x01
)
+// Maximum length of a connection ID.
+const maxConnIDLen = 20
+
// isLongHeader returns true if b is the first byte of a long header.
func isLongHeader(b byte) bool {
return b&headerFormLong == headerFormLong
diff --git a/internal/quic/packet_parser.go b/internal/quic/packet_parser.go
index ce04339..8bcd866 100644
--- a/internal/quic/packet_parser.go
+++ b/internal/quic/packet_parser.go
@@ -47,7 +47,7 @@
// Destination Connection ID Length (8),
// Destination Connection ID (0..160),
p.dstConnID, n = consumeUint8Bytes(b)
- if n < 0 || len(p.dstConnID) > 20 {
+ if n < 0 || len(p.dstConnID) > maxConnIDLen {
return longPacket{}, -1
}
b = b[n:]
@@ -55,7 +55,7 @@
// Source Connection ID Length (8),
// Source Connection ID (0..160),
p.srcConnID, n = consumeUint8Bytes(b)
- if n < 0 || len(p.dstConnID) > 20 {
+ if n < 0 || len(p.dstConnID) > maxConnIDLen {
return longPacket{}, -1
}
b = b[n:]
diff --git a/internal/quic/retry.go b/internal/quic/retry.go
new file mode 100644
index 0000000..e3d9f4d
--- /dev/null
+++ b/internal/quic/retry.go
@@ -0,0 +1,235 @@
+// 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.
+
+//go:build go1.21
+
+package quic
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/binary"
+ "net/netip"
+ "time"
+
+ "golang.org/x/crypto/chacha20poly1305"
+)
+
+// AEAD and nonce used to compute the Retry Integrity Tag.
+// https://www.rfc-editor.org/rfc/rfc9001#section-5.8
+var (
+ retrySecret = []byte{0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, 0xc8, 0x4e}
+ retryNonce = []byte{0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb}
+ retryAEAD = func() cipher.AEAD {
+ c, err := aes.NewCipher(retrySecret)
+ if err != nil {
+ panic(err)
+ }
+ aead, err := cipher.NewGCM(c)
+ if err != nil {
+ panic(err)
+ }
+ return aead
+ }()
+)
+
+// retryTokenValidityPeriod is how long we accept a Retry packet token after sending it.
+const retryTokenValidityPeriod = 5 * time.Second
+
+// retryState generates and validates a listener's retry tokens.
+type retryState struct {
+ aead cipher.AEAD
+}
+
+func (rs *retryState) init() error {
+ // Retry tokens are authenticated using a per-server key chosen at start time.
+ // TODO: Provide a way for the user to set this key.
+ secret := make([]byte, chacha20poly1305.KeySize)
+ if _, err := rand.Read(secret); err != nil {
+ return err
+ }
+ aead, err := chacha20poly1305.NewX(secret)
+ if err != nil {
+ panic(err)
+ }
+ rs.aead = aead
+ return nil
+}
+
+// Retry tokens are encrypted with an AEAD.
+// The plaintext contains the time the token was created and
+// the original destination connection ID.
+// The additional data contains the sender's source address and original source connection ID.
+// The token nonce is randomly generated.
+// We use the nonce as the Source Connection ID of the Retry packet.
+// Since the 24-byte XChaCha20-Poly1305 nonce is too large to fit in a 20-byte connection ID,
+// we include the remaining 4 bytes of nonce in the token.
+//
+// Token {
+// Last 4 Bytes of Nonce (32),
+// Ciphertext (..),
+// }
+//
+// Plaintext {
+// Timestamp (64),
+// Original Destination Connection ID,
+// }
+//
+//
+// Additional Data {
+// Original Source Connection ID Length (8),
+// Original Source Connection ID (..),
+// IP Address (32..128),
+// Port (16),
+// }
+//
+// TODO: Consider using AES-256-GCM-SIV once crypto/tls supports it.
+
+func (rs *retryState) makeToken(now time.Time, srcConnID, origDstConnID []byte, addr netip.AddrPort) (token, newDstConnID []byte, err error) {
+ nonce := make([]byte, rs.aead.NonceSize())
+ if _, err := rand.Read(nonce); err != nil {
+ return nil, nil, err
+ }
+
+ var plaintext []byte
+ plaintext = binary.BigEndian.AppendUint64(plaintext, uint64(now.Unix()))
+ plaintext = append(plaintext, origDstConnID...)
+
+ token = append(token, nonce[maxConnIDLen:]...)
+ token = rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr))
+ return token, nonce[:maxConnIDLen], nil
+}
+
+func (rs *retryState) validateToken(now time.Time, token, srcConnID, dstConnID []byte, addr netip.AddrPort) (origDstConnID []byte, ok bool) {
+ tokenNonceLen := rs.aead.NonceSize() - maxConnIDLen
+ if len(token) < tokenNonceLen {
+ return nil, false
+ }
+ nonce := append([]byte{}, dstConnID...)
+ nonce = append(nonce, token[:tokenNonceLen]...)
+ ciphertext := token[tokenNonceLen:]
+
+ plaintext, err := rs.aead.Open(nil, nonce, ciphertext, rs.additionalData(srcConnID, addr))
+ if err != nil {
+ return nil, false
+ }
+ if len(plaintext) < 8 {
+ return nil, false
+ }
+ when := time.Unix(int64(binary.BigEndian.Uint64(plaintext)), 0)
+ origDstConnID = plaintext[8:]
+
+ // We allow for tokens created in the future (up to the validity period),
+ // which likely indicates that the system clock was adjusted backwards.
+ if d := abs(now.Sub(when)); d > retryTokenValidityPeriod {
+ return nil, false
+ }
+
+ return origDstConnID, true
+}
+
+func (rs *retryState) additionalData(srcConnID []byte, addr netip.AddrPort) []byte {
+ var additional []byte
+ additional = appendUint8Bytes(additional, srcConnID)
+ additional = append(additional, addr.Addr().AsSlice()...)
+ additional = binary.BigEndian.AppendUint16(additional, addr.Port())
+ return additional
+}
+
+func (l *Listener) validateInitialAddress(now time.Time, p genericLongPacket, addr netip.AddrPort) (origDstConnID []byte, ok bool) {
+ // The retry token is at the start of an Initial packet's data.
+ token, n := consumeUint8Bytes(p.data)
+ if n < 0 {
+ // We've already validated that the packet is at least 1200 bytes long,
+ // so there's no way for even a maximum size token to not fit.
+ // Check anyway.
+ return nil, false
+ }
+ if len(token) == 0 {
+ // The sender has not provided a token.
+ // Send a Retry packet to them with one.
+ l.sendRetry(now, p, addr)
+ return nil, false
+ }
+ origDstConnID, ok = l.retry.validateToken(now, token, p.srcConnID, p.dstConnID, addr)
+ if !ok {
+ // This does not seem to be a valid token.
+ // Close the connection with an INVALID_TOKEN error.
+ // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5
+ l.sendConnectionClose(p, addr, errInvalidToken)
+ return nil, false
+ }
+ return origDstConnID, true
+}
+
+func (l *Listener) sendRetry(now time.Time, p genericLongPacket, addr netip.AddrPort) {
+ token, srcConnID, err := l.retry.makeToken(now, p.srcConnID, p.dstConnID, addr)
+ if err != nil {
+ return
+ }
+ b := encodeRetryPacket(p.dstConnID, retryPacket{
+ dstConnID: p.srcConnID,
+ srcConnID: srcConnID,
+ token: token,
+ })
+ l.sendDatagram(b, addr)
+}
+
+type retryPacket struct {
+ dstConnID []byte
+ srcConnID []byte
+ token []byte
+}
+
+func encodeRetryPacket(originalDstConnID []byte, p retryPacket) []byte {
+ // Retry packets include an integrity tag, computed by AEAD_AES_128_GCM over
+ // the original destination connection ID followed by the Retry packet
+ // (less the integrity tag itself).
+ // https://www.rfc-editor.org/rfc/rfc9001#section-5.8
+ //
+ // Create the pseudo-packet (including the original DCID), append the tag,
+ // and return the Retry packet.
+ var b []byte
+ b = appendUint8Bytes(b, originalDstConnID) // Original Destination Connection ID
+ start := len(b) // start of the Retry packet
+ b = append(b, headerFormLong|fixedBit|longPacketTypeRetry)
+ b = binary.BigEndian.AppendUint32(b, quicVersion1) // Version
+ b = appendUint8Bytes(b, p.dstConnID) // Destination Connection ID
+ b = appendUint8Bytes(b, p.srcConnID) // Source Connection ID
+ b = append(b, p.token...) // Token
+ b = retryAEAD.Seal(b, retryNonce, nil, b) // Retry Integrity Tag
+ return b[start:]
+}
+
+func parseRetryPacket(b, origDstConnID []byte) (p retryPacket, ok bool) {
+ const retryIntegrityTagLength = 128 / 8
+
+ lp, ok := parseGenericLongHeaderPacket(b)
+ if !ok {
+ return retryPacket{}, false
+ }
+ if len(lp.data) < retryIntegrityTagLength {
+ return retryPacket{}, false
+ }
+ gotTag := lp.data[len(lp.data)-retryIntegrityTagLength:]
+
+ // Create the pseudo-packet consisting of the original destination connection ID
+ // followed by the Retry packet (less the integrity tag).
+ // Use this to validate the packet integrity tag.
+ pseudo := appendUint8Bytes(nil, origDstConnID)
+ pseudo = append(pseudo, b[:len(b)-retryIntegrityTagLength]...)
+ wantTag := retryAEAD.Seal(nil, retryNonce, nil, pseudo)
+ if !bytes.Equal(gotTag, wantTag) {
+ return retryPacket{}, false
+ }
+
+ token := lp.data[:len(lp.data)-retryIntegrityTagLength]
+ return retryPacket{
+ dstConnID: lp.dstConnID,
+ srcConnID: lp.srcConnID,
+ token: token,
+ }, true
+}
diff --git a/internal/quic/retry_test.go b/internal/quic/retry_test.go
new file mode 100644
index 0000000..f754270
--- /dev/null
+++ b/internal/quic/retry_test.go
@@ -0,0 +1,568 @@
+// 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.
+
+//go:build go1.21
+
+package quic
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "net/netip"
+ "testing"
+ "time"
+)
+
+type retryServerTest struct {
+ tl *testListener
+ originalSrcConnID []byte
+ originalDstConnID []byte
+ retry retryPacket
+ initialCrypto []byte
+}
+
+// newRetryServerTest creates a test server connection,
+// sends the connection an Initial packet,
+// and expects a Retry in response.
+func newRetryServerTest(t *testing.T) *retryServerTest {
+ t.Helper()
+ config := &Config{
+ TLSConfig: newTestTLSConfig(serverSide),
+ RequireAddressValidation: true,
+ }
+ tl := newTestListener(t, config)
+ srcID := testPeerConnID(0)
+ dstID := testLocalConnID(-1)
+ params := defaultTransportParameters()
+ params.initialSrcConnID = srcID
+ initialCrypto := initialClientCrypto(t, tl, params)
+
+ // Initial packet with no Token.
+ // Server responds with a Retry containing a token.
+ tl.writeDatagram(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 0,
+ version: quicVersion1,
+ srcConnID: srcID,
+ dstConnID: dstID,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: initialCrypto,
+ },
+ },
+ }},
+ paddedSize: 1200,
+ })
+ got := tl.readDatagram()
+ if len(got.packets) != 1 || got.packets[0].ptype != packetTypeRetry {
+ t.Fatalf("got datagram: %v\nwant Retry", got)
+ }
+ p := got.packets[0]
+ if got, want := p.dstConnID, srcID; !bytes.Equal(got, want) {
+ t.Fatalf("Retry destination = {%x}, want {%x}", got, want)
+ }
+
+ return &retryServerTest{
+ tl: tl,
+ originalSrcConnID: srcID,
+ originalDstConnID: dstID,
+ retry: retryPacket{
+ dstConnID: p.dstConnID,
+ srcConnID: p.srcConnID,
+ token: p.token,
+ },
+ initialCrypto: initialCrypto,
+ }
+}
+
+func TestRetryServerSucceeds(t *testing.T) {
+ rt := newRetryServerTest(t)
+ tl := rt.tl
+ tl.advance(retryTokenValidityPeriod)
+ tl.writeDatagram(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 1,
+ version: quicVersion1,
+ srcConnID: rt.originalSrcConnID,
+ dstConnID: rt.retry.srcConnID,
+ token: rt.retry.token,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: rt.initialCrypto,
+ },
+ },
+ }},
+ paddedSize: 1200,
+ })
+ tc := tl.accept()
+ initial := tc.readPacket()
+ if initial == nil || initial.ptype != packetTypeInitial {
+ t.Fatalf("got packet:\n%v\nwant: Initial", initial)
+ }
+ handshake := tc.readPacket()
+ if handshake == nil || handshake.ptype != packetTypeHandshake {
+ t.Fatalf("got packet:\n%v\nwant: Handshake", initial)
+ }
+ if got, want := tc.sentTransportParameters.retrySrcConnID, rt.retry.srcConnID; !bytes.Equal(got, want) {
+ t.Errorf("retry_source_connection_id = {%x}, want {%x}", got, want)
+ }
+ if got, want := tc.sentTransportParameters.initialSrcConnID, initial.srcConnID; !bytes.Equal(got, want) {
+ t.Errorf("initial_source_connection_id = {%x}, want {%x}", got, want)
+ }
+ if got, want := tc.sentTransportParameters.originalDstConnID, rt.originalDstConnID; !bytes.Equal(got, want) {
+ t.Errorf("original_destination_connection_id = {%x}, want {%x}", got, want)
+ }
+}
+
+func TestRetryServerTokenInvalid(t *testing.T) {
+ // "If a server receives a client Initial that contains an invalid Retry token [...]
+ // the server SHOULD immediately close [...] the connection with an
+ // INVALID_TOKEN error."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-5
+ rt := newRetryServerTest(t)
+ tl := rt.tl
+ tl.writeDatagram(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 1,
+ version: quicVersion1,
+ srcConnID: rt.originalSrcConnID,
+ dstConnID: rt.retry.srcConnID,
+ token: append(rt.retry.token, 0),
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: rt.initialCrypto,
+ },
+ },
+ }},
+ paddedSize: 1200,
+ })
+ tl.wantDatagram("server closes connection after Initial with invalid Retry token",
+ initialConnectionCloseDatagram(
+ rt.retry.srcConnID,
+ rt.originalSrcConnID,
+ errInvalidToken))
+}
+
+func TestRetryServerTokenTooOld(t *testing.T) {
+ // "[...] a token SHOULD have an expiration time [...]"
+ // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3
+ rt := newRetryServerTest(t)
+ tl := rt.tl
+ tl.advance(retryTokenValidityPeriod + time.Second)
+ tl.writeDatagram(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 1,
+ version: quicVersion1,
+ srcConnID: rt.originalSrcConnID,
+ dstConnID: rt.retry.srcConnID,
+ token: rt.retry.token,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: rt.initialCrypto,
+ },
+ },
+ }},
+ paddedSize: 1200,
+ })
+ tl.wantDatagram("server closes connection after Initial with expired token",
+ initialConnectionCloseDatagram(
+ rt.retry.srcConnID,
+ rt.originalSrcConnID,
+ errInvalidToken))
+}
+
+func TestRetryServerTokenWrongIP(t *testing.T) {
+ // "Tokens sent in Retry packets SHOULD include information that allows the server
+ // to verify that the source IP address and port in client packets remain constant."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3
+ rt := newRetryServerTest(t)
+ tl := rt.tl
+ tl.writeDatagram(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 1,
+ version: quicVersion1,
+ srcConnID: rt.originalSrcConnID,
+ dstConnID: rt.retry.srcConnID,
+ token: rt.retry.token,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: rt.initialCrypto,
+ },
+ },
+ }},
+ paddedSize: 1200,
+ addr: netip.MustParseAddrPort("10.0.0.2:8000"),
+ })
+ tl.wantDatagram("server closes connection after Initial from wrong address",
+ initialConnectionCloseDatagram(
+ rt.retry.srcConnID,
+ rt.originalSrcConnID,
+ errInvalidToken))
+}
+
+func TestRetryServerIgnoresRetry(t *testing.T) {
+ tc := newTestConn(t, serverSide)
+ tc.handshake()
+ tc.write(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: testLocalConnID(-1),
+ srcConnID: testPeerConnID(0),
+ dstConnID: testLocalConnID(0),
+ token: []byte{1, 2, 3, 4},
+ }},
+ })
+ // Send two packets, to trigger an immediate ACK.
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.writeFrames(packetType1RTT, debugFramePing{})
+ tc.wantFrameType("server connection ignores spurious Retry packet",
+ packetType1RTT, debugFrameAck{})
+}
+
+func TestRetryClientSuccess(t *testing.T) {
+ // "This token MUST be repeated by the client in all Initial packets it sends
+ // for that connection after it receives the Retry packet."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1
+ tc := newTestConn(t, clientSide)
+ tc.wantFrame("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ })
+ newServerConnID := []byte("new_conn_id")
+ token := []byte("token")
+ tc.write(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: testLocalConnID(-1),
+ srcConnID: newServerConnID,
+ dstConnID: testLocalConnID(0),
+ token: token,
+ }},
+ })
+ tc.wantPacket("client sends a new Initial packet with a token",
+ &testPacket{
+ ptype: packetTypeInitial,
+ num: 1,
+ version: quicVersion1,
+ srcConnID: testLocalConnID(0),
+ dstConnID: newServerConnID,
+ token: token,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ },
+ },
+ },
+ )
+ tc.advanceToTimer()
+ tc.wantPacket("after PTO client sends another Initial packet with a token",
+ &testPacket{
+ ptype: packetTypeInitial,
+ num: 2,
+ version: quicVersion1,
+ srcConnID: testLocalConnID(0),
+ dstConnID: newServerConnID,
+ token: token,
+ frames: []debugFrame{
+ debugFrameCrypto{
+ data: tc.cryptoDataOut[tls.QUICEncryptionLevelInitial],
+ },
+ },
+ },
+ )
+}
+
+func TestRetryClientInvalidServerTransportParameters(t *testing.T) {
+ // Various permutations of missing or invalid values for transport parameters
+ // after a Retry.
+ // https://www.rfc-editor.org/rfc/rfc9000#section-7.3
+ initialSrcConnID := testPeerConnID(0)
+ originalDstConnID := testLocalConnID(-1)
+ retrySrcConnID := testPeerConnID(100)
+ for _, test := range []struct {
+ name string
+ f func(*transportParameters)
+ ok bool
+ }{{
+ name: "valid",
+ f: func(p *transportParameters) {},
+ ok: true,
+ }, {
+ name: "missing initial_source_connection_id",
+ f: func(p *transportParameters) {
+ p.initialSrcConnID = nil
+ },
+ }, {
+ name: "invalid initial_source_connection_id",
+ f: func(p *transportParameters) {
+ p.initialSrcConnID = []byte("invalid")
+ },
+ }, {
+ name: "missing original_destination_connection_id",
+ f: func(p *transportParameters) {
+ p.originalDstConnID = nil
+ },
+ }, {
+ name: "invalid original_destination_connection_id",
+ f: func(p *transportParameters) {
+ p.originalDstConnID = []byte("invalid")
+ },
+ }, {
+ name: "missing retry_source_connection_id",
+ f: func(p *transportParameters) {
+ p.retrySrcConnID = nil
+ },
+ }, {
+ name: "invalid retry_source_connection_id",
+ f: func(p *transportParameters) {
+ p.retrySrcConnID = []byte("invalid")
+ },
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ tc := newTestConn(t, clientSide,
+ func(p *transportParameters) {
+ p.initialSrcConnID = initialSrcConnID
+ p.originalDstConnID = originalDstConnID
+ p.retrySrcConnID = retrySrcConnID
+ },
+ test.f)
+ tc.ignoreFrame(frameTypeAck)
+ tc.wantFrameType("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ tc.write(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: originalDstConnID,
+ srcConnID: retrySrcConnID,
+ dstConnID: testLocalConnID(0),
+ token: []byte{1, 2, 3, 4},
+ }},
+ })
+ tc.wantFrameType("client resends Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ tc.writeFrames(packetTypeInitial,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
+ })
+ tc.writeFrames(packetTypeHandshake,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
+ })
+ if test.ok {
+ tc.wantFrameType("valid params, client sends Handshake",
+ packetTypeHandshake, debugFrameCrypto{})
+ } else {
+ tc.wantFrame("invalid transport parameters",
+ packetTypeInitial, debugFrameConnectionCloseTransport{
+ code: errTransportParameter,
+ })
+ }
+ })
+ }
+}
+
+func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) {
+ // "After the client has received and processed an Initial or Retry packet
+ // from the server, it MUST discard any subsequent Retry packets that it receives."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
+ tc := newTestConn(t, clientSide)
+ tc.ignoreFrame(frameTypeAck)
+ tc.ignoreFrame(frameTypeNewConnectionID)
+ tc.wantFrameType("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ tc.writeFrames(packetTypeInitial,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelInitial],
+ })
+ retry := &testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: testLocalConnID(-1),
+ srcConnID: testPeerConnID(100),
+ dstConnID: testLocalConnID(0),
+ token: []byte{1, 2, 3, 4},
+ }},
+ }
+ tc.write(retry)
+ tc.wantIdle("client ignores Retry after receiving Initial packet")
+ tc.writeFrames(packetTypeHandshake,
+ debugFrameCrypto{
+ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake],
+ })
+ tc.wantFrameType("client Handshake CRYPTO data",
+ packetTypeHandshake, debugFrameCrypto{})
+ tc.write(retry)
+ tc.wantIdle("client ignores Retry after discarding Initial keys")
+}
+
+func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) {
+ // "After the client has received and processed an Initial or Retry packet
+ // from the server, it MUST discard any subsequent Retry packets that it receives."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1
+ tc := newTestConn(t, clientSide)
+ tc.wantFrameType("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ retry := &testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: testLocalConnID(-1),
+ srcConnID: testPeerConnID(100),
+ dstConnID: testLocalConnID(0),
+ token: []byte{1, 2, 3, 4},
+ }},
+ }
+ tc.write(retry)
+ tc.wantFrameType("client resends Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ tc.write(retry)
+ tc.wantIdle("client ignores second Retry")
+}
+
+func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) {
+ tc := newTestConn(t, clientSide)
+ tc.wantFrameType("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ pkt := encodeRetryPacket(testLocalConnID(-1), retryPacket{
+ srcConnID: testPeerConnID(100),
+ dstConnID: testLocalConnID(0),
+ token: []byte{1, 2, 3, 4},
+ })
+ pkt[len(pkt)-1] ^= 1 // invalidate the integrity tag
+ tc.listener.write(&datagram{
+ b: pkt,
+ addr: testClientAddr,
+ })
+ tc.wantIdle("client ignores Retry with invalid integrity tag")
+}
+
+func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) {
+ // "A client MUST discard a Retry packet with a zero-length Retry Token field."
+ // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2
+ tc := newTestConn(t, clientSide)
+ tc.wantFrameType("client Initial CRYPTO data",
+ packetTypeInitial, debugFrameCrypto{})
+ tc.write(&testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeRetry,
+ originalDstConnID: testLocalConnID(-1),
+ srcConnID: testPeerConnID(100),
+ dstConnID: testLocalConnID(0),
+ token: []byte{},
+ }},
+ })
+ tc.wantIdle("client ignores Retry with zero-length token")
+}
+
+func TestRetryStateValidateInvalidToken(t *testing.T) {
+ // Test handling of tokens that may have a valid signature,
+ // but unexpected contents.
+ var rs retryState
+ if err := rs.init(); err != nil {
+ t.Fatal(err)
+ }
+ nonce := make([]byte, rs.aead.NonceSize())
+ now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
+ srcConnID := []byte{1, 2, 3, 4}
+ dstConnID := nonce[:20]
+ addr := testClientAddr
+
+ for _, test := range []struct {
+ name string
+ token []byte
+ }{{
+ name: "token too short",
+ token: []byte{1, 2, 3},
+ }, {
+ name: "token plaintext too short",
+ token: func() []byte {
+ plaintext := make([]byte, 7) // not enough bytes of content
+ token := append([]byte{}, nonce[20:]...)
+ return rs.aead.Seal(token, nonce, plaintext, rs.additionalData(srcConnID, addr))
+ }(),
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ if _, ok := rs.validateToken(now, test.token, srcConnID, dstConnID, addr); ok {
+ t.Errorf("validateToken succeeded, want failure")
+ }
+ })
+ }
+}
+
+func TestParseInvalidRetryPackets(t *testing.T) {
+ originalDstConnID := []byte{1, 2, 3, 4}
+ goodPkt := encodeRetryPacket(originalDstConnID, retryPacket{
+ dstConnID: []byte{1},
+ srcConnID: []byte{2},
+ token: []byte{3},
+ })
+ for _, test := range []struct {
+ name string
+ pkt []byte
+ }{{
+ name: "packet too short",
+ pkt: goodPkt[:len(goodPkt)-4],
+ }, {
+ name: "packet header invalid",
+ pkt: goodPkt[:5],
+ }, {
+ name: "integrity tag invalid",
+ pkt: func() []byte {
+ pkt := cloneBytes(goodPkt)
+ pkt[len(pkt)-1] ^= 1
+ return pkt
+ }(),
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ if _, ok := parseRetryPacket(test.pkt, originalDstConnID); ok {
+ t.Errorf("parseRetryPacket succeded, want failure")
+ }
+ })
+ }
+}
+
+func initialClientCrypto(t *testing.T, l *testListener, p transportParameters) []byte {
+ t.Helper()
+ config := &tls.QUICConfig{TLSConfig: newTestTLSConfig(clientSide)}
+ tlsClient := tls.QUICClient(config)
+ tlsClient.SetTransportParameters(marshalTransportParameters(p))
+ tlsClient.Start(context.Background())
+ //defer tlsClient.Close()
+ l.peerTLSConn = tlsClient
+ var data []byte
+ for {
+ e := tlsClient.NextEvent()
+ switch e.Kind {
+ case tls.QUICNoEvent:
+ return data
+ case tls.QUICWriteData:
+ if e.Level != tls.QUICEncryptionLevelInitial {
+ t.Fatal("initial data at unexpected level")
+ }
+ data = append(data, e.Data...)
+ }
+ }
+}
+
+func initialConnectionCloseDatagram(srcConnID, dstConnID []byte, code transportError) *testDatagram {
+ return &testDatagram{
+ packets: []*testPacket{{
+ ptype: packetTypeInitial,
+ num: 0,
+ version: quicVersion1,
+ srcConnID: srcConnID,
+ dstConnID: dstConnID,
+ frames: []debugFrame{
+ debugFrameConnectionCloseTransport{
+ code: code,
+ },
+ },
+ }},
+ }
+}