forked from quic-go/quic-go
evict old paths when the client probes a new path (#5034)
* store paths in a slice, not a map No functional change expected. * save the timestamp of the last received packet for a path * evict old paths * reduce the path timeout to 5s
This commit is contained in:
@@ -3,6 +3,8 @@ package quic
|
||||
import (
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go/internal/ackhandler"
|
||||
"github.com/quic-go/quic-go/internal/protocol"
|
||||
@@ -14,10 +16,21 @@ type pathID int64
|
||||
|
||||
const invalidPathID pathID = -1
|
||||
|
||||
// Maximum number of paths to keep track of.
|
||||
// If the peer probes another path (before the pathTimeout of an existing path expires),
|
||||
// this probing attempt is ignored.
|
||||
const maxPaths = 3
|
||||
|
||||
// If no packet is received for a path for pathTimeout,
|
||||
// the path can be evicted when the peer probes another path.
|
||||
// This prevents an attacker from churning through paths by duplicating packets and
|
||||
// sending them with spoofed source addresses.
|
||||
const pathTimeout = 5 * time.Second
|
||||
|
||||
type path struct {
|
||||
id pathID
|
||||
addr net.Addr
|
||||
lastPacketTime time.Time
|
||||
pathChallenge [8]byte
|
||||
validated bool
|
||||
rcvdNonProbing bool
|
||||
@@ -25,7 +38,8 @@ type path struct {
|
||||
|
||||
type pathManager struct {
|
||||
nextPathID pathID
|
||||
paths map[pathID]*path
|
||||
// ordered by lastPacketTime, with the most recently used path at the end
|
||||
paths []*path
|
||||
|
||||
getConnID func(pathID) (_ protocol.ConnectionID, ok bool)
|
||||
retireConnID func(pathID)
|
||||
@@ -39,7 +53,7 @@ func newPathManager(
|
||||
logger utils.Logger,
|
||||
) *pathManager {
|
||||
return &pathManager{
|
||||
paths: make(map[pathID]*path),
|
||||
paths: make([]*path, 0, maxPaths+1),
|
||||
getConnID: getConnID,
|
||||
retireConnID: retireConnID,
|
||||
logger: logger,
|
||||
@@ -50,15 +64,15 @@ func newPathManager(
|
||||
// May return nil.
|
||||
func (pm *pathManager) HandlePacket(
|
||||
remoteAddr net.Addr,
|
||||
t time.Time,
|
||||
pathChallenge *wire.PathChallengeFrame, // may be nil if the packet didn't contain a PATH_CHALLENGE
|
||||
isNonProbing bool,
|
||||
) (_ protocol.ConnectionID, _ []ackhandler.Frame, shouldSwitch bool) {
|
||||
var p *path
|
||||
pathID := pm.nextPathID
|
||||
for id, path := range pm.paths {
|
||||
for i, path := range pm.paths {
|
||||
if addrsEqual(path.addr, remoteAddr) {
|
||||
p = path
|
||||
pathID = id
|
||||
p.lastPacketTime = t
|
||||
// already sent a PATH_CHALLENGE for this path
|
||||
if isNonProbing {
|
||||
path.rcvdNonProbing = true
|
||||
@@ -67,6 +81,11 @@ func (pm *pathManager) HandlePacket(
|
||||
pm.logger.Debugf("received packet for path %s that was already probed, validated: %t", remoteAddr, path.validated)
|
||||
}
|
||||
shouldSwitch = path.validated && path.rcvdNonProbing
|
||||
if i != len(pm.paths)-1 {
|
||||
// move the path to the end of the list
|
||||
pm.paths = slices.Delete(pm.paths, i, i+1)
|
||||
pm.paths = append(pm.paths, p)
|
||||
}
|
||||
if pathChallenge == nil {
|
||||
return protocol.ConnectionID{}, nil, shouldSwitch
|
||||
}
|
||||
@@ -74,10 +93,22 @@ func (pm *pathManager) HandlePacket(
|
||||
}
|
||||
|
||||
if len(pm.paths) >= maxPaths {
|
||||
if pm.logger.Debug() {
|
||||
pm.logger.Debugf("received packet for previously unseen path %s, but already have %d paths", remoteAddr, len(pm.paths))
|
||||
if pm.paths[0].lastPacketTime.Add(pathTimeout).After(t) {
|
||||
if pm.logger.Debug() {
|
||||
pm.logger.Debugf("received packet for previously unseen path %s, but already have %d paths", remoteAddr, len(pm.paths))
|
||||
}
|
||||
return protocol.ConnectionID{}, nil, shouldSwitch
|
||||
}
|
||||
return protocol.ConnectionID{}, nil, shouldSwitch
|
||||
// evict the oldest path, if the last packet was received more than pathTimeout ago
|
||||
pm.retireConnID(pm.paths[0].id)
|
||||
pm.paths = pm.paths[1:]
|
||||
}
|
||||
|
||||
var pathID pathID
|
||||
if p != nil {
|
||||
pathID = p.id
|
||||
} else {
|
||||
pathID = pm.nextPathID
|
||||
}
|
||||
|
||||
// previously unseen path, initiate path validation by sending a PATH_CHALLENGE
|
||||
@@ -92,16 +123,18 @@ func (pm *pathManager) HandlePacket(
|
||||
var pathChallengeData [8]byte
|
||||
rand.Read(pathChallengeData[:])
|
||||
p = &path{
|
||||
id: pm.nextPathID,
|
||||
addr: remoteAddr,
|
||||
lastPacketTime: t,
|
||||
rcvdNonProbing: isNonProbing,
|
||||
pathChallenge: pathChallengeData,
|
||||
}
|
||||
pm.nextPathID++
|
||||
pm.paths = append(pm.paths, p)
|
||||
frames = append(frames, ackhandler.Frame{
|
||||
Frame: &wire.PathChallengeFrame{Data: p.pathChallenge},
|
||||
Handler: (*pathManagerAckHandler)(pm),
|
||||
})
|
||||
pm.paths[pm.nextPathID] = p
|
||||
pm.nextPathID++
|
||||
pm.logger.Debugf("enqueueing PATH_CHALLENGE for new path %s", remoteAddr)
|
||||
}
|
||||
if pathChallenge != nil {
|
||||
@@ -127,14 +160,15 @@ func (pm *pathManager) HandlePathResponseFrame(f *wire.PathResponseFrame) {
|
||||
// SwitchToPath is called when the connection switches to a new path
|
||||
func (pm *pathManager) SwitchToPath(addr net.Addr) {
|
||||
// retire all other paths
|
||||
for id := range pm.paths {
|
||||
if addrsEqual(pm.paths[id].addr, addr) {
|
||||
pm.logger.Debugf("switching to path %d (%s)", id, addr)
|
||||
for _, path := range pm.paths {
|
||||
if addrsEqual(path.addr, addr) {
|
||||
pm.logger.Debugf("switching to path %d (%s)", path.id, addr)
|
||||
continue
|
||||
}
|
||||
pm.retireConnID(id)
|
||||
pm.retireConnID(path.id)
|
||||
}
|
||||
clear(pm.paths)
|
||||
pm.paths = pm.paths[:0]
|
||||
}
|
||||
|
||||
type pathManagerAckHandler pathManager
|
||||
@@ -147,10 +181,10 @@ func (pm *pathManagerAckHandler) OnAcked(f wire.Frame) {}
|
||||
func (pm *pathManagerAckHandler) OnLost(f wire.Frame) {
|
||||
// TODO: retransmit the packet the first time it is lost
|
||||
pc := f.(*wire.PathChallengeFrame)
|
||||
for id, path := range pm.paths {
|
||||
for i, path := range pm.paths {
|
||||
if path.pathChallenge == pc.Data {
|
||||
delete(pm.paths, id)
|
||||
pm.retireConnID(id)
|
||||
pm.paths = slices.Delete(pm.paths, i, i+1)
|
||||
pm.retireConnID(path.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user