diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:25:22 +0000 |
commit | f6ad4dcef54c5ce997a4bad5a6d86de229015700 (patch) | |
tree | 7cfa4e31ace5c2bd95c72b154d15af494b2bcbef /src/net/dial_test.go | |
parent | Initial commit. (diff) | |
download | golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.tar.xz golang-1.22-f6ad4dcef54c5ce997a4bad5a6d86de229015700.zip |
Adding upstream version 1.22.1.upstream/1.22.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/net/dial_test.go')
-rw-r--r-- | src/net/dial_test.go | 1101 |
1 files changed, 1101 insertions, 0 deletions
diff --git a/src/net/dial_test.go b/src/net/dial_test.go new file mode 100644 index 0000000..1d0832e --- /dev/null +++ b/src/net/dial_test.go @@ -0,0 +1,1101 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package net + +import ( + "bufio" + "context" + "errors" + "fmt" + "internal/testenv" + "io" + "os" + "runtime" + "strings" + "sync" + "syscall" + "testing" + "time" +) + +var prohibitionaryDialArgTests = []struct { + network string + address string +}{ + {"tcp6", "127.0.0.1"}, + {"tcp6", "::ffff:127.0.0.1"}, +} + +func TestProhibitionaryDialArg(t *testing.T) { + testenv.MustHaveExternalNetwork(t) + + switch runtime.GOOS { + case "plan9": + t.Skipf("not supported on %s", runtime.GOOS) + } + if !supportsIPv4map() { + t.Skip("mapping ipv4 address inside ipv6 address not supported") + } + + ln, err := Listen("tcp", "[::]:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + _, port, err := SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + + for i, tt := range prohibitionaryDialArgTests { + c, err := Dial(tt.network, JoinHostPort(tt.address, port)) + if err == nil { + c.Close() + t.Errorf("#%d: %v", i, err) + } + } +} + +func TestDialLocal(t *testing.T) { + ln := newLocalListener(t, "tcp") + defer ln.Close() + _, port, err := SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + c, err := Dial("tcp", JoinHostPort("", port)) + if err != nil { + t.Fatal(err) + } + c.Close() +} + +func TestDialerDualStackFDLeak(t *testing.T) { + switch runtime.GOOS { + case "plan9": + t.Skipf("%s does not have full support of socktest", runtime.GOOS) + case "windows": + t.Skipf("not implemented a way to cancel dial racers in TCP SYN-SENT state on %s", runtime.GOOS) + case "openbsd": + testenv.SkipFlaky(t, 15157) + } + if !supportsIPv4() || !supportsIPv6() { + t.Skip("both IPv4 and IPv6 are required") + } + + before := sw.Sockets() + origTestHookLookupIP := testHookLookupIP + defer func() { testHookLookupIP = origTestHookLookupIP }() + testHookLookupIP = lookupLocalhost + handler := func(dss *dualStackServer, ln Listener) { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + } + dss, err := newDualStackServer() + if err != nil { + t.Fatal(err) + } + if err := dss.buildup(handler); err != nil { + dss.teardown() + t.Fatal(err) + } + + const N = 10 + var wg sync.WaitGroup + wg.Add(N) + d := &Dialer{DualStack: true, Timeout: 5 * time.Second} + for i := 0; i < N; i++ { + go func() { + defer wg.Done() + c, err := d.Dial("tcp", JoinHostPort("localhost", dss.port)) + if err != nil { + t.Error(err) + return + } + c.Close() + }() + } + wg.Wait() + dss.teardown() + after := sw.Sockets() + if len(after) != len(before) { + t.Errorf("got %d; want %d", len(after), len(before)) + } +} + +// 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 = "198.18.0.254" + slowDst6 = "2001:2::254" +) + +// 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(ctx context.Context, network string, laddr, raddr *TCPAddr) (*TCPConn, error) { + sd := &sysDialer{network: network, address: raddr.String()} + c, err := sd.doDialTCP(ctx, laddr, raddr) + if ParseIP(slowDst4).Equal(raddr.IP) || ParseIP(slowDst6).Equal(raddr.IP) { + // Wait for the deadline, or indefinitely if none exists. + <-ctx.Done() + } + return c, err +} + +func dialClosedPort(t *testing.T) (dialLatency time.Duration) { + // On most platforms, dialing a closed port should be nearly instantaneous — + // less than a few hundred milliseconds. However, on some platforms it may be + // much slower: on Windows and OpenBSD, it has been observed to take up to a + // few seconds. + + l, err := Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("dialClosedPort: Listen failed: %v", err) + } + addr := l.Addr().String() + l.Close() + + startTime := time.Now() + c, err := Dial("tcp", addr) + if err == nil { + c.Close() + } + elapsed := time.Since(startTime) + t.Logf("dialClosedPort: measured delay %v", elapsed) + return elapsed +} + +func TestDialParallel(t *testing.T) { + const instant time.Duration = 0 + const fallbackDelay = 200 * time.Millisecond + + 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, instant}, + {[]string{"::1", "127.0.0.1"}, []string{}, "tcp6", true, instant}, + // Skip a "connection refused" in the fallback thread. + {[]string{slowDst4, slowDst6}, []string{"::1", "127.0.0.1"}, "tcp6", true, fallbackDelay}, + // Primary refused, fallback without delay. + {[]string{"127.0.0.1"}, []string{"::1"}, "tcp4", true, instant}, + {[]string{"::1"}, []string{"127.0.0.1"}, "tcp6", true, instant}, + // Everything is refused. + {[]string{"127.0.0.1"}, []string{}, "tcp4", false, instant}, + // 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}, + } + + // 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 { + i, tt := i, tt + t.Run(fmt.Sprint(i), func(t *testing.T) { + dialTCP := func(ctx context.Context, network string, laddr, raddr *TCPAddr) (*TCPConn, error) { + n := "tcp6" + if raddr.IP.To4() != nil { + n = "tcp4" + } + if n == tt.teardownNetwork { + return nil, errors.New("unreachable") + } + if r := raddr.IP.String(); r == slowDst4 || r == slowDst6 { + <-ctx.Done() + return nil, ctx.Err() + } + return &TCPConn{}, nil + } + + primaries := makeAddrs(tt.primaries, "80") + fallbacks := makeAddrs(tt.fallbacks, "80") + d := Dialer{ + FallbackDelay: fallbackDelay, + } + const forever = 60 * time.Minute + if tt.expectElapsed == instant { + d.FallbackDelay = forever + } + startTime := time.Now() + sd := &sysDialer{ + Dialer: d, + network: "tcp", + address: "?", + testHookDialTCP: dialTCP, + } + c, err := sd.dialParallel(context.Background(), primaries, fallbacks) + elapsed := time.Since(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) + } + + if elapsed < tt.expectElapsed || elapsed >= forever { + t.Errorf("#%d: got %v; want >= %v, < forever", i, elapsed, tt.expectElapsed) + } + + // Repeat each case, ensuring that it can be canceled. + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + wg.Add(1) + go func() { + time.Sleep(5 * time.Millisecond) + cancel() + wg.Done() + }() + // Ignore errors, since all we care about is that the + // call can be canceled. + c, _ = sd.dialParallel(ctx, primaries, fallbacks) + if c != nil { + c.Close() + } + wg.Wait() + }) + } +} + +func lookupSlowFast(ctx context.Context, fn func(context.Context, string, string) ([]IPAddr, error), network, 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(ctx, network, host) + } +} + +func TestDialerFallbackDelay(t *testing.T) { + testenv.MustHaveExternalNetwork(t) + + 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}, + } + + handler := func(dss *dualStackServer, ln Listener) { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + } + dss, err := newDualStackServer() + 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} + + startTime := time.Now() + c, err := d.Dial("tcp", JoinHostPort("slow6loopback4", dss.port)) + elapsed := time.Since(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 TestDialParallelSpuriousConnection(t *testing.T) { + if !supportsIPv4() || !supportsIPv6() { + t.Skip("both IPv4 and IPv6 are required") + } + + var readDeadline time.Time + if td, ok := t.Deadline(); ok { + const arbitraryCleanupMargin = 1 * time.Second + readDeadline = td.Add(-arbitraryCleanupMargin) + } else { + readDeadline = time.Now().Add(5 * time.Second) + } + + var closed sync.WaitGroup + closed.Add(2) + handler := func(dss *dualStackServer, ln Listener) { + // Accept one connection per address. + c, err := ln.Accept() + if err != nil { + t.Fatal(err) + } + + // Workaround for https://go.dev/issue/37795. + // On arm64 macOS (current as of macOS 12.4), + // reading from a socket at the same time as the client + // is closing it occasionally hangs for 60 seconds before + // returning ECONNRESET. Sleep for a bit to give the + // socket time to close before trying to read from it. + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + time.Sleep(10 * time.Millisecond) + } + + // The client should close itself, without sending data. + c.SetReadDeadline(readDeadline) + var b [1]byte + if _, err := c.Read(b[:]); err != io.EOF { + t.Errorf("got %v; want %v", err, io.EOF) + } + c.Close() + closed.Done() + } + dss, err := newDualStackServer() + if err != nil { + t.Fatal(err) + } + defer dss.teardown() + if err := dss.buildup(handler); err != nil { + t.Fatal(err) + } + + const fallbackDelay = 100 * time.Millisecond + + var dialing sync.WaitGroup + dialing.Add(2) + origTestHookDialTCP := testHookDialTCP + defer func() { testHookDialTCP = origTestHookDialTCP }() + testHookDialTCP = func(ctx context.Context, net string, laddr, raddr *TCPAddr) (*TCPConn, error) { + // Wait until Happy Eyeballs kicks in and both connections are dialing, + // and inhibit cancellation. + // This forces dialParallel to juggle two successful connections. + dialing.Done() + dialing.Wait() + + // Now ignore the provided context (which will be canceled) and use a + // different one to make sure this completes with a valid connection, + // which we hope to be closed below: + sd := &sysDialer{network: net, address: raddr.String()} + return sd.doDialTCP(context.Background(), laddr, raddr) + } + + d := Dialer{ + FallbackDelay: fallbackDelay, + } + sd := &sysDialer{ + Dialer: d, + network: "tcp", + address: "?", + } + + makeAddr := func(ip string) addrList { + addr, err := ResolveTCPAddr("tcp", JoinHostPort(ip, dss.port)) + if err != nil { + t.Fatal(err) + } + return addrList{addr} + } + + // dialParallel returns one connection (and closes the other.) + c, err := sd.dialParallel(context.Background(), makeAddr("127.0.0.1"), makeAddr("::1")) + if err != nil { + t.Fatal(err) + } + c.Close() + + // The server should've seen both connections. + closed.Wait() +} + +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 { + deadline, err := partialDeadline(tt.now, tt.deadline, tt.addrs) + if err != tt.expectErr { + t.Errorf("#%d: got %v; want %v", i, err, tt.expectErr) + } + if !deadline.Equal(tt.expectDeadline) { + t.Errorf("#%d: got %v; want %v", i, deadline, tt.expectDeadline) + } + } +} + +// isEADDRINUSE reports whether err is syscall.EADDRINUSE. +var isEADDRINUSE = func(err error) bool { return false } + +func TestDialerLocalAddr(t *testing.T) { + if !supportsIPv4() || !supportsIPv6() { + t.Skip("both IPv4 and IPv6 are required") + } + + type test struct { + network, raddr string + laddr Addr + error + } + var tests = []test{ + {"tcp4", "127.0.0.1", nil, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{}, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: ParseIP("0.0.0.0")}, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: ParseIP("0.0.0.0").To4()}, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: ParseIP("::")}, &AddrError{Err: "some error"}}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: ParseIP("127.0.0.1").To4()}, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: ParseIP("127.0.0.1").To16()}, nil}, + {"tcp4", "127.0.0.1", &TCPAddr{IP: IPv6loopback}, errNoSuitableAddress}, + {"tcp4", "127.0.0.1", &UDPAddr{}, &AddrError{Err: "some error"}}, + {"tcp4", "127.0.0.1", &UnixAddr{}, &AddrError{Err: "some error"}}, + + {"tcp6", "::1", nil, nil}, + {"tcp6", "::1", &TCPAddr{}, nil}, + {"tcp6", "::1", &TCPAddr{IP: ParseIP("0.0.0.0")}, nil}, + {"tcp6", "::1", &TCPAddr{IP: ParseIP("0.0.0.0").To4()}, nil}, + {"tcp6", "::1", &TCPAddr{IP: ParseIP("::")}, nil}, + {"tcp6", "::1", &TCPAddr{IP: ParseIP("127.0.0.1").To4()}, errNoSuitableAddress}, + {"tcp6", "::1", &TCPAddr{IP: ParseIP("127.0.0.1").To16()}, errNoSuitableAddress}, + {"tcp6", "::1", &TCPAddr{IP: IPv6loopback}, nil}, + {"tcp6", "::1", &UDPAddr{}, &AddrError{Err: "some error"}}, + {"tcp6", "::1", &UnixAddr{}, &AddrError{Err: "some error"}}, + + {"tcp", "127.0.0.1", nil, nil}, + {"tcp", "127.0.0.1", &TCPAddr{}, nil}, + {"tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("0.0.0.0")}, nil}, + {"tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("0.0.0.0").To4()}, nil}, + {"tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("127.0.0.1").To4()}, nil}, + {"tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("127.0.0.1").To16()}, nil}, + {"tcp", "127.0.0.1", &TCPAddr{IP: IPv6loopback}, errNoSuitableAddress}, + {"tcp", "127.0.0.1", &UDPAddr{}, &AddrError{Err: "some error"}}, + {"tcp", "127.0.0.1", &UnixAddr{}, &AddrError{Err: "some error"}}, + + {"tcp", "::1", nil, nil}, + {"tcp", "::1", &TCPAddr{}, nil}, + {"tcp", "::1", &TCPAddr{IP: ParseIP("0.0.0.0")}, nil}, + {"tcp", "::1", &TCPAddr{IP: ParseIP("0.0.0.0").To4()}, nil}, + {"tcp", "::1", &TCPAddr{IP: ParseIP("::")}, nil}, + {"tcp", "::1", &TCPAddr{IP: ParseIP("127.0.0.1").To4()}, errNoSuitableAddress}, + {"tcp", "::1", &TCPAddr{IP: ParseIP("127.0.0.1").To16()}, errNoSuitableAddress}, + {"tcp", "::1", &TCPAddr{IP: IPv6loopback}, nil}, + {"tcp", "::1", &UDPAddr{}, &AddrError{Err: "some error"}}, + {"tcp", "::1", &UnixAddr{}, &AddrError{Err: "some error"}}, + } + + issue34264Index := -1 + if supportsIPv4map() { + issue34264Index = len(tests) + tests = append(tests, test{ + "tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("::")}, nil, + }) + } else { + tests = append(tests, test{ + "tcp", "127.0.0.1", &TCPAddr{IP: ParseIP("::")}, &AddrError{Err: "some error"}, + }) + } + + origTestHookLookupIP := testHookLookupIP + defer func() { testHookLookupIP = origTestHookLookupIP }() + testHookLookupIP = lookupLocalhost + handler := func(ls *localServer, ln Listener) { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + } + var lss [2]*localServer + for i, network := range []string{"tcp4", "tcp6"} { + lss[i] = newLocalServer(t, network) + defer lss[i].teardown() + if err := lss[i].buildup(handler); err != nil { + t.Fatal(err) + } + } + + for i, tt := range tests { + d := &Dialer{LocalAddr: tt.laddr} + var addr string + ip := ParseIP(tt.raddr) + if ip.To4() != nil { + addr = lss[0].Listener.Addr().String() + } + if ip.To16() != nil && ip.To4() == nil { + addr = lss[1].Listener.Addr().String() + } + c, err := d.Dial(tt.network, addr) + if err == nil && tt.error != nil || err != nil && tt.error == nil { + if i == issue34264Index && runtime.GOOS == "freebsd" && isEADDRINUSE(err) { + // https://golang.org/issue/34264: FreeBSD through at least version 12.2 + // has been observed to fail with EADDRINUSE when dialing from an IPv6 + // local address to an IPv4 remote address. + t.Logf("%s %v->%s: got %v; want %v", tt.network, tt.laddr, tt.raddr, err, tt.error) + t.Logf("(spurious EADDRINUSE ignored on freebsd: see https://golang.org/issue/34264)") + } else { + t.Errorf("%s %v->%s: got %v; want %v", tt.network, tt.laddr, tt.raddr, err, tt.error) + } + } + if err != nil { + if perr := parseDialError(err); perr != nil { + t.Error(perr) + } + continue + } + c.Close() + } +} + +func TestDialerDualStack(t *testing.T) { + testenv.SkipFlaky(t, 13324) + + if !supportsIPv4() || !supportsIPv6() { + t.Skip("both IPv4 and IPv6 are required") + } + + closedPortDelay := dialClosedPort(t) + + origTestHookLookupIP := testHookLookupIP + defer func() { testHookLookupIP = origTestHookLookupIP }() + testHookLookupIP = lookupLocalhost + handler := func(dss *dualStackServer, ln Listener) { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + } + + var timeout = 150*time.Millisecond + closedPortDelay + for _, dualstack := range []bool{false, true} { + dss, err := newDualStackServer() + if err != nil { + t.Fatal(err) + } + defer dss.teardown() + if err := dss.buildup(handler); err != nil { + t.Fatal(err) + } + + d := &Dialer{DualStack: dualstack, Timeout: timeout} + 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() + } + } +} + +func TestDialerKeepAlive(t *testing.T) { + handler := func(ls *localServer, ln Listener) { + for { + c, err := ln.Accept() + if err != nil { + return + } + c.Close() + } + } + ls := newLocalServer(t, "tcp") + defer ls.teardown() + if err := ls.buildup(handler); err != nil { + t.Fatal(err) + } + defer func() { testHookSetKeepAlive = func(time.Duration) {} }() + + tests := []struct { + ka time.Duration + expected time.Duration + }{ + {-1, -1}, + {0, 15 * time.Second}, + {5 * time.Second, 5 * time.Second}, + {30 * time.Second, 30 * time.Second}, + } + + for _, test := range tests { + var got time.Duration = -1 + testHookSetKeepAlive = func(d time.Duration) { got = d } + d := Dialer{KeepAlive: test.ka} + c, err := d.Dial("tcp", ls.Listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + c.Close() + if got != test.expected { + t.Errorf("Dialer.KeepAlive = %v: SetKeepAlive set to %v, want %v", d.KeepAlive, got, test.expected) + } + } +} + +func TestDialCancel(t *testing.T) { + mustHaveExternalNetwork(t) + + blackholeIPPort := JoinHostPort(slowDst4, "1234") + if !supportsIPv4() { + blackholeIPPort = JoinHostPort(slowDst6, "1234") + } + + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + const cancelTick = 5 // the timer tick we cancel the dial at + const timeoutTick = 100 + + var d Dialer + cancel := make(chan struct{}) + d.Cancel = cancel + errc := make(chan error, 1) + connc := make(chan Conn, 1) + go func() { + if c, err := d.Dial("tcp", blackholeIPPort); err != nil { + errc <- err + } else { + connc <- c + } + }() + ticks := 0 + for { + select { + case <-ticker.C: + ticks++ + if ticks == cancelTick { + close(cancel) + } + if ticks == timeoutTick { + t.Fatal("timeout waiting for dial to fail") + } + case c := <-connc: + c.Close() + t.Fatal("unexpected successful connection") + case err := <-errc: + if perr := parseDialError(err); perr != nil { + t.Error(perr) + } + if ticks < cancelTick { + // Using strings.Contains is ugly but + // may work on plan9 and windows. + ignorable := []string{ + "connection refused", + "unreachable", + "no route to host", + "invalid argument", + } + e := err.Error() + for _, ignore := range ignorable { + if strings.Contains(e, ignore) { + t.Skipf("connection to %v failed fast with %v", blackholeIPPort, err) + } + } + + t.Fatalf("dial error after %d ticks (%d before cancel sent): %v", + ticks, cancelTick-ticks, err) + } + if oe, ok := err.(*OpError); !ok || oe.Err != errCanceled { + t.Fatalf("dial error = %v (%T); want OpError with Err == errCanceled", err, err) + } + return // success. + } + } +} + +func TestCancelAfterDial(t *testing.T) { + if testing.Short() { + t.Skip("avoiding time.Sleep") + } + + ln := newLocalListener(t, "tcp") + + var wg sync.WaitGroup + wg.Add(1) + defer func() { + ln.Close() + wg.Wait() + }() + + // Echo back the first line of each incoming connection. + go func() { + for { + c, err := ln.Accept() + if err != nil { + break + } + rb := bufio.NewReader(c) + line, err := rb.ReadString('\n') + if err != nil { + t.Error(err) + c.Close() + continue + } + if _, err := c.Write([]byte(line)); err != nil { + t.Error(err) + } + c.Close() + } + wg.Done() + }() + + try := func() { + cancel := make(chan struct{}) + d := &Dialer{Cancel: cancel} + c, err := d.Dial("tcp", ln.Addr().String()) + + // Immediately after dialing, request cancellation and sleep. + // Before Issue 15078 was fixed, this would cause subsequent operations + // to fail with an i/o timeout roughly 50% of the time. + close(cancel) + time.Sleep(10 * time.Millisecond) + + if err != nil { + t.Fatal(err) + } + defer c.Close() + + // Send some data to confirm that the connection is still alive. + const message = "echo!\n" + if _, err := c.Write([]byte(message)); err != nil { + t.Fatal(err) + } + + // The server should echo the line, and close the connection. + rb := bufio.NewReader(c) + line, err := rb.ReadString('\n') + if err != nil { + t.Fatal(err) + } + if line != message { + t.Errorf("got %q; want %q", line, message) + } + if _, err := rb.ReadByte(); err != io.EOF { + t.Errorf("got %v; want %v", err, io.EOF) + } + } + + // This bug manifested about 50% of the time, so try it a few times. + for i := 0; i < 10; i++ { + try() + } +} + +func TestDialClosedPortFailFast(t *testing.T) { + if runtime.GOOS != "windows" { + // Reported by go.dev/issues/23366. + t.Skip("skipping windows only test") + } + for _, network := range []string{"tcp", "tcp4", "tcp6"} { + t.Run(network, func(t *testing.T) { + if !testableNetwork(network) { + t.Skipf("skipping: can't listen on %s", network) + } + // Reserve a local port till the end of the + // test by opening a listener and connecting to + // it using Dial. + ln := newLocalListener(t, network) + addr := ln.Addr().String() + conn1, err := Dial(network, addr) + if err != nil { + ln.Close() + t.Fatal(err) + } + defer conn1.Close() + // Now close the listener so the next Dial fails + // keeping conn1 alive so the port is not made + // available. + ln.Close() + + maxElapsed := time.Second + // The host can be heavy-loaded and take + // longer than configured. Retry until + // Dial takes less than maxElapsed or + // the test times out. + for { + startTime := time.Now() + conn2, err := Dial(network, addr) + if err == nil { + conn2.Close() + t.Fatal("error expected") + } + elapsed := time.Since(startTime) + if elapsed < maxElapsed { + break + } + t.Logf("got %v; want < %v", elapsed, maxElapsed) + } + }) + } +} + +// Issue 18806: it should always be possible to net.Dial a +// net.Listener().Addr().String when the listen address was ":n", even +// if the machine has halfway configured IPv6 such that it can bind on +// "::" not connect back to that same address. +func TestDialListenerAddr(t *testing.T) { + if !testableNetwork("tcp4") { + t.Skipf("skipping: can't listen on tcp4") + } + + // The original issue report was for listening on just ":0" on a system that + // supports both tcp4 and tcp6 for external traffic but only tcp4 for loopback + // traffic. However, the port opened by ":0" is externally-accessible, and may + // trigger firewall alerts or otherwise be mistaken for malicious activity + // (see https://go.dev/issue/59497). Moreover, it often does not reproduce + // the scenario in the issue, in which the port *cannot* be dialed as tcp6. + // + // To address both of those problems, we open a tcp4-only localhost port, but + // then dial the address string that the listener would have reported for a + // dual-stack port. + ln, err := Listen("tcp4", "localhost:0") + if err != nil { + t.Fatal(err) + } + defer ln.Close() + + t.Logf("listening on %q", ln.Addr()) + _, port, err := SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + + // If we had opened a dual-stack port without an explicit "localhost" address, + // the Listener would arbitrarily report an empty tcp6 address in its Addr + // string. + // + // The documentation for Dial says ‘if the host is empty or a literal + // unspecified IP address, as in ":80", "0.0.0.0:80" or "[::]:80" for TCP and + // UDP, "", "0.0.0.0" or "::" for IP, the local system is assumed.’ + // In #18806, it was decided that that should include the local tcp4 host + // even if the string is in the tcp6 format. + dialAddr := "[::]:" + port + c, err := Dial("tcp4", dialAddr) + if err != nil { + t.Fatalf(`Dial("tcp4", %q): %v`, dialAddr, err) + } + c.Close() + t.Logf(`Dial("tcp4", %q) succeeded`, dialAddr) +} + +func TestDialerControl(t *testing.T) { + switch runtime.GOOS { + case "plan9": + t.Skipf("not supported on %s", runtime.GOOS) + case "js", "wasip1": + t.Skipf("skipping: fake net does not support Dialer.Control") + } + + t.Run("StreamDial", func(t *testing.T) { + for _, network := range []string{"tcp", "tcp4", "tcp6", "unix", "unixpacket"} { + if !testableNetwork(network) { + continue + } + ln := newLocalListener(t, network) + defer ln.Close() + d := Dialer{Control: controlOnConnSetup} + c, err := d.Dial(network, ln.Addr().String()) + if err != nil { + t.Error(err) + continue + } + c.Close() + } + }) + t.Run("PacketDial", func(t *testing.T) { + for _, network := range []string{"udp", "udp4", "udp6", "unixgram"} { + if !testableNetwork(network) { + continue + } + c1 := newLocalPacketListener(t, network) + if network == "unixgram" { + defer os.Remove(c1.LocalAddr().String()) + } + defer c1.Close() + d := Dialer{Control: controlOnConnSetup} + c2, err := d.Dial(network, c1.LocalAddr().String()) + if err != nil { + t.Error(err) + continue + } + c2.Close() + } + }) +} + +func TestDialerControlContext(t *testing.T) { + switch runtime.GOOS { + case "plan9": + t.Skipf("%s does not have full support of socktest", runtime.GOOS) + case "js", "wasip1": + t.Skipf("skipping: fake net does not support Dialer.ControlContext") + } + t.Run("StreamDial", func(t *testing.T) { + for i, network := range []string{"tcp", "tcp4", "tcp6", "unix", "unixpacket"} { + t.Run(network, func(t *testing.T) { + if !testableNetwork(network) { + t.Skipf("skipping: %s not available", network) + } + + ln := newLocalListener(t, network) + defer ln.Close() + var id int + d := Dialer{ControlContext: func(ctx context.Context, network string, address string, c syscall.RawConn) error { + id = ctx.Value("id").(int) + return controlOnConnSetup(network, address, c) + }} + c, err := d.DialContext(context.WithValue(context.Background(), "id", i+1), network, ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + if id != i+1 { + t.Errorf("got id %d, want %d", id, i+1) + } + c.Close() + }) + } + }) +} + +// mustHaveExternalNetwork is like testenv.MustHaveExternalNetwork +// except on non-Linux, non-mobile builders it permits the test to +// run in -short mode. +func mustHaveExternalNetwork(t *testing.T) { + t.Helper() + definitelyHasLongtestBuilder := runtime.GOOS == "linux" + mobile := runtime.GOOS == "android" || runtime.GOOS == "ios" + fake := runtime.GOOS == "js" || runtime.GOOS == "wasip1" + if testenv.Builder() != "" && !definitelyHasLongtestBuilder && !mobile && !fake { + // On a non-Linux, non-mobile builder (e.g., freebsd-amd64-13_0). + // + // Don't skip testing because otherwise the test may never run on + // any builder if this port doesn't also have a -longtest builder. + return + } + testenv.MustHaveExternalNetwork(t) +} + +type contextWithNonZeroDeadline struct { + context.Context +} + +func (contextWithNonZeroDeadline) Deadline() (time.Time, bool) { + // Return non-zero time.Time value with false indicating that no deadline is set. + return time.Unix(0, 0), false +} + +func TestDialWithNonZeroDeadline(t *testing.T) { + ln := newLocalListener(t, "tcp") + defer ln.Close() + _, port, err := SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatal(err) + } + + ctx := contextWithNonZeroDeadline{Context: context.Background()} + var dialer Dialer + c, err := dialer.DialContext(ctx, "tcp", JoinHostPort("", port)) + if err != nil { + t.Fatal(err) + } + c.Close() +} |