package quic
import (
"context"
"crypto/rand"
"errors"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go/internal/ackhandler"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/wire"
)
var (
ErrPathClosed = errors .New ("path closed" )
ErrPathNotValidated = errors .New ("path not yet validated" )
)
var errPathDoesNotExist = errors .New ("path does not exist" )
type Path struct {
id pathID
pathManager *pathManagerOutgoing
tr *Transport
initialRTT time .Duration
enablePath func ()
validated atomic .Bool
abandon chan struct {}
}
func (p *Path ) Probe (ctx context .Context ) error {
path := p .pathManager .addPath (p , p .enablePath )
p .pathManager .enqueueProbe (p )
nextProbeDur := p .initialRTT
var timer *time .Timer
var timerChan <-chan time .Time
for {
select {
case <- ctx .Done ():
return context .Cause (ctx )
case <- path .Validated ():
p .validated .Store (true )
return nil
case <- timerChan :
nextProbeDur *= 2
p .pathManager .enqueueProbe (p )
case <- path .ProbeSent ():
case <- p .abandon :
return ErrPathClosed
}
if timer != nil {
timer .Stop ()
}
timer = time .NewTimer (nextProbeDur )
timerChan = timer .C
}
}
func (p *Path ) Switch () error {
if err := p .pathManager .switchToPath (p .id ); err != nil {
switch {
case errors .Is (err , ErrPathNotValidated ):
return err
case errors .Is (err , errPathDoesNotExist ) && !p .validated .Load ():
select {
case <- p .abandon :
return ErrPathClosed
default :
return ErrPathNotValidated
}
default :
return ErrPathClosed
}
}
return nil
}
func (p *Path ) Close () error {
select {
case <- p .abandon :
return nil
default :
}
if err := p .pathManager .removePath (p .id ); err != nil {
return err
}
close (p .abandon )
return nil
}
type pathOutgoing struct {
pathChallenges [][8 ]byte
tr *Transport
isValidated bool
probeSent chan struct {}
validated chan struct {}
enablePath func ()
}
func (p *pathOutgoing ) ProbeSent () <-chan struct {} { return p .probeSent }
func (p *pathOutgoing ) Validated () <-chan struct {} { return p .validated }
type pathManagerOutgoing struct {
getConnID func (pathID ) (_ protocol .ConnectionID , ok bool )
retireConnID func (pathID )
scheduleSending func ()
mx sync .Mutex
activePath pathID
pathsToProbe []pathID
paths map [pathID ]*pathOutgoing
nextPathID pathID
pathToSwitchTo *pathOutgoing
}
func newPathManagerOutgoing(
getConnID func (pathID ) (_ protocol .ConnectionID , ok bool ),
retireConnID func (pathID ),
scheduleSending func (),
) *pathManagerOutgoing {
return &pathManagerOutgoing {
activePath : 0 ,
nextPathID : 1 ,
getConnID : getConnID ,
retireConnID : retireConnID ,
scheduleSending : scheduleSending ,
paths : make (map [pathID ]*pathOutgoing , 4 ),
}
}
func (pm *pathManagerOutgoing ) addPath (p *Path , enablePath func ()) *pathOutgoing {
pm .mx .Lock ()
defer pm .mx .Unlock ()
if existingPath , ok := pm .paths [p .id ]; ok {
existingPath .validated = make (chan struct {})
return existingPath
}
path := &pathOutgoing {
tr : p .tr ,
probeSent : make (chan struct {}, 1 ),
validated : make (chan struct {}),
enablePath : enablePath ,
}
pm .paths [p .id ] = path
return path
}
func (pm *pathManagerOutgoing ) enqueueProbe (p *Path ) {
pm .mx .Lock ()
pm .pathsToProbe = append (pm .pathsToProbe , p .id )
pm .mx .Unlock ()
pm .scheduleSending ()
}
func (pm *pathManagerOutgoing ) removePath (id pathID ) error {
if err := pm .removePathImpl (id ); err != nil {
return err
}
pm .scheduleSending ()
return nil
}
func (pm *pathManagerOutgoing ) removePathImpl (id pathID ) error {
pm .mx .Lock ()
defer pm .mx .Unlock ()
if id == pm .activePath {
return errors .New ("cannot close active path" )
}
p , ok := pm .paths [id ]
if !ok {
return nil
}
if len (p .pathChallenges ) > 0 {
pm .retireConnID (id )
}
delete (pm .paths , id )
return nil
}
func (pm *pathManagerOutgoing ) switchToPath (id pathID ) error {
pm .mx .Lock ()
defer pm .mx .Unlock ()
p , ok := pm .paths [id ]
if !ok {
return errPathDoesNotExist
}
if !p .isValidated {
return ErrPathNotValidated
}
pm .pathToSwitchTo = p
pm .activePath = id
return nil
}
func (pm *pathManagerOutgoing ) NewPath (t *Transport , initialRTT time .Duration , enablePath func ()) *Path {
pm .mx .Lock ()
defer pm .mx .Unlock ()
id := pm .nextPathID
pm .nextPathID ++
return &Path {
pathManager : pm ,
id : id ,
tr : t ,
enablePath : enablePath ,
initialRTT : initialRTT ,
abandon : make (chan struct {}),
}
}
func (pm *pathManagerOutgoing ) NextPathToProbe () (_ protocol .ConnectionID , _ ackhandler .Frame , _ *Transport , hasPath bool ) {
pm .mx .Lock ()
defer pm .mx .Unlock ()
var p *pathOutgoing
id := invalidPathID
for _ , pID := range pm .pathsToProbe {
var ok bool
p , ok = pm .paths [pID ]
if ok {
id = pID
break
}
pm .pathsToProbe = pm .pathsToProbe [1 :]
}
if id == invalidPathID {
return protocol .ConnectionID {}, ackhandler .Frame {}, nil , false
}
connID , ok := pm .getConnID (id )
if !ok {
return protocol .ConnectionID {}, ackhandler .Frame {}, nil , false
}
var b [8 ]byte
_, _ = rand .Read (b [:])
p .pathChallenges = append (p .pathChallenges , b )
pm .pathsToProbe = pm .pathsToProbe [1 :]
p .enablePath ()
select {
case p .probeSent <- struct {}{}:
default :
}
frame := ackhandler .Frame {
Frame : &wire .PathChallengeFrame {Data : b },
Handler : (*pathManagerOutgoingAckHandler )(pm ),
}
return connID , frame , p .tr , true
}
func (pm *pathManagerOutgoing ) HandlePathResponseFrame (f *wire .PathResponseFrame ) {
pm .mx .Lock ()
defer pm .mx .Unlock ()
for _ , p := range pm .paths {
if slices .Contains (p .pathChallenges , f .Data ) {
if !p .isValidated {
p .isValidated = true
p .pathChallenges = nil
close (p .validated )
}
break
}
}
}
func (pm *pathManagerOutgoing ) ShouldSwitchPath () (*Transport , bool ) {
pm .mx .Lock ()
defer pm .mx .Unlock ()
if pm .pathToSwitchTo == nil {
return nil , false
}
p := pm .pathToSwitchTo
pm .pathToSwitchTo = nil
return p .tr , true
}
type pathManagerOutgoingAckHandler pathManagerOutgoing
var _ ackhandler .FrameHandler = &pathManagerOutgoingAckHandler {}
func (pm *pathManagerOutgoingAckHandler ) OnAcked (wire .Frame ) {}
func (pm *pathManagerOutgoingAckHandler ) OnLost (wire .Frame ) {}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .