quic: handle PATH_CHALLENGE and PATH_RESPONSE frames

We do not support path migration yet, and will ignore packets
sent from anything other than the peer's original address.
Handle PATH_CHALLENGE frames by sending a PATH_RESPONSE.
Handle PATH_RESPONSE frames by closing the connection
(since we never send a challenge to respond to).

For golang/go#58547

Change-Id: I828b9dcb23e17f5edf3d605b8f04efdafb392807
Reviewed-on: https://go-review.googlesource.com/c/net/+/565795
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Damien Neil
2023-11-28 15:31:58 -08:00
parent a6a24dd292
commit 57e4cc7d88
12 changed files with 255 additions and 32 deletions

View File

@@ -37,6 +37,7 @@ type Conn struct {
connIDState connIDState
loss lossState
streams streamsState
path pathState
// Packet protection keys, CRYPTO streams, and TLS state.
keysInitial fixedKeyPair

View File

@@ -663,6 +663,29 @@ func TestLostRetireConnectionIDFrame(t *testing.T) {
})
}
func TestLostPathResponseFrame(t *testing.T) {
// "Responses to path validation using PATH_RESPONSE frames are sent just once."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.12
lostFrameTest(t, func(t *testing.T, pto bool) {
tc := newTestConn(t, clientSide)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
tc.ignoreFrame(frameTypePing)
data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
tc.writeFrames(packetType1RTT, debugFramePathChallenge{
data: data,
})
tc.wantFrame("response to PATH_CHALLENGE",
packetType1RTT, debugFramePathResponse{
data: data,
})
tc.triggerLossOrPTO(packetType1RTT, pto)
tc.wantIdle("lost PATH_RESPONSE frame is not retransmitted")
})
}
func TestLostHandshakeDoneFrame(t *testing.T) {
// "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16

View File

@@ -46,11 +46,11 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) (handled bool) {
// https://www.rfc-editor.org/rfc/rfc9000#section-14.1-4
return false
}
n = c.handleLongHeader(now, ptype, initialSpace, c.keysInitial.r, buf)
n = c.handleLongHeader(now, dgram, ptype, initialSpace, c.keysInitial.r, buf)
case packetTypeHandshake:
n = c.handleLongHeader(now, ptype, handshakeSpace, c.keysHandshake.r, buf)
n = c.handleLongHeader(now, dgram, ptype, handshakeSpace, c.keysHandshake.r, buf)
case packetType1RTT:
n = c.handle1RTT(now, buf)
n = c.handle1RTT(now, dgram, buf)
case packetTypeRetry:
c.handleRetry(now, buf)
return true
@@ -86,7 +86,7 @@ func (c *Conn) handleDatagram(now time.Time, dgram *datagram) (handled bool) {
return true
}
func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int {
func (c *Conn) handleLongHeader(now time.Time, dgram *datagram, ptype packetType, space numberSpace, k fixedKeys, buf []byte) int {
if !k.isSet() {
return skipLongHeaderPacket(buf)
}
@@ -125,7 +125,7 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa
c.logLongPacketReceived(p, buf[:n])
}
c.connIDState.handlePacket(c, p.ptype, p.srcConnID)
ackEliciting := c.handleFrames(now, ptype, space, p.payload)
ackEliciting := c.handleFrames(now, dgram, ptype, space, p.payload)
c.acks[space].receive(now, space, p.num, ackEliciting)
if p.ptype == packetTypeHandshake && c.side == serverSide {
c.loss.validateClientAddress()
@@ -138,7 +138,7 @@ func (c *Conn) handleLongHeader(now time.Time, ptype packetType, space numberSpa
return n
}
func (c *Conn) handle1RTT(now time.Time, buf []byte) int {
func (c *Conn) handle1RTT(now time.Time, dgram *datagram, buf []byte) int {
if !c.keysAppData.canRead() {
// 1-RTT packets extend to the end of the datagram,
// so skip the remainder of the datagram if we can't parse this.
@@ -175,7 +175,7 @@ func (c *Conn) handle1RTT(now time.Time, buf []byte) int {
if c.logEnabled(QLogLevelPacket) {
c.log1RTTPacketReceived(p, buf)
}
ackEliciting := c.handleFrames(now, packetType1RTT, appDataSpace, p.payload)
ackEliciting := c.handleFrames(now, dgram, packetType1RTT, appDataSpace, p.payload)
c.acks[appDataSpace].receive(now, appDataSpace, p.num, ackEliciting)
return len(buf)
}
@@ -252,7 +252,7 @@ func (c *Conn) handleVersionNegotiation(now time.Time, pkt []byte) {
c.abortImmediately(now, errVersionNegotiation)
}
func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) {
func (c *Conn) handleFrames(now time.Time, dgram *datagram, ptype packetType, space numberSpace, payload []byte) (ackEliciting bool) {
if len(payload) == 0 {
// "An endpoint MUST treat receipt of a packet containing no frames
// as a connection error of type PROTOCOL_VIOLATION."
@@ -373,6 +373,16 @@ func (c *Conn) handleFrames(now time.Time, ptype packetType, space numberSpace,
return
}
n = c.handleRetireConnectionIDFrame(now, space, payload)
case frameTypePathChallenge:
if !frameOK(c, ptype, __01) {
return
}
n = c.handlePathChallengeFrame(now, dgram, space, payload)
case frameTypePathResponse:
if !frameOK(c, ptype, ___1) {
return
}
n = c.handlePathResponseFrame(now, space, payload)
case frameTypeConnectionCloseTransport:
// Transport CONNECTION_CLOSE is OK in all spaces.
n = c.handleConnectionCloseTransportFrame(now, payload)
@@ -546,6 +556,24 @@ func (c *Conn) handleRetireConnectionIDFrame(now time.Time, space numberSpace, p
return n
}
func (c *Conn) handlePathChallengeFrame(now time.Time, dgram *datagram, space numberSpace, payload []byte) int {
data, n := consumePathChallengeFrame(payload)
if n < 0 {
return -1
}
c.handlePathChallenge(now, dgram, data)
return n
}
func (c *Conn) handlePathResponseFrame(now time.Time, space numberSpace, payload []byte) int {
data, n := consumePathResponseFrame(payload)
if n < 0 {
return -1
}
c.handlePathResponse(now, data)
return n
}
func (c *Conn) handleConnectionCloseTransportFrame(now time.Time, payload []byte) int {
code, _, reason, n := consumeConnectionCloseTransportFrame(payload)
if n < 0 {

View File

@@ -271,6 +271,13 @@ func (c *Conn) appendFrames(now time.Time, space numberSpace, pnum packetNumber,
return
}
// PATH_RESPONSE
if pad, ok := c.appendPathFrames(); !ok {
return
} else if pad {
defer c.w.appendPaddingTo(smallestMaxDatagramSize)
}
// All stream-related frames. This should come last in the packet,
// so large amounts of STREAM data don't crowd out other frames
// we may need to send.

View File

@@ -168,6 +168,7 @@ type testConn struct {
sentDatagrams [][]byte
sentPackets []*testPacket
sentFrames []debugFrame
lastDatagram *testDatagram
lastPacket *testPacket
recvDatagram chan *datagram
@@ -576,6 +577,7 @@ func (tc *testConn) readDatagram() *testDatagram {
}
p.frames = frames
}
tc.lastDatagram = d
return d
}

View File

@@ -77,6 +77,7 @@ func parseDebugFrame(b []byte) (f debugFrame, n int) {
// debugFramePadding is a sequence of PADDING frames.
type debugFramePadding struct {
size int
to int // alternate for writing packets: pad to
}
func parseDebugFramePadding(b []byte) (f debugFramePadding, n int) {
@@ -95,6 +96,10 @@ func (f debugFramePadding) write(w *packetWriter) bool {
if w.avail() == 0 {
return false
}
if f.to > 0 {
w.appendPaddingTo(f.to)
return true
}
for i := 0; i < f.size && w.avail() > 0; i++ {
w.b = append(w.b, frameTypePadding)
}
@@ -584,7 +589,7 @@ func (f debugFrameRetireConnectionID) LogValue() slog.Value {
// debugFramePathChallenge is a PATH_CHALLENGE frame.
type debugFramePathChallenge struct {
data uint64
data pathChallengeData
}
func parseDebugFramePathChallenge(b []byte) (f debugFramePathChallenge, n int) {
@@ -593,7 +598,7 @@ func parseDebugFramePathChallenge(b []byte) (f debugFramePathChallenge, n int) {
}
func (f debugFramePathChallenge) String() string {
return fmt.Sprintf("PATH_CHALLENGE Data=%016x", f.data)
return fmt.Sprintf("PATH_CHALLENGE Data=%x", f.data)
}
func (f debugFramePathChallenge) write(w *packetWriter) bool {
@@ -603,13 +608,13 @@ func (f debugFramePathChallenge) write(w *packetWriter) bool {
func (f debugFramePathChallenge) LogValue() slog.Value {
return slog.GroupValue(
slog.String("frame_type", "path_challenge"),
slog.String("data", fmt.Sprintf("%016x", f.data)),
slog.String("data", fmt.Sprintf("%x", f.data)),
)
}
// debugFramePathResponse is a PATH_RESPONSE frame.
type debugFramePathResponse struct {
data uint64
data pathChallengeData
}
func parseDebugFramePathResponse(b []byte) (f debugFramePathResponse, n int) {
@@ -618,7 +623,7 @@ func parseDebugFramePathResponse(b []byte) (f debugFramePathResponse, n int) {
}
func (f debugFramePathResponse) String() string {
return fmt.Sprintf("PATH_RESPONSE Data=%016x", f.data)
return fmt.Sprintf("PATH_RESPONSE Data=%x", f.data)
}
func (f debugFramePathResponse) write(w *packetWriter) bool {
@@ -628,7 +633,7 @@ func (f debugFramePathResponse) write(w *packetWriter) bool {
func (f debugFramePathResponse) LogValue() slog.Value {
return slog.GroupValue(
slog.String("frame_type", "path_response"),
slog.String("data", fmt.Sprintf("%016x", f.data)),
slog.String("data", fmt.Sprintf("%x", f.data)),
)
}

View File

@@ -517,7 +517,7 @@ func TestFrameEncodeDecode(t *testing.T) {
s: "PATH_CHALLENGE Data=0123456789abcdef",
j: `{"frame_type":"path_challenge","data":"0123456789abcdef"}`,
f: debugFramePathChallenge{
data: 0x0123456789abcdef,
data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
},
b: []byte{
0x1a, // Type (i) = 0x1a,
@@ -527,7 +527,7 @@ func TestFrameEncodeDecode(t *testing.T) {
s: "PATH_RESPONSE Data=0123456789abcdef",
j: `{"frame_type":"path_response","data":"0123456789abcdef"}`,
f: debugFramePathResponse{
data: 0x0123456789abcdef,
data: pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
},
b: []byte{
0x1b, // Type (i) = 0x1b,

View File

@@ -463,18 +463,17 @@ func consumeRetireConnectionIDFrame(b []byte) (seq int64, n int) {
return seq, n
}
func consumePathChallengeFrame(b []byte) (data uint64, n int) {
func consumePathChallengeFrame(b []byte) (data pathChallengeData, n int) {
n = 1
var nn int
data, nn = consumeUint64(b[n:])
if nn < 0 {
return 0, -1
nn := copy(data[:], b[n:])
if nn != len(data) {
return data, -1
}
n += nn
return data, n
}
func consumePathResponseFrame(b []byte) (data uint64, n int) {
func consumePathResponseFrame(b []byte) (data pathChallengeData, n int) {
return consumePathChallengeFrame(b) // identical frame format
}

View File

@@ -243,10 +243,7 @@ func (w *packetWriter) appendPingFrame() (added bool) {
return false
}
w.b = append(w.b, frameTypePing)
// Mark this packet as ack-eliciting and in-flight,
// but there's no need to record the presence of a PING frame in it.
w.sent.ackEliciting = true
w.sent.inFlight = true
w.sent.markAckEliciting() // no need to record the frame itself
return true
}
@@ -495,23 +492,23 @@ func (w *packetWriter) appendRetireConnectionIDFrame(seq int64) (added bool) {
return true
}
func (w *packetWriter) appendPathChallengeFrame(data uint64) (added bool) {
func (w *packetWriter) appendPathChallengeFrame(data pathChallengeData) (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)
w.b = append(w.b, data[:]...)
w.sent.markAckEliciting() // no need to record the frame itself
return true
}
func (w *packetWriter) appendPathResponseFrame(data uint64) (added bool) {
func (w *packetWriter) appendPathResponseFrame(data pathChallengeData) (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)
w.b = append(w.b, data[:]...)
w.sent.markAckEliciting() // no need to record the frame itself
return true
}

89
internal/quic/path.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright 2024 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 "time"
type pathState struct {
// Response to a peer's PATH_CHALLENGE.
// This is not a sentVal, because we don't resend lost PATH_RESPONSE frames.
// We only track the most recent PATH_CHALLENGE.
// If the peer sends a second PATH_CHALLENGE before we respond to the first,
// we'll drop the first response.
sendPathResponse pathResponseType
data pathChallengeData
}
// pathChallengeData is data carried in a PATH_CHALLENGE or PATH_RESPONSE frame.
type pathChallengeData [64 / 8]byte
type pathResponseType uint8
const (
pathResponseNotNeeded = pathResponseType(iota)
pathResponseSmall // send PATH_RESPONSE, do not expand datagram
pathResponseExpanded // send PATH_RESPONSE, expand datagram to 1200 bytes
)
func (c *Conn) handlePathChallenge(_ time.Time, dgram *datagram, data pathChallengeData) {
// A PATH_RESPONSE is sent in a datagram expanded to 1200 bytes,
// except when this would exceed the anti-amplification limit.
//
// Rather than maintaining anti-amplification state for each path
// we may be sending a PATH_RESPONSE on, follow the following heuristic:
//
// If we receive a PATH_CHALLENGE in an expanded datagram,
// respond with an expanded datagram.
//
// If we receive a PATH_CHALLENGE in a non-expanded datagram,
// then the peer is presumably blocked by its own anti-amplification limit.
// Respond with a non-expanded datagram. Receiving this PATH_RESPONSE
// will validate the path to the peer, remove its anti-amplification limit,
// and permit it to send a followup PATH_CHALLENGE in an expanded datagram.
// https://www.rfc-editor.org/rfc/rfc9000.html#section-8.2.1
if len(dgram.b) >= smallestMaxDatagramSize {
c.path.sendPathResponse = pathResponseExpanded
} else {
c.path.sendPathResponse = pathResponseSmall
}
c.path.data = data
}
func (c *Conn) handlePathResponse(now time.Time, _ pathChallengeData) {
// "If the content of a PATH_RESPONSE frame does not match the content of
// a PATH_CHALLENGE frame previously sent by the endpoint,
// the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.18-4
//
// We never send PATH_CHALLENGE frames.
c.abort(now, localTransportError{
code: errProtocolViolation,
reason: "PATH_RESPONSE received when no PATH_CHALLENGE sent",
})
}
// appendPathFrames appends path validation related frames to the current packet.
// If the return value pad is true, then the packet should be padded to 1200 bytes.
func (c *Conn) appendPathFrames() (pad, ok bool) {
if c.path.sendPathResponse == pathResponseNotNeeded {
return pad, true
}
// We're required to send the PATH_RESPONSE on the path where the
// PATH_CHALLENGE was received (RFC 9000, Section 8.2.2).
//
// At the moment, we don't support path migration and reject packets if
// the peer changes its source address, so just sending the PATH_RESPONSE
// in a regular datagram is fine.
if !c.w.appendPathResponseFrame(c.path.data) {
return pad, false
}
if c.path.sendPathResponse == pathResponseExpanded {
pad = true
}
c.path.sendPathResponse = pathResponseNotNeeded
return pad, true
}

View File

@@ -0,0 +1,66 @@
// 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 (
"testing"
)
func TestPathChallengeReceived(t *testing.T) {
for _, test := range []struct {
name string
padTo int
wantPadding int
}{{
name: "unexpanded",
padTo: 0,
wantPadding: 0,
}, {
name: "expanded",
padTo: 1200,
wantPadding: 1200,
}} {
// "The recipient of [a PATH_CHALLENGE] frame MUST generate
// a PATH_RESPONSE frame [...] containing the same Data value."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7
tc := newTestConn(t, clientSide)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}
tc.writeFrames(packetType1RTT, debugFramePathChallenge{
data: data,
}, debugFramePadding{
to: test.padTo,
})
tc.wantFrame("response to PATH_CHALLENGE",
packetType1RTT, debugFramePathResponse{
data: data,
})
if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want {
t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want)
}
tc.wantIdle("connection is idle")
}
}
func TestPathResponseMismatchReceived(t *testing.T) {
// "If the content of a PATH_RESPONSE frame does not match the content of
// a PATH_CHALLENGE frame previously sent by the endpoint,
// the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION."
// https://www.rfc-editor.org/rfc/rfc9000.html#section-19.18-4
tc := newTestConn(t, clientSide)
tc.handshake()
tc.ignoreFrame(frameTypeAck)
tc.writeFrames(packetType1RTT, debugFramePathResponse{
data: pathChallengeData{},
})
tc.wantFrame("invalid PATH_RESPONSE causes the connection to close",
packetType1RTT, debugFrameConnectionCloseTransport{
code: errProtocolViolation,
},
)
}

View File

@@ -59,6 +59,12 @@ func (sent *sentPacket) reset() {
}
}
// markAckEliciting marks the packet as containing an ack-eliciting frame.
func (sent *sentPacket) markAckEliciting() {
sent.ackEliciting = true
sent.inFlight = true
}
// The append* methods record information about frames in the packet.
func (sent *sentPacket) appendNonAckElicitingFrame(frameType byte) {