net: make Dial fail faster on Windows closed loopback devices

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>
This commit is contained in:
qmuntal 2023-05-17 18:21:04 +02:00 committed by Quim Muntal
parent 6333725d5f
commit 656a20a52a
3 changed files with 97 additions and 0 deletions

View File

@ -5,6 +5,7 @@
package windows package windows
import ( import (
"sync"
"syscall" "syscall"
_ "unsafe" _ "unsafe"
) )
@ -16,3 +17,24 @@ func WSASendtoInet4(s syscall.Handle, bufs *syscall.WSABuf, bufcnt uint32, sent
//go:linkname WSASendtoInet6 syscall.wsaSendtoInet6 //go:linkname WSASendtoInet6 syscall.wsaSendtoInet6
//go:noescape //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) 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)

View 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 // Issue 18806: it should always be possible to net.Dial a
// net.Listener().Addr().String when the listen address was ":n", even // net.Listener().Addr().String when the listen address was ":n", even
// if the machine has halfway configured IPv6 such that it can bind on // if the machine has halfway configured IPv6 such that it can bind on

View File

@ -7,6 +7,7 @@ package net
import ( import (
"context" "context"
"internal/poll" "internal/poll"
"internal/syscall/windows"
"os" "os"
"runtime" "runtime"
"syscall" "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 // Wait for the goroutine converting context.Done into a write timeout
// to exist, otherwise our caller might cancel the context and // to exist, otherwise our caller might cancel the context and
// cause fd.setWriteDeadline(aLongTimeAgo) to cancel a successful dial. // cause fd.setWriteDeadline(aLongTimeAgo) to cancel a successful dial.