]> Cypherpunks repositories - gostls13.git/commitdiff
net: make Dial fail faster on Windows closed loopback devices
authorqmuntal <quimmuntal@gmail.com>
Wed, 17 May 2023 16:21:04 +0000 (18:21 +0200)
committerQuim Muntal <quimmuntal@gmail.com>
Fri, 19 May 2023 20:19:50 +0000 (20:19 +0000)
On Windows when connecting to an unavailable port, ConnectEx() will
retry for 2s, even on loopback devices.

This CL uses a call to WSAIoctl to make the ConnectEx() call fail
faster on local connections.

Fixes #23366

Change-Id: Iafeca8ea0053f01116b2504c45d88120f84d05e9
Reviewed-on: https://go-review.googlesource.com/c/go/+/495875
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Bryan Mills <bcmills@google.com>
Run-TryBot: Quim Muntal <quimmuntal@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/internal/syscall/windows/net_windows.go
src/net/dial_test.go
src/net/fd_windows.go

index 3d3df7161c1ce2986da917e645b9a9877e0a76fe..42c600c1447df674a46ab745a596d04c7f323190 100644 (file)
@@ -5,6 +5,7 @@
 package windows
 
 import (
+       "sync"
        "syscall"
        _ "unsafe"
 )
@@ -16,3 +17,24 @@ func WSASendtoInet4(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent
 //go:linkname WSASendtoInet6 syscall.wsaSendtoInet6
 //go:noescape
 func WSASendtoInet6(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent *uint32, flags uint32, to *syscall.SockaddrInet6, overlapped *syscall.Overlapped, croutine *byte) (err error)
+
+const (
+       SIO_TCP_INITIAL_RTO                    = syscall.IOC_IN | syscall.IOC_VENDOR | 17
+       TCP_INITIAL_RTO_UNSPECIFIED_RTT        = ^uint16(0)
+       TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS = ^uint8(1)
+)
+
+type TCP_INITIAL_RTO_PARAMETERS struct {
+       Rtt                   uint16
+       MaxSynRetransmissions uint8
+}
+
+var Support_TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS = sync.OnceValue(func() bool {
+       var maj, min, build uint32
+       rtlGetNtVersionNumbers(&maj, &min, &build)
+       return maj >= 10 && build&0xffff >= 16299
+})
+
+//go:linkname rtlGetNtVersionNumbers syscall.rtlGetNtVersionNumbers
+//go:noescape
+func rtlGetNtVersionNumbers(majorVersion *uint32, minorVersion *uint32, buildNumber *uint32)
index d4db405e3732a8592a8e51ad1c89f8434e055cd6..2eea66a097a90ead748415626d3b757825c8c528 100644 (file)
@@ -878,6 +878,54 @@ func TestCancelAfterDial(t *testing.T) {
        }
 }
 
+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
index 030b6a15fb5123ca9bd3f2207afaafe76b489a7a..eeb994dfd9cb25f1b712cc1c9f0be98001b8322e 100644 (file)
@@ -7,6 +7,7 @@ package net
 import (
        "context"
        "internal/poll"
+       "internal/syscall/windows"
        "os"
        "runtime"
        "syscall"
@@ -86,6 +87,32 @@ func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (syscall.
                }
        }
 
+       var isloopback bool
+       switch ra := ra.(type) {
+       case *syscall.SockaddrInet4:
+               isloopback = ra.Addr[0] == 127
+       case *syscall.SockaddrInet6:
+               isloopback = ra.Addr == [16]byte(IPv6loopback)
+       default:
+               panic("unexpected type in connect")
+       }
+       if isloopback {
+               // This makes ConnectEx() fails faster if the target port on the localhost
+               // is not reachable, instead of waiting for 2s.
+               params := windows.TCP_INITIAL_RTO_PARAMETERS{
+                       Rtt:                   windows.TCP_INITIAL_RTO_UNSPECIFIED_RTT, // use the default or overridden by the Administrator
+                       MaxSynRetransmissions: 1,                                       // minimum possible value before Windows 10.0.16299
+               }
+               if windows.Support_TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS() {
+                       // In Windows 10.0.16299 TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS makes ConnectEx() fails instantly.
+                       params.MaxSynRetransmissions = windows.TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS
+               }
+               var out uint32
+               // Don't abort the connection if WSAIoctl fails, as it is only an optimization.
+               // If it fails reliably, we expect TestDialClosedPortFailFast to detect it.
+               _ = fd.pfd.WSAIoctl(windows.SIO_TCP_INITIAL_RTO, (*byte)(unsafe.Pointer(&params)), uint32(unsafe.Sizeof(params)), nil, 0, &out, nil, 0)
+       }
+
        // Wait for the goroutine converting context.Done into a write timeout
        // to exist, otherwise our caller might cancel the context and
        // cause fd.setWriteDeadline(aLongTimeAgo) to cancel a successful dial.