mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00: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
4 changed files with 568 additions and 89 deletions
|
|
@ -5,6 +5,7 @@
|
|||
package net
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/internal/socktest"
|
||||
"runtime"
|
||||
"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) {
|
||||
ch := make(chan error, 1)
|
||||
handler := func(ls *localServer, ln Listener) {
|
||||
|
|
@ -262,33 +617,36 @@ func TestDialerDualStack(t *testing.T) {
|
|||
c.Close()
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
const T = 100 * time.Millisecond
|
||||
d := &Dialer{DualStack: true, Timeout: T}
|
||||
for range dss.lns {
|
||||
c, err := d.Dial("tcp", JoinHostPort("localhost", dss.port))
|
||||
for _, dualstack := range []bool{false, true} {
|
||||
dss, err := newDualStackServer([]streamListener{
|
||||
{network: "tcp4", address: "127.0.0.1"},
|
||||
{network: "tcp6", address: "::1"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch addr := c.LocalAddr().(*TCPAddr); {
|
||||
case addr.IP.To4() != nil:
|
||||
dss.teardownNetwork("tcp4")
|
||||
case addr.IP.To16() != nil && addr.IP.To4() == nil:
|
||||
dss.teardownNetwork("tcp6")
|
||||
defer dss.teardown()
|
||||
if err := dss.buildup(handler); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
d := &Dialer{DualStack: dualstack, Timeout: T}
|
||||
for range dss.lns {
|
||||
c, err := d.Dial("tcp", JoinHostPort("localhost", dss.port))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
switch addr := c.LocalAddr().(*TCPAddr); {
|
||||
case addr.IP.To4() != nil:
|
||||
dss.teardownNetwork("tcp4")
|
||||
case addr.IP.To16() != nil && addr.IP.To4() == nil:
|
||||
dss.teardownNetwork("tcp6")
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
time.Sleep(2 * T) // wait for the dial racers to stop
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue