mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-19 04:59:56 +00:00
The success path in handleResult left settled at 0, so drainLoser waited on a result that was already consumed and blocked forever, leaking a goroutine and the results channel on every successful race. Increment settled before stop() so drainLoser only waits for actual started losers.
166 lines
3.7 KiB
Go
166 lines
3.7 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
raceTotalTimeout = 40 * time.Second
|
|
raceFallbackDelay = 10 * time.Second
|
|
)
|
|
|
|
type raceAttempt struct {
|
|
conn net.Conn
|
|
err error
|
|
}
|
|
|
|
type raceOutcome struct {
|
|
conn net.Conn
|
|
err error
|
|
done bool
|
|
}
|
|
|
|
type connRace struct {
|
|
opener *FallbackOpener
|
|
peerKey string
|
|
remoteRelayServer RelayServer
|
|
preferForeign bool
|
|
|
|
raceCtx context.Context
|
|
otherCtx context.Context
|
|
cancelPreferred context.CancelFunc
|
|
cancelOther context.CancelFunc
|
|
results chan raceAttempt
|
|
fallbackTimer *time.Timer
|
|
|
|
otherStarted bool
|
|
settled int
|
|
lastErr error
|
|
}
|
|
|
|
type FallbackOpener struct {
|
|
home *Client
|
|
foreignStore *ForeignRelaysStore
|
|
}
|
|
|
|
func NewFallbackOpener(home *Client, foreignStore *ForeignRelaysStore) *FallbackOpener {
|
|
return &FallbackOpener{
|
|
home: home,
|
|
foreignStore: foreignStore,
|
|
}
|
|
}
|
|
|
|
func (r *FallbackOpener) Run(ctx context.Context, peerKey string, remoteRelayServer RelayServer, preferForeign bool) (net.Conn, error) {
|
|
raceCtx, cancel := context.WithTimeout(ctx, raceTotalTimeout)
|
|
defer cancel()
|
|
|
|
preferredCtx, cancelPreferred := context.WithCancel(raceCtx)
|
|
otherCtx, cancelOther := context.WithCancel(raceCtx)
|
|
|
|
race := &connRace{
|
|
opener: r,
|
|
peerKey: peerKey,
|
|
remoteRelayServer: remoteRelayServer,
|
|
preferForeign: preferForeign,
|
|
raceCtx: raceCtx,
|
|
otherCtx: otherCtx,
|
|
cancelPreferred: cancelPreferred,
|
|
cancelOther: cancelOther,
|
|
results: make(chan raceAttempt, 2),
|
|
fallbackTimer: time.NewTimer(raceFallbackDelay),
|
|
}
|
|
defer race.fallbackTimer.Stop()
|
|
|
|
go func() {
|
|
race.results <- r.open(preferredCtx, peerKey, remoteRelayServer, preferForeign)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-race.fallbackTimer.C:
|
|
race.startOther()
|
|
case res := <-race.results:
|
|
if o := race.handleResult(res); o.done {
|
|
return o.conn, o.err
|
|
}
|
|
case <-raceCtx.Done():
|
|
return race.onTimeout()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *connRace) startOther() {
|
|
if c.otherStarted {
|
|
return
|
|
}
|
|
c.otherStarted = true
|
|
c.fallbackTimer.Stop()
|
|
go func() {
|
|
c.results <- c.opener.open(c.otherCtx, c.peerKey, c.remoteRelayServer, !c.preferForeign)
|
|
}()
|
|
}
|
|
|
|
func (c *connRace) handleResult(res raceAttempt) raceOutcome {
|
|
if (res.err == nil && res.conn != nil) || errors.Is(res.err, ErrConnAlreadyExists) {
|
|
c.settled++
|
|
c.stop()
|
|
return raceOutcome{conn: res.conn, err: res.err, done: true}
|
|
}
|
|
|
|
c.lastErr = res.err
|
|
c.settled++
|
|
if !c.otherStarted {
|
|
c.startOther()
|
|
return raceOutcome{}
|
|
}
|
|
if c.settled == 2 {
|
|
c.cancelPreferred()
|
|
c.cancelOther()
|
|
return raceOutcome{err: c.lastErr, done: true}
|
|
}
|
|
return raceOutcome{}
|
|
}
|
|
|
|
func (c *connRace) onTimeout() (net.Conn, error) {
|
|
c.stop()
|
|
if c.lastErr != nil {
|
|
return nil, c.lastErr
|
|
}
|
|
return nil, c.raceCtx.Err()
|
|
}
|
|
|
|
func (c *connRace) stop() {
|
|
c.cancelPreferred()
|
|
c.cancelOther()
|
|
go c.opener.drainLoser(c.results, c.settled, c.otherStarted)
|
|
}
|
|
|
|
func (r *FallbackOpener) open(ctx context.Context, peerKey string, remoteRelayServer RelayServer, foreign bool) raceAttempt {
|
|
if foreign {
|
|
conn, err := r.foreignStore.OpenConn(ctx, peerKey, remoteRelayServer)
|
|
return raceAttempt{conn: conn, err: err}
|
|
}
|
|
conn, err := r.home.OpenConn(ctx, peerKey)
|
|
return raceAttempt{conn: conn, err: err}
|
|
}
|
|
|
|
func (r *FallbackOpener) drainLoser(results chan raceAttempt, settled int, otherStarted bool) {
|
|
started := 1
|
|
if otherStarted {
|
|
started = 2
|
|
}
|
|
for i := settled; i < started; i++ {
|
|
res := <-results
|
|
if res.conn != nil {
|
|
if err := res.conn.Close(); err != nil {
|
|
log.Debugf("failed to close losing relay connection: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|