mirror of https://github.com/golang/go.git
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:
parent
6333725d5f
commit
656a20a52a
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(¶ms)), 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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue