mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-19 23:11:29 +02:00
181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
package dialer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
DefaultConnectionTimeout = 30 * time.Second
|
|
)
|
|
|
|
type DialeFn interface {
|
|
// Dial connects to address. serverName, when non-empty, overrides the TLS
|
|
// ServerName used for SNI/cert validation. Empty means derive from address.
|
|
Dial(ctx context.Context, address, serverName string) (net.Conn, error)
|
|
Protocol() string
|
|
}
|
|
|
|
type dialResult struct {
|
|
Conn net.Conn
|
|
Protocol string
|
|
Err error
|
|
}
|
|
|
|
type RaceDial struct {
|
|
log *log.Entry
|
|
serverURL string
|
|
serverName string
|
|
dialerFns []DialeFn
|
|
connectionTimeout time.Duration
|
|
sequential bool
|
|
}
|
|
|
|
func NewRaceDial(log *log.Entry, connectionTimeout time.Duration, serverURL string, dialerFns ...DialeFn) *RaceDial {
|
|
return &RaceDial{
|
|
log: log,
|
|
serverURL: serverURL,
|
|
dialerFns: dialerFns,
|
|
connectionTimeout: connectionTimeout,
|
|
}
|
|
}
|
|
|
|
// WithServerName sets a TLS SNI/cert validation override. Used when serverURL
|
|
// contains an IP literal but the cert is issued for a different hostname.
|
|
//
|
|
// Mutates the receiver and is not safe for concurrent reconfiguration; a
|
|
// RaceDial is intended to be constructed per dial and discarded.
|
|
func (r *RaceDial) WithServerName(serverName string) *RaceDial {
|
|
r.serverName = serverName
|
|
return r
|
|
}
|
|
|
|
// WithSequential makes Dial try the dialers in order, falling back to the next
|
|
// only when one fails to connect, instead of racing them concurrently.
|
|
//
|
|
// Mutates the receiver and is not safe for concurrent reconfiguration; a
|
|
// RaceDial is intended to be constructed per dial and discarded.
|
|
func (r *RaceDial) WithSequential() *RaceDial {
|
|
r.sequential = true
|
|
return r
|
|
}
|
|
|
|
func (r *RaceDial) Dial(ctx context.Context) (net.Conn, error) {
|
|
if r.sequential {
|
|
return r.dialSequential(ctx)
|
|
}
|
|
|
|
connChan := make(chan dialResult, len(r.dialerFns))
|
|
winnerConn := make(chan net.Conn, 1)
|
|
errChan := make(chan error, 1)
|
|
abortCtx, abort := context.WithCancel(ctx)
|
|
defer abort()
|
|
|
|
for _, dfn := range r.dialerFns {
|
|
go r.dial(dfn, abortCtx, connChan)
|
|
}
|
|
|
|
go r.processResults(connChan, winnerConn, errChan, abort)
|
|
|
|
conn, ok := <-winnerConn
|
|
if !ok {
|
|
return nil, <-errChan
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// dialSequential tries each dialer in order, returning the first connection and
|
|
// falling back to the next on failure.
|
|
func (r *RaceDial) dialSequential(ctx context.Context) (net.Conn, error) {
|
|
var errs []error
|
|
for _, dfn := range r.dialerFns {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
attemptCtx, cancel := context.WithTimeout(ctx, r.connectionTimeout)
|
|
r.log.Infof("dialing Relay server via %s", dfn.Protocol())
|
|
conn, err := dfn.Dial(attemptCtx, r.serverURL, r.serverName)
|
|
cancel()
|
|
if err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
return nil, err
|
|
}
|
|
r.log.Errorf("failed to dial via %s: %s", dfn.Protocol(), err)
|
|
errs = append(errs, fmt.Errorf("%s: %w", dfn.Protocol(), err))
|
|
continue
|
|
}
|
|
r.log.Infof("successfully dialed via: %s", dfn.Protocol())
|
|
return conn, nil
|
|
}
|
|
return nil, dialErr(errs)
|
|
}
|
|
|
|
func (r *RaceDial) dial(dfn DialeFn, abortCtx context.Context, connChan chan dialResult) {
|
|
ctx, cancel := context.WithTimeout(abortCtx, r.connectionTimeout)
|
|
defer cancel()
|
|
|
|
r.log.Infof("dialing Relay server via %s", dfn.Protocol())
|
|
conn, err := dfn.Dial(ctx, r.serverURL, r.serverName)
|
|
connChan <- dialResult{Conn: conn, Protocol: dfn.Protocol(), Err: err}
|
|
}
|
|
|
|
func (r *RaceDial) processResults(connChan chan dialResult, winnerConn chan net.Conn, errChan chan error, abort context.CancelFunc) {
|
|
var hasWinner bool
|
|
errsByProtocol := make(map[string]error)
|
|
for i := 0; i < len(r.dialerFns); i++ {
|
|
dr := <-connChan
|
|
if dr.Err != nil {
|
|
if errors.Is(dr.Err, context.Canceled) {
|
|
r.log.Infof("connection attempt aborted via: %s", dr.Protocol)
|
|
} else {
|
|
r.log.Errorf("failed to dial via %s: %s", dr.Protocol, dr.Err)
|
|
errsByProtocol[dr.Protocol] = fmt.Errorf("%s: %w", dr.Protocol, dr.Err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if hasWinner {
|
|
if cerr := dr.Conn.Close(); cerr != nil {
|
|
r.log.Warnf("failed to close connection via %s: %s", dr.Protocol, cerr)
|
|
}
|
|
continue
|
|
}
|
|
|
|
r.log.Infof("successfully dialed via: %s", dr.Protocol)
|
|
|
|
abort()
|
|
hasWinner = true
|
|
winnerConn <- dr.Conn
|
|
}
|
|
if !hasWinner {
|
|
errChan <- dialErr(r.orderedErrs(errsByProtocol))
|
|
}
|
|
close(winnerConn)
|
|
}
|
|
|
|
// orderedErrs returns the per-protocol errors in dialer order, so the combined
|
|
// error is stable regardless of which attempt failed first.
|
|
func (r *RaceDial) orderedErrs(byProtocol map[string]error) []error {
|
|
errs := make([]error, 0, len(byProtocol))
|
|
for _, dfn := range r.dialerFns {
|
|
if err, ok := byProtocol[dfn.Protocol()]; ok {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// dialErr combines per-dialer failures, preserving the underlying reasons
|
|
// (e.g. "connection refused") rather than a generic message.
|
|
func dialErr(errs []error) error {
|
|
if len(errs) == 0 {
|
|
return errors.New("no relay transport available")
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|