97 lines
2.0 KiB
Go
97 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package netx
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"tailscale.com/util/slicesx"
|
|
)
|
|
|
|
// RaceDial races TCP connect attempts across addrs using a
|
|
// happy-eyeballs-style staggered approach: a new dial is started every
|
|
// fallbackDelay, and the first successful connection wins. Losers are
|
|
// cancelled and their connections closed. If all dials fail, the first
|
|
// error is returned.
|
|
//
|
|
// Addresses are interleaved v6-first so that IPv6 is preferred but both
|
|
// families are tried promptly. The dial func is always called with
|
|
// network "tcp".
|
|
func RaceDial(ctx context.Context, addrs []netip.AddrPort, dial DialFunc, fallbackDelay time.Duration) (net.Conn, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
var v4, v6 []netip.AddrPort
|
|
for _, a := range addrs {
|
|
if a.Addr().Is6() {
|
|
v6 = append(v6, a)
|
|
} else {
|
|
v4 = append(v4, a)
|
|
}
|
|
}
|
|
ordered := slicesx.Interleave(v6, v4)
|
|
|
|
type result struct {
|
|
c net.Conn
|
|
err error
|
|
}
|
|
resc := make(chan result) // unbuffered: senders sync with collector
|
|
failBoost := make(chan struct{}, 1) // wake the launcher when a dial fails fast
|
|
|
|
go func() {
|
|
for i, addr := range ordered {
|
|
if i > 0 {
|
|
t := time.NewTimer(fallbackDelay)
|
|
select {
|
|
case <-t.C:
|
|
case <-failBoost:
|
|
t.Stop()
|
|
case <-ctx.Done():
|
|
t.Stop()
|
|
return
|
|
}
|
|
}
|
|
go func() {
|
|
c, err := dial(ctx, "tcp", addr.String())
|
|
if err != nil {
|
|
select {
|
|
case failBoost <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
select {
|
|
case resc <- result{c, err}:
|
|
case <-ctx.Done():
|
|
if c != nil {
|
|
c.Close()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
|
|
var firstErr error
|
|
var nFailed int
|
|
for {
|
|
select {
|
|
case r := <-resc:
|
|
if r.err == nil {
|
|
return r.c, nil
|
|
}
|
|
if firstErr == nil {
|
|
firstErr = r.err
|
|
}
|
|
nFailed++
|
|
if nFailed >= len(ordered) {
|
|
return nil, firstErr
|
|
}
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
}
|