mirror of
https://github.com/golang/go
synced 2024-10-04 15:21:22 -06:00
net: add sequential and RFC 6555-compliant TCP dialing.
dialSerial connects to a list of addresses in sequence. If a timeout is specified, then each address gets an equal fraction of the remaining time, with a magic constant (2 seconds) to prevent "dial a million addresses" from allotting zero time to each. Normally, net.Dial passes the DNS stub resolver's output to dialSerial. If an error occurs (like destination/port unreachable), it quickly skips to the next address, but a blackhole in the network will cause the connection to hang until the timeout elapses. This is how UNIXy clients traditionally behave, and is usually sufficient for non-broken networks. The DualStack flag enables dialParallel, which implements Happy Eyeballs by racing two dialSerial goroutines, giving the preferred family a head start (300ms by default). This allows clients to avoid long timeouts when the network blackholes IPv4 xor IPv6. Fixes #8453 Fixes #8455 Fixes #8847 Change-Id: Ie415809c9226a1f7342b0217dcdd8f224ae19058 Reviewed-on: https://go-review.googlesource.com/8768 Reviewed-by: Mikio Hara <mikioh.mikioh@gmail.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
parent
12b05bf8fd
commit
0d8366e2d6
241
src/net/dial.go
241
src/net/dial.go
@ -21,6 +21,9 @@ type Dialer struct {
|
|||||||
//
|
//
|
||||||
// The default is no timeout.
|
// The default is no timeout.
|
||||||
//
|
//
|
||||||
|
// When dialing a name with multiple IP addresses, the timeout
|
||||||
|
// may be divided between them.
|
||||||
|
//
|
||||||
// With or without a timeout, the operating system may impose
|
// With or without a timeout, the operating system may impose
|
||||||
// its own earlier timeout. For instance, TCP timeouts are
|
// its own earlier timeout. For instance, TCP timeouts are
|
||||||
// often around 3 minutes.
|
// often around 3 minutes.
|
||||||
@ -38,13 +41,17 @@ type Dialer struct {
|
|||||||
// If nil, a local address is automatically chosen.
|
// If nil, a local address is automatically chosen.
|
||||||
LocalAddr Addr
|
LocalAddr Addr
|
||||||
|
|
||||||
// DualStack allows a single dial to attempt to establish
|
// DualStack enables RFC 6555-compliant "Happy Eyeballs" dialing
|
||||||
// multiple IPv4 and IPv6 connections and to return the first
|
// when the network is "tcp" and the destination is a host name
|
||||||
// established connection when the network is "tcp" and the
|
// with both IPv4 and IPv6 addresses. This allows a client to
|
||||||
// destination is a host name that has multiple address family
|
// tolerate networks where one address family is silently broken.
|
||||||
// DNS records.
|
|
||||||
DualStack bool
|
DualStack bool
|
||||||
|
|
||||||
|
// FallbackDelay specifies the length of time to wait before
|
||||||
|
// spawning a fallback connection, when DualStack is enabled.
|
||||||
|
// If zero, a default delay of 300ms is used.
|
||||||
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
// KeepAlive specifies the keep-alive period for an active
|
// KeepAlive specifies the keep-alive period for an active
|
||||||
// network connection.
|
// network connection.
|
||||||
// If zero, keep-alives are not enabled. Network protocols
|
// If zero, keep-alives are not enabled. Network protocols
|
||||||
@ -54,11 +61,11 @@ type Dialer struct {
|
|||||||
|
|
||||||
// Return either now+Timeout or Deadline, whichever comes first.
|
// Return either now+Timeout or Deadline, whichever comes first.
|
||||||
// Or zero, if neither is set.
|
// Or zero, if neither is set.
|
||||||
func (d *Dialer) deadline() time.Time {
|
func (d *Dialer) deadline(now time.Time) time.Time {
|
||||||
if d.Timeout == 0 {
|
if d.Timeout == 0 {
|
||||||
return d.Deadline
|
return d.Deadline
|
||||||
}
|
}
|
||||||
timeoutDeadline := time.Now().Add(d.Timeout)
|
timeoutDeadline := now.Add(d.Timeout)
|
||||||
if d.Deadline.IsZero() || timeoutDeadline.Before(d.Deadline) {
|
if d.Deadline.IsZero() || timeoutDeadline.Before(d.Deadline) {
|
||||||
return timeoutDeadline
|
return timeoutDeadline
|
||||||
} else {
|
} else {
|
||||||
@ -66,6 +73,39 @@ func (d *Dialer) deadline() time.Time {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// partialDeadline returns the deadline to use for a single address,
|
||||||
|
// when multiple addresses are pending.
|
||||||
|
func (d *Dialer) partialDeadline(now time.Time, addrsRemaining int) (time.Time, error) {
|
||||||
|
deadline := d.deadline(now)
|
||||||
|
if deadline.IsZero() {
|
||||||
|
return deadline, nil
|
||||||
|
}
|
||||||
|
timeRemaining := deadline.Sub(now)
|
||||||
|
if timeRemaining <= 0 {
|
||||||
|
return time.Time{}, errTimeout
|
||||||
|
}
|
||||||
|
// Tentatively allocate equal time to each remaining address.
|
||||||
|
timeout := timeRemaining / time.Duration(addrsRemaining)
|
||||||
|
// If the time per address is too short, steal from the end of the list.
|
||||||
|
const saneMinimum = 2 * time.Second
|
||||||
|
if timeout < saneMinimum {
|
||||||
|
if timeRemaining < saneMinimum {
|
||||||
|
timeout = timeRemaining
|
||||||
|
} else {
|
||||||
|
timeout = saneMinimum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return now.Add(timeout), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dialer) fallbackDelay() time.Duration {
|
||||||
|
if d.FallbackDelay > 0 {
|
||||||
|
return d.FallbackDelay
|
||||||
|
} else {
|
||||||
|
return 300 * time.Millisecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseNetwork(net string) (afnet string, proto int, err error) {
|
func parseNetwork(net string) (afnet string, proto int, err error) {
|
||||||
i := last(net, ':')
|
i := last(net, ':')
|
||||||
if i < 0 { // no colon
|
if i < 0 { // no colon
|
||||||
@ -154,30 +194,44 @@ func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {
|
|||||||
return d.Dial(network, address)
|
return d.Dial(network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dialContext holds common state for all dial operations.
|
||||||
|
type dialContext struct {
|
||||||
|
Dialer
|
||||||
|
network, address string
|
||||||
|
}
|
||||||
|
|
||||||
// Dial connects to the address on the named network.
|
// Dial connects to the address on the named network.
|
||||||
//
|
//
|
||||||
// See func Dial for a description of the network and address
|
// See func Dial for a description of the network and address
|
||||||
// parameters.
|
// parameters.
|
||||||
func (d *Dialer) Dial(network, address string) (Conn, error) {
|
func (d *Dialer) Dial(network, address string) (Conn, error) {
|
||||||
addrs, err := resolveAddrList("dial", network, address, d.deadline())
|
addrs, err := resolveAddrList("dial", network, address, d.deadline(time.Now()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
|
return nil, &OpError{Op: "dial", Net: network, Source: nil, Addr: nil, Err: err}
|
||||||
}
|
}
|
||||||
var dialer func(deadline time.Time) (Conn, error)
|
|
||||||
|
ctx := &dialContext{
|
||||||
|
Dialer: *d,
|
||||||
|
network: network,
|
||||||
|
address: address,
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaries, fallbacks addrList
|
||||||
if d.DualStack && network == "tcp" {
|
if d.DualStack && network == "tcp" {
|
||||||
primaries, fallbacks := addrs.partition(isIPv4)
|
primaries, fallbacks = addrs.partition(isIPv4)
|
||||||
if len(fallbacks) > 0 {
|
} else {
|
||||||
dialer = func(deadline time.Time) (Conn, error) {
|
primaries = addrs
|
||||||
return dialMulti(network, address, d.LocalAddr, addrList{primaries[0], fallbacks[0]}, deadline)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var c Conn
|
||||||
|
if len(fallbacks) == 0 {
|
||||||
|
// dialParallel can accept an empty fallbacks list,
|
||||||
|
// but this shortcut avoids the goroutine/channel overhead.
|
||||||
|
c, err = dialSerial(ctx, primaries, nil)
|
||||||
|
} else {
|
||||||
|
c, err = dialParallel(ctx, primaries, fallbacks)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if dialer == nil {
|
|
||||||
dialer = func(deadline time.Time) (Conn, error) {
|
|
||||||
return dialSingle(network, address, d.LocalAddr, addrs.first(isIPv4), deadline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c, err := dial(network, addrs.first(isIPv4), dialer, d.deadline())
|
|
||||||
if d.KeepAlive > 0 && err == nil {
|
if d.KeepAlive > 0 && err == nil {
|
||||||
if tc, ok := c.(*TCPConn); ok {
|
if tc, ok := c.(*TCPConn); ok {
|
||||||
setKeepAlive(tc.fd, true)
|
setKeepAlive(tc.fd, true)
|
||||||
@ -188,70 +242,135 @@ func (d *Dialer) Dial(network, address string) (Conn, error) {
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// dialMulti attempts to establish connections to each destination of
|
// dialParallel races two copies of dialSerial, giving the first a
|
||||||
// the list of addresses. It will return the first established
|
// head start. It returns the first established connection and
|
||||||
// connection and close the other connections. Otherwise it returns
|
// closes the others. Otherwise it returns an error from the first
|
||||||
// error on the last attempt.
|
// primary address.
|
||||||
func dialMulti(net, addr string, la Addr, ras addrList, deadline time.Time) (Conn, error) {
|
func dialParallel(ctx *dialContext, primaries, fallbacks addrList) (Conn, error) {
|
||||||
type racer struct {
|
results := make(chan dialResult) // unbuffered, so dialSerialAsync can detect race loss & cleanup
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
defer close(cancel)
|
||||||
|
|
||||||
|
// Spawn the primary racer.
|
||||||
|
go dialSerialAsync(ctx, primaries, nil, cancel, results)
|
||||||
|
|
||||||
|
// Spawn the fallback racer.
|
||||||
|
fallbackTimer := time.NewTimer(ctx.fallbackDelay())
|
||||||
|
go dialSerialAsync(ctx, fallbacks, fallbackTimer, cancel, results)
|
||||||
|
|
||||||
|
var primaryErr error
|
||||||
|
for nracers := 2; nracers > 0; nracers-- {
|
||||||
|
res := <-results
|
||||||
|
// If we're still waiting for a connection, then hasten the delay.
|
||||||
|
// Otherwise, disable the Timer and let cancel take over.
|
||||||
|
if fallbackTimer.Stop() && res.error != nil {
|
||||||
|
fallbackTimer.Reset(0)
|
||||||
|
}
|
||||||
|
if res.error == nil {
|
||||||
|
return res.Conn, nil
|
||||||
|
}
|
||||||
|
if res.primary {
|
||||||
|
primaryErr = res.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, primaryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type dialResult struct {
|
||||||
Conn
|
Conn
|
||||||
error
|
error
|
||||||
|
primary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialSerialAsync runs dialSerial after some delay, and returns the
|
||||||
|
// resulting connection through a channel. When racing two connections,
|
||||||
|
// the primary goroutine uses a nil timer to omit the delay.
|
||||||
|
func dialSerialAsync(ctx *dialContext, ras addrList, timer *time.Timer, cancel <-chan struct{}, results chan<- dialResult) {
|
||||||
|
if timer != nil {
|
||||||
|
// We're in the fallback goroutine; sleep before connecting.
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
case <-cancel:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// Sig controls the flow of dial results on lane. It passes a
|
}
|
||||||
// token to the next racer and also indicates the end of flow
|
c, err := dialSerial(ctx, ras, cancel)
|
||||||
// by using closed channel.
|
select {
|
||||||
sig := make(chan bool, 1)
|
case results <- dialResult{c, err, timer == nil}:
|
||||||
lane := make(chan racer, 1)
|
// We won the race.
|
||||||
for _, ra := range ras {
|
case <-cancel:
|
||||||
go func(ra Addr) {
|
// The other goroutine won the race.
|
||||||
c, err := dialSingle(net, addr, la, ra, deadline)
|
if c != nil {
|
||||||
if _, ok := <-sig; ok {
|
|
||||||
lane <- racer{c, err}
|
|
||||||
} else if err == nil {
|
|
||||||
// We have to return the resources
|
|
||||||
// that belong to the other
|
|
||||||
// connections here for avoiding
|
|
||||||
// unnecessary resource starvation.
|
|
||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
}(ra)
|
|
||||||
}
|
}
|
||||||
defer close(sig)
|
}
|
||||||
lastErr := errTimeout
|
|
||||||
nracers := len(ras)
|
// dialSerial connects to a list of addresses in sequence, returning
|
||||||
for nracers > 0 {
|
// either the first successful connection, or the first error.
|
||||||
sig <- true
|
func dialSerial(ctx *dialContext, ras addrList, cancel <-chan struct{}) (Conn, error) {
|
||||||
racer := <-lane
|
var firstErr error // The error from the first address is most relevant.
|
||||||
if racer.error == nil {
|
|
||||||
return racer.Conn, nil
|
for i, ra := range ras {
|
||||||
|
select {
|
||||||
|
case <-cancel:
|
||||||
|
return nil, &OpError{Op: "dial", Net: ctx.network, Source: ctx.LocalAddr, Addr: ra, Err: errCanceled}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
lastErr = racer.error
|
|
||||||
nracers--
|
partialDeadline, err := ctx.partialDeadline(time.Now(), len(ras)-i)
|
||||||
|
if err != nil {
|
||||||
|
// Ran out of time.
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = &OpError{Op: "dial", Net: ctx.network, Source: ctx.LocalAddr, Addr: ra, Err: err}
|
||||||
}
|
}
|
||||||
return nil, lastErr
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialTCP does not support cancelation (see golang.org/issue/11225),
|
||||||
|
// so if cancel fires, we'll continue trying to connect until the next
|
||||||
|
// timeout, or return a spurious connection for the caller to close.
|
||||||
|
dialer := func(d time.Time) (Conn, error) {
|
||||||
|
return dialSingle(ctx, ra, d)
|
||||||
|
}
|
||||||
|
c, err := dial(ctx.network, ra, dialer, partialDeadline)
|
||||||
|
if err == nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = &OpError{Op: "dial", Net: ctx.network, Source: nil, Addr: nil, Err: errMissingAddress}
|
||||||
|
}
|
||||||
|
return nil, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// dialSingle attempts to establish and returns a single connection to
|
// dialSingle attempts to establish and returns a single connection to
|
||||||
// the destination address.
|
// the destination address. This must be called through the OS-specific
|
||||||
func dialSingle(net, addr string, la, ra Addr, deadline time.Time) (c Conn, err error) {
|
// dial function, because some OSes don't implement the deadline feature.
|
||||||
|
func dialSingle(ctx *dialContext, ra Addr, deadline time.Time) (c Conn, err error) {
|
||||||
|
la := ctx.LocalAddr
|
||||||
if la != nil && la.Network() != ra.Network() {
|
if la != nil && la.Network() != ra.Network() {
|
||||||
return nil, &OpError{Op: "dial", Net: net, Source: la, Addr: ra, Err: errors.New("mismatched local address type " + la.Network())}
|
return nil, &OpError{Op: "dial", Net: ctx.network, Source: la, Addr: ra, Err: errors.New("mismatched local address type " + la.Network())}
|
||||||
}
|
}
|
||||||
switch ra := ra.(type) {
|
switch ra := ra.(type) {
|
||||||
case *TCPAddr:
|
case *TCPAddr:
|
||||||
la, _ := la.(*TCPAddr)
|
la, _ := la.(*TCPAddr)
|
||||||
c, err = dialTCP(net, la, ra, deadline)
|
c, err = testHookDialTCP(ctx.network, la, ra, deadline)
|
||||||
case *UDPAddr:
|
case *UDPAddr:
|
||||||
la, _ := la.(*UDPAddr)
|
la, _ := la.(*UDPAddr)
|
||||||
c, err = dialUDP(net, la, ra, deadline)
|
c, err = dialUDP(ctx.network, la, ra, deadline)
|
||||||
case *IPAddr:
|
case *IPAddr:
|
||||||
la, _ := la.(*IPAddr)
|
la, _ := la.(*IPAddr)
|
||||||
c, err = dialIP(net, la, ra, deadline)
|
c, err = dialIP(ctx.network, la, ra, deadline)
|
||||||
case *UnixAddr:
|
case *UnixAddr:
|
||||||
la, _ := la.(*UnixAddr)
|
la, _ := la.(*UnixAddr)
|
||||||
c, err = dialUnix(net, la, ra, deadline)
|
c, err = dialUnix(ctx.network, la, ra, deadline)
|
||||||
default:
|
default:
|
||||||
return nil, &OpError{Op: "dial", Net: net, Source: la, Addr: ra, Err: &AddrError{Err: "unexpected address type", Addr: addr}}
|
return nil, &OpError{Op: "dial", Net: ctx.network, Source: la, Addr: ra, Err: &AddrError{Err: "unexpected address type", Addr: ctx.address}}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err // c is non-nil interface containing nil pointer
|
return nil, err // c is non-nil interface containing nil pointer
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package net
|
package net
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/internal/socktest"
|
"net/internal/socktest"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
@ -207,6 +208,360 @@ func TestDialerDualStackFDLeak(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define a pair of blackholed (IPv4, IPv6) addresses, for which dialTCP is
|
||||||
|
// expected to hang until the timeout elapses. These addresses are reserved
|
||||||
|
// for benchmarking by RFC 6890.
|
||||||
|
const (
|
||||||
|
slowDst4 = "192.18.0.254"
|
||||||
|
slowDst6 = "2001:2::254"
|
||||||
|
slowTimeout = 1 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// In some environments, the slow IPs may be explicitly unreachable, and fail
|
||||||
|
// more quickly than expected. This test hook prevents dialTCP from returning
|
||||||
|
// before the deadline.
|
||||||
|
func slowDialTCP(net string, laddr, raddr *TCPAddr, deadline time.Time) (*TCPConn, error) {
|
||||||
|
c, err := dialTCP(net, laddr, raddr, deadline)
|
||||||
|
if ParseIP(slowDst4).Equal(raddr.IP) || ParseIP(slowDst6).Equal(raddr.IP) {
|
||||||
|
time.Sleep(deadline.Sub(time.Now()))
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialClosedPort() time.Duration {
|
||||||
|
l, err := Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return 999 * time.Hour
|
||||||
|
}
|
||||||
|
addr := l.Addr().String()
|
||||||
|
l.Close()
|
||||||
|
// On OpenBSD, interference from TestSelfConnect is mysteriously
|
||||||
|
// causing the first attempt to hang for a few seconds, so we throw
|
||||||
|
// away the first result and keep the second.
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
startTime := time.Now()
|
||||||
|
c, err := Dial("tcp", addr)
|
||||||
|
if err == nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
elapsed := time.Now().Sub(startTime)
|
||||||
|
if i == 2 {
|
||||||
|
return elapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialParallel(t *testing.T) {
|
||||||
|
if testing.Short() || !*testExternal {
|
||||||
|
t.Skip("avoid external network")
|
||||||
|
}
|
||||||
|
if !supportsIPv4 || !supportsIPv6 {
|
||||||
|
t.Skip("both IPv4 and IPv6 are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the time required to dial a closed port.
|
||||||
|
// On Windows, this takes roughly 1 second, but other platforms
|
||||||
|
// are expected to be instantaneous.
|
||||||
|
closedPortDelay := dialClosedPort()
|
||||||
|
var expectClosedPortDelay time.Duration
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
expectClosedPortDelay = 1095 * time.Millisecond
|
||||||
|
} else {
|
||||||
|
expectClosedPortDelay = 95 * time.Millisecond
|
||||||
|
}
|
||||||
|
if closedPortDelay > expectClosedPortDelay {
|
||||||
|
t.Errorf("got %v; want <= %v", closedPortDelay, expectClosedPortDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instant time.Duration = 0
|
||||||
|
const fallbackDelay = 200 * time.Millisecond
|
||||||
|
|
||||||
|
// Some cases will run quickly when "connection refused" is fast,
|
||||||
|
// or trigger the fallbackDelay on Windows. This value holds the
|
||||||
|
// lesser of the two delays.
|
||||||
|
var closedPortOrFallbackDelay time.Duration
|
||||||
|
if closedPortDelay < fallbackDelay {
|
||||||
|
closedPortOrFallbackDelay = closedPortDelay
|
||||||
|
} else {
|
||||||
|
closedPortOrFallbackDelay = fallbackDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
origTestHookDialTCP := testHookDialTCP
|
||||||
|
defer func() { testHookDialTCP = origTestHookDialTCP }()
|
||||||
|
testHookDialTCP = slowDialTCP
|
||||||
|
|
||||||
|
nCopies := func(s string, n int) []string {
|
||||||
|
out := make([]string, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
out[i] = s
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
primaries []string
|
||||||
|
fallbacks []string
|
||||||
|
teardownNetwork string
|
||||||
|
expectOk bool
|
||||||
|
expectElapsed time.Duration
|
||||||
|
}{
|
||||||
|
// These should just work on the first try.
|
||||||
|
{[]string{"127.0.0.1"}, []string{}, "", true, instant},
|
||||||
|
{[]string{"::1"}, []string{}, "", true, instant},
|
||||||
|
{[]string{"127.0.0.1", "::1"}, []string{slowDst6}, "tcp6", true, instant},
|
||||||
|
{[]string{"::1", "127.0.0.1"}, []string{slowDst4}, "tcp4", true, instant},
|
||||||
|
// Primary is slow; fallback should kick in.
|
||||||
|
{[]string{slowDst4}, []string{"::1"}, "", true, fallbackDelay},
|
||||||
|
// Skip a "connection refused" in the primary thread.
|
||||||
|
{[]string{"127.0.0.1", "::1"}, []string{}, "tcp4", true, closedPortDelay},
|
||||||
|
{[]string{"::1", "127.0.0.1"}, []string{}, "tcp6", true, closedPortDelay},
|
||||||
|
// Skip a "connection refused" in the fallback thread.
|
||||||
|
{[]string{slowDst4, slowDst6}, []string{"::1", "127.0.0.1"}, "tcp6", true, fallbackDelay + closedPortDelay},
|
||||||
|
// Primary refused, fallback without delay.
|
||||||
|
{[]string{"127.0.0.1"}, []string{"::1"}, "tcp4", true, closedPortOrFallbackDelay},
|
||||||
|
{[]string{"::1"}, []string{"127.0.0.1"}, "tcp6", true, closedPortOrFallbackDelay},
|
||||||
|
// Everything is refused.
|
||||||
|
{[]string{"127.0.0.1"}, []string{}, "tcp4", false, closedPortDelay},
|
||||||
|
// Nothing to do; fail instantly.
|
||||||
|
{[]string{}, []string{}, "", false, instant},
|
||||||
|
// Connecting to tons of addresses should not trip the deadline.
|
||||||
|
{nCopies("::1", 1000), []string{}, "", true, instant},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := func(dss *dualStackServer, ln Listener) {
|
||||||
|
for {
|
||||||
|
c, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a list of IP strings into TCPAddrs.
|
||||||
|
makeAddrs := func(ips []string, port string) addrList {
|
||||||
|
var out addrList
|
||||||
|
for _, ip := range ips {
|
||||||
|
addr, err := ResolveTCPAddr("tcp", JoinHostPort(ip, port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out = append(out, addr)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range testCases {
|
||||||
|
dss, err := newDualStackServer([]streamListener{
|
||||||
|
{network: "tcp4", address: "127.0.0.1"},
|
||||||
|
{network: "tcp6", address: "::1"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dss.teardown()
|
||||||
|
if err := dss.buildup(handler); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if tt.teardownNetwork != "" {
|
||||||
|
// Destroy one of the listening sockets, creating an unreachable port.
|
||||||
|
dss.teardownNetwork(tt.teardownNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
primaries := makeAddrs(tt.primaries, dss.port)
|
||||||
|
fallbacks := makeAddrs(tt.fallbacks, dss.port)
|
||||||
|
ctx := &dialContext{
|
||||||
|
Dialer: Dialer{
|
||||||
|
FallbackDelay: fallbackDelay,
|
||||||
|
Timeout: slowTimeout,
|
||||||
|
},
|
||||||
|
network: "tcp",
|
||||||
|
address: "?",
|
||||||
|
}
|
||||||
|
startTime := time.Now()
|
||||||
|
c, err := dialParallel(ctx, primaries, fallbacks)
|
||||||
|
elapsed := time.Now().Sub(startTime)
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectOk && err != nil {
|
||||||
|
t.Errorf("#%d: got %v; want nil", i, err)
|
||||||
|
} else if !tt.expectOk && err == nil {
|
||||||
|
t.Errorf("#%d: got nil; want non-nil", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectElapsedMin := tt.expectElapsed - 95*time.Millisecond
|
||||||
|
expectElapsedMax := tt.expectElapsed + 95*time.Millisecond
|
||||||
|
if !(elapsed >= expectElapsedMin) {
|
||||||
|
t.Errorf("#%d: got %v; want >= %v", i, elapsed, expectElapsedMin)
|
||||||
|
} else if !(elapsed <= expectElapsedMax) {
|
||||||
|
t.Errorf("#%d: got %v; want <= %v", i, elapsed, expectElapsedMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wait for any slowDst4/slowDst6 connections to timeout.
|
||||||
|
time.Sleep(slowTimeout * 3 / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupSlowFast(fn func(string) ([]IPAddr, error), host string) ([]IPAddr, error) {
|
||||||
|
switch host {
|
||||||
|
case "slow6loopback4":
|
||||||
|
// Returns a slow IPv6 address, and a local IPv4 address.
|
||||||
|
return []IPAddr{
|
||||||
|
{IP: ParseIP(slowDst6)},
|
||||||
|
{IP: ParseIP("127.0.0.1")},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return fn(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialerFallbackDelay(t *testing.T) {
|
||||||
|
if testing.Short() || !*testExternal {
|
||||||
|
t.Skip("avoid external network")
|
||||||
|
}
|
||||||
|
if !supportsIPv4 || !supportsIPv6 {
|
||||||
|
t.Skip("both IPv4 and IPv6 are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
origTestHookLookupIP := testHookLookupIP
|
||||||
|
defer func() { testHookLookupIP = origTestHookLookupIP }()
|
||||||
|
testHookLookupIP = lookupSlowFast
|
||||||
|
|
||||||
|
origTestHookDialTCP := testHookDialTCP
|
||||||
|
defer func() { testHookDialTCP = origTestHookDialTCP }()
|
||||||
|
testHookDialTCP = slowDialTCP
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
dualstack bool
|
||||||
|
delay time.Duration
|
||||||
|
expectElapsed time.Duration
|
||||||
|
}{
|
||||||
|
// Use a very brief delay, which should fallback immediately.
|
||||||
|
{true, 1 * time.Nanosecond, 0},
|
||||||
|
// Use a 200ms explicit timeout.
|
||||||
|
{true, 200 * time.Millisecond, 200 * time.Millisecond},
|
||||||
|
// The default is 300ms.
|
||||||
|
{true, 0, 300 * time.Millisecond},
|
||||||
|
// This case is last, in order to wait for hanging slowDst6 connections.
|
||||||
|
{false, 0, slowTimeout},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := func(dss *dualStackServer, ln Listener) {
|
||||||
|
for {
|
||||||
|
c, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dss, err := newDualStackServer([]streamListener{
|
||||||
|
{network: "tcp", address: "127.0.0.1"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer dss.teardown()
|
||||||
|
if err := dss.buildup(handler); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range testCases {
|
||||||
|
d := &Dialer{DualStack: tt.dualstack, FallbackDelay: tt.delay, Timeout: slowTimeout}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
c, err := d.Dial("tcp", JoinHostPort("slow6loopback4", dss.port))
|
||||||
|
elapsed := time.Now().Sub(startTime)
|
||||||
|
if err == nil {
|
||||||
|
c.Close()
|
||||||
|
} else if tt.dualstack {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
expectMin := tt.expectElapsed - 1*time.Millisecond
|
||||||
|
expectMax := tt.expectElapsed + 95*time.Millisecond
|
||||||
|
if !(elapsed >= expectMin) {
|
||||||
|
t.Errorf("#%d: got %v; want >= %v", i, elapsed, expectMin)
|
||||||
|
}
|
||||||
|
if !(elapsed <= expectMax) {
|
||||||
|
t.Errorf("#%d: got %v; want <= %v", i, elapsed, expectMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialSerialAsyncSpuriousConnection(t *testing.T) {
|
||||||
|
ln, err := newLocalListener("tcp")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
ctx := &dialContext{
|
||||||
|
network: "tcp",
|
||||||
|
address: "?",
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(chan dialResult)
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
|
||||||
|
// Spawn a connection in the background.
|
||||||
|
go dialSerialAsync(ctx, addrList{ln.Addr()}, nil, cancel, results)
|
||||||
|
|
||||||
|
// Receive it at the server.
|
||||||
|
c, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// Tell dialSerialAsync that someone else won the race.
|
||||||
|
close(cancel)
|
||||||
|
|
||||||
|
// The connection should close itself, without sending data.
|
||||||
|
c.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
var b [1]byte
|
||||||
|
if _, err := c.Read(b[:]); err != io.EOF {
|
||||||
|
t.Errorf("got %v; want %v", err, io.EOF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDialerPartialDeadline(t *testing.T) {
|
||||||
|
now := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
var testCases = []struct {
|
||||||
|
now time.Time
|
||||||
|
deadline time.Time
|
||||||
|
addrs int
|
||||||
|
expectDeadline time.Time
|
||||||
|
expectErr error
|
||||||
|
}{
|
||||||
|
// Regular division.
|
||||||
|
{now, now.Add(12 * time.Second), 1, now.Add(12 * time.Second), nil},
|
||||||
|
{now, now.Add(12 * time.Second), 2, now.Add(6 * time.Second), nil},
|
||||||
|
{now, now.Add(12 * time.Second), 3, now.Add(4 * time.Second), nil},
|
||||||
|
// Bump against the 2-second sane minimum.
|
||||||
|
{now, now.Add(12 * time.Second), 999, now.Add(2 * time.Second), nil},
|
||||||
|
// Total available is now below the sane minimum.
|
||||||
|
{now, now.Add(1900 * time.Millisecond), 999, now.Add(1900 * time.Millisecond), nil},
|
||||||
|
// Null deadline.
|
||||||
|
{now, noDeadline, 1, noDeadline, nil},
|
||||||
|
// Step the clock forward and cross the deadline.
|
||||||
|
{now.Add(-1 * time.Millisecond), now, 1, now, nil},
|
||||||
|
{now.Add(0 * time.Millisecond), now, 1, noDeadline, errTimeout},
|
||||||
|
{now.Add(1 * time.Millisecond), now, 1, noDeadline, errTimeout},
|
||||||
|
}
|
||||||
|
for i, tt := range testCases {
|
||||||
|
d := Dialer{Deadline: tt.deadline}
|
||||||
|
deadline, err := d.partialDeadline(tt.now, tt.addrs)
|
||||||
|
if err != tt.expectErr {
|
||||||
|
t.Errorf("#%d: got %v; want %v", i, err, tt.expectErr)
|
||||||
|
}
|
||||||
|
if deadline != tt.expectDeadline {
|
||||||
|
t.Errorf("#%d: got %v; want %v", i, deadline, tt.expectDeadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDialerLocalAddr(t *testing.T) {
|
func TestDialerLocalAddr(t *testing.T) {
|
||||||
ch := make(chan error, 1)
|
ch := make(chan error, 1)
|
||||||
handler := func(ls *localServer, ln Listener) {
|
handler := func(ls *localServer, ln Listener) {
|
||||||
@ -262,6 +617,9 @@ func TestDialerDualStack(t *testing.T) {
|
|||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const T = 100 * time.Millisecond
|
||||||
|
for _, dualstack := range []bool{false, true} {
|
||||||
dss, err := newDualStackServer([]streamListener{
|
dss, err := newDualStackServer([]streamListener{
|
||||||
{network: "tcp4", address: "127.0.0.1"},
|
{network: "tcp4", address: "127.0.0.1"},
|
||||||
{network: "tcp6", address: "::1"},
|
{network: "tcp6", address: "::1"},
|
||||||
@ -274,8 +632,7 @@ func TestDialerDualStack(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
const T = 100 * time.Millisecond
|
d := &Dialer{DualStack: dualstack, Timeout: T}
|
||||||
d := &Dialer{DualStack: true, Timeout: T}
|
|
||||||
for range dss.lns {
|
for range dss.lns {
|
||||||
c, err := d.Dial("tcp", JoinHostPort("localhost", dss.port))
|
c, err := d.Dial("tcp", JoinHostPort("localhost", dss.port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -290,6 +647,7 @@ func TestDialerDualStack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
c.Close()
|
c.Close()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
time.Sleep(2 * T) // wait for the dial racers to stop
|
time.Sleep(2 * T) // wait for the dial racers to stop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package net
|
package net
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
testHookDialTCP = dialTCP
|
||||||
testHookHostsPath = "/etc/hosts"
|
testHookHostsPath = "/etc/hosts"
|
||||||
testHookLookupIP = func(fn func(string) ([]IPAddr, error), host string) ([]IPAddr, error) { return fn(host) }
|
testHookLookupIP = func(fn func(string) ([]IPAddr, error), host string) ([]IPAddr, error) { return fn(host) }
|
||||||
testHookSetKeepAlive = func() {}
|
testHookSetKeepAlive = func() {}
|
||||||
|
@ -321,6 +321,7 @@ var (
|
|||||||
|
|
||||||
// For both read and write operations.
|
// For both read and write operations.
|
||||||
errTimeout error = &timeoutError{}
|
errTimeout error = &timeoutError{}
|
||||||
|
errCanceled = errors.New("operation was canceled")
|
||||||
errClosing = errors.New("use of closed network connection")
|
errClosing = errors.New("use of closed network connection")
|
||||||
ErrWriteToConnected = errors.New("use of WriteTo with pre-connected connection")
|
ErrWriteToConnected = errors.New("use of WriteTo with pre-connected connection")
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user