From 99e88edf736827a26e8ce9d87da972f41ce828d9 Mon Sep 17 00:00:00 2001 From: Bartosz Sosnowski Date: Thu, 13 Aug 2020 17:07:21 +0200 Subject: [PATCH] tcp: fail instantly if local port is unbound On Windows when connecting to an unavailable port, the connect() will retry for 2s, even on loopback devices. This uses a call to WSAIoctl to make the connect() call fail instantly on local connections. PR-URL: https://github.com/libuv/libuv/pull/2896 Reviewed-By: Ben Noordhuis Reviewed-By: Santiago Gimeno Reviewed-By: Jameson Nash --- src/win/tcp.c | 54 ++++++++++++++++ src/win/winapi.h | 12 ++++ test/test-list.h | 4 ++ test/test-tcp-connect-timeout.c | 105 ++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+) diff --git a/src/win/tcp.c b/src/win/tcp.c index 3b820066..ece52952 100644 --- a/src/win/tcp.c +++ b/src/win/tcp.c @@ -748,6 +748,40 @@ int uv_tcp_read_start(uv_tcp_t* handle, uv_alloc_cb alloc_cb, return 0; } +static int uv__is_loopback(const struct sockaddr_storage* storage) { + const struct sockaddr_in* in4; + const struct sockaddr_in6* in6; + int i; + + if (storage->ss_family == AF_INET) { + in4 = (const struct sockaddr_in*) storage; + return in4->sin_addr.S_un.S_un_b.s_b1 == 127; + } + if (storage->ss_family == AF_INET6) { + in6 = (const struct sockaddr_in6*) storage; + for (i = 0; i < 7; ++i) { + if (in6->sin6_addr.u.Word[i] != 0) + return 0; + } + return in6->sin6_addr.u.Word[7] == htons(1); + } + return 0; +} + +// Check if Windows version is 10.0.16299 or later +static int uv__is_fast_loopback_fail_supported() { + OSVERSIONINFOW os_info; + if (!pRtlGetVersion) + return 0; + pRtlGetVersion(&os_info); + if (os_info.dwMajorVersion < 10) + return 0; + if (os_info.dwMajorVersion > 10) + return 1; + if (os_info.dwMinorVersion > 0) + return 1; + return os_info.dwBuildNumber >= 16299; +} static int uv_tcp_try_connect(uv_connect_t* req, uv_tcp_t* handle, @@ -755,6 +789,7 @@ static int uv_tcp_try_connect(uv_connect_t* req, unsigned int addrlen, uv_connect_cb cb) { uv_loop_t* loop = handle->loop; + TCP_INITIAL_RTO_PARAMETERS retransmit_ioctl; const struct sockaddr* bind_addr; struct sockaddr_storage converted; BOOL success; @@ -790,6 +825,25 @@ static int uv_tcp_try_connect(uv_connect_t* req, } } + /* This makes connect() fail instantly if the target port on the localhost + * is not reachable, instead of waiting for 2s. We do not care if this fails. + * This only works on Windows version 10.0.16299 and later. + */ + if (uv__is_fast_loopback_fail_supported() && uv__is_loopback(&converted)) { + memset(&retransmit_ioctl, 0, sizeof(retransmit_ioctl)); + retransmit_ioctl.Rtt = TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS; + retransmit_ioctl.MaxSynRetransmissions = TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS; + WSAIoctl(handle->socket, + SIO_TCP_INITIAL_RTO, + &retransmit_ioctl, + sizeof(retransmit_ioctl), + NULL, + 0, + &bytes, + NULL, + NULL); + } + UV_REQ_INIT(req, UV_CONNECT); req->handle = (uv_stream_t*) handle; req->cb = cb; diff --git a/src/win/winapi.h b/src/win/winapi.h index cbe1437a..0b66b563 100644 --- a/src/win/winapi.h +++ b/src/win/winapi.h @@ -4726,6 +4726,18 @@ typedef HWINEVENTHOOK (WINAPI *sSetWinEventHook) DWORD idThread, UINT dwflags); +/* From mstcpip.h */ +typedef struct _TCP_INITIAL_RTO_PARAMETERS { + USHORT Rtt; + UCHAR MaxSynRetransmissions; +} TCP_INITIAL_RTO_PARAMETERS, *PTCP_INITIAL_RTO_PARAMETERS; + +#ifndef TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS +# define TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS ((UCHAR) -2) +#endif +#ifndef SIO_TCP_INITIAL_RTO +# define SIO_TCP_INITIAL_RTO _WSAIOW(IOC_VENDOR,17) +#endif /* Ntdll function pointers */ extern sRtlGetVersion pRtlGetVersion; diff --git a/test/test-list.h b/test/test-list.h index ebf72700..310db149 100644 --- a/test/test-list.h +++ b/test/test-list.h @@ -127,6 +127,8 @@ TEST_DECLARE (tcp_bind_writable_flags) TEST_DECLARE (tcp_listen_without_bind) TEST_DECLARE (tcp_connect_error_fault) TEST_DECLARE (tcp_connect_timeout) +TEST_DECLARE (tcp_local_connect_timeout) +TEST_DECLARE (tcp6_local_connect_timeout) TEST_DECLARE (tcp_close_while_connecting) TEST_DECLARE (tcp_close) TEST_DECLARE (tcp_close_reset_accepted) @@ -684,6 +686,8 @@ TASK_LIST_START TEST_ENTRY (tcp_listen_without_bind) TEST_ENTRY (tcp_connect_error_fault) TEST_ENTRY (tcp_connect_timeout) + TEST_ENTRY (tcp_local_connect_timeout) + TEST_ENTRY (tcp6_local_connect_timeout) TEST_ENTRY (tcp_close_while_connecting) TEST_ENTRY (tcp_close) TEST_ENTRY (tcp_close_reset_accepted) diff --git a/test/test-tcp-connect-timeout.c b/test/test-tcp-connect-timeout.c index 081424b8..6b455276 100644 --- a/test/test-tcp-connect-timeout.c +++ b/test/test-tcp-connect-timeout.c @@ -89,3 +89,108 @@ TEST_IMPL(tcp_connect_timeout) { MAKE_VALGRIND_HAPPY(); return 0; } + +/* Make sure connect fails instantly if the target is nonexisting + * local port. + */ + +static void connect_local_cb(uv_connect_t* req, int status) { + ASSERT_PTR_EQ(req, &connect_req); + ASSERT_NE(status, UV_ECANCELED); + connect_cb_called++; +} + +static int is_supported_system() { + int semver[3]; + int min_semver[3] = {10, 0, 16299}; + int cnt; + uv_utsname_t uname; + ASSERT_EQ(uv_os_uname(&uname), 0); + if (strcmp(uname.sysname, "Windows_NT") == 0) { + cnt = sscanf(uname.release, "%d.%d.%d", &semver[0], &semver[1], &semver[2]); + if (cnt != 3) { + return 0; + } + // relase >= 10.0.16299 + for (cnt = 0; cnt < 3; ++cnt) { + if (semver[cnt] > min_semver[cnt]) + return 1; + if (semver[cnt] < min_semver[cnt]) + return 0; + } + return 1; + } + return 1; +} + +TEST_IMPL(tcp_local_connect_timeout) { + struct sockaddr_in addr; + int r; + + if (!is_supported_system()) { + RETURN_SKIP("Unsupported system"); + } + ASSERT_EQ(0, uv_ip4_addr("127.0.0.1", TEST_PORT, &addr)); + + r = uv_timer_init(uv_default_loop(), &timer); + ASSERT_EQ(r, 0); + + /* Give it 1s to timeout. */ + r = uv_timer_start(&timer, timer_cb, 1000, 0); + ASSERT_EQ(r, 0); + + r = uv_tcp_init(uv_default_loop(), &conn); + ASSERT_EQ(r, 0); + + r = uv_tcp_connect(&connect_req, + &conn, + (const struct sockaddr*) &addr, + connect_local_cb); + if (r == UV_ENETUNREACH) + RETURN_SKIP("Network unreachable."); + ASSERT_EQ(r, 0); + + r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); + ASSERT(r == 0); + + MAKE_VALGRIND_HAPPY(); + return 0; +} + +TEST_IMPL(tcp6_local_connect_timeout) { + struct sockaddr_in6 addr; + int r; + + if (!is_supported_system()) { + RETURN_SKIP("Unsupported system"); + } + if (!can_ipv6()) { + RETURN_SKIP("IPv6 not supported"); + } + + ASSERT_EQ(0, uv_ip6_addr("::1", 9999, &addr)); + + r = uv_timer_init(uv_default_loop(), &timer); + ASSERT_EQ(r, 0); + + /* Give it 1s to timeout. */ + r = uv_timer_start(&timer, timer_cb, 1000, 0); + ASSERT_EQ(r, 0); + + r = uv_tcp_init(uv_default_loop(), &conn); + ASSERT_EQ(r, 0); + + r = uv_tcp_connect(&connect_req, + &conn, + (const struct sockaddr*) &addr, + connect_local_cb); + if (r == UV_ENETUNREACH) + RETURN_SKIP("Network unreachable."); + ASSERT_EQ(r, 0); + + r = uv_run(uv_default_loop(), UV_RUN_DEFAULT); + ASSERT_EQ(r, 0); + + MAKE_VALGRIND_HAPPY(); + return 0; +}