uNetworking / uNetworking/uSockets
`connect()` blocks for ~21 seconds on Windows when server is unreachable (SYN dropped)
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 1.5k
- Forks
- 307
- PR merge metrics
- No merged PRs in 30d
Description
Problem Background
When a TCP client attempts to connect to a server that is unreachable (e.g., firewall silently drops SYN packets, or server is down and no RST is returned), the connect() call on a blocking socket on Windows blocks for approximately 21 seconds (the default TCP SYN retransmission timeout: 3s + 6s + 12s exponential backoff).
In uSockets with the libuv backend (LIBUS_USE_LIBUV), this causes the entire event loop to freeze for 21 seconds, making the application completely unresponsive. No timers fire, no other I/O is processed, and no callbacks are invoked during this period.
Root Cause
The issue is in the interaction between bsd_create_socket, bsd_set_nonblocking, and the call order of connect() on Windows.
In the original code, bsd_create_socket creates a socket and then calls bsd_set_nonblocking:
// src/bsd.c
LIBUS_SOCKET_DESCRIPTOR bsd_create_socket(int domain, int type, int protocol) {
int flags = 0;
#if defined(SOCK_CLOEXEC) && defined(SOCK_NONBLOCK)
flags = SOCK_CLOEXEC | SOCK_NONBLOCK;
#endif
LIBUS_SOCKET_DESCRIPTOR created_fd = socket(domain, type | flags, protocol);
return bsd_set_nonblocking(apple_no_sigpipe(created_fd));
}
On Linux, SOCK_NONBLOCK is defined, so the socket is created as non-blocking directly in the socket() syscall. The bsd_set_nonblocking call is a safety net.
On Windows, however:
SOCK_NONBLOCKis not defined — thesocket()call creates a blocking socket.- The original
bsd_set_nonblockingon Windows was either missing or did not actually set the socket to non-blocking mode beforeconnect()was called. - The original code implicitly relied on libuv's
uv_poll_init_socket()to set the socket to non-blocking mode. However,uv_poll_init_socket()is called afterconnect()has already been invoked on a blocking socket. - When
connect()is called on a blocking socket and the server is unreachable (SYN silently dropped by a firewall), Windows blocks insideconnect()for the full TCP SYN timeout period (~21 seconds).
Call sequence (before fix):
bsd_create_socket()
→ socket() // creates BLOCKING socket on Windows (no SOCK_NONBLOCK)
→ bsd_set_nonblocking() // either missing or ineffective on Windows
→ returns fd
bsd_create_connect_socket()
→ bsd_create_socket()
→ connect(fd, ...) // BLOCKS for ~21 seconds on Windows when SYN is dropped!
// Event loop is completely frozen during this time
→ returns fd
us_socket_context_connect()
→ bsd_create_connect_socket()
→ us_poll_init() // registers with libuv
→ us_poll_start() // uv_poll_start() → uv_poll_init_socket() sets non-blocking
// BUT TOO LATE — connect() has already blocked for 21s
Wireshark Evidence
When the server is unreachable and the firewall drops SYN packets, the Windows TCP stack retransmits SYN at the following intervals:
T+0.0s [SYN] (initial SYN)
T+3.0s [SYN] (1st retransmit)
T+9.0s [SYN] (2nd retransmit)
T+21.0s connect() finally returns with WSAETIMEDOUT
During this entire 21-second period, the uSockets event loop is completely blocked — no timers, no I/O, no callbacks can be processed.
Impact
- Application becomes completely unresponsive for ~21 seconds when connecting to an unreachable server on Windows.
- Connection retry logic (e.g., 5-second reconnect timers) is completely blocked — they can only fire after the 21-second blocking
connect()returns. - The problem is Windows-specific because:
- Linux uses
SOCK_NONBLOCKat socket creation time. - macOS/BSD also support
SOCK_NONBLOCK. - Windows has no equivalent flag for
socket().
- Linux uses
Solution
Set the socket to non-blocking mode immediately after creation, before connect() is called, using ioctlsocket(fd, FIONBIO, &nonblocking) on Windows.
Patch
In src/bsd.c, fix bsd_set_nonblocking to actually set non-blocking mode on Windows:
LIBUS_SOCKET_DESCRIPTOR bsd_set_nonblocking(LIBUS_SOCKET_DESCRIPTOR fd) {
#ifdef _WIN32
- /* Empty or ineffective implementation */
- /* Previously relied on libuv's uv_poll_init_socket to set non-blocking,
- * but that happens AFTER connect() has already blocked for 21 seconds. */
+ /* WARNING: Libuv's uv_poll_init_socket does set the socket to non-blocking,
+ * but only AFTER connect() has already been called on a blocking socket.
+ * On Windows, connect() on a blocking socket blocks for ~21 seconds when
+ * the server is unreachable (firewall drops SYN), freezing the entire
+ * event loop. We must set non-blocking BEFORE connect() is called. */
+ unsigned long nonblocking = 1;
+ ioctlsocket(fd, FIONBIO, &nonblocking);
#else
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
#endif
How It Works
ioctlsocket(fd, FIONBIO, &nonblocking)is the Windows API to set a socket to non-blocking mode (equivalent tofcntl(..., O_NONBLOCK)on POSIX systems).- The call is placed in
bsd_set_nonblocking, which is called frombsd_create_socketbeforebsd_create_connect_socketcallsconnect(). - With a non-blocking socket,
connect()returns immediately withWSAEWOULDBLOCK(equivalent toEINPROGRESSon POSIX). The actual connection happens asynchronously. - libuv monitors the socket for writability and reports completion (or error) via the event loop — without blocking.
Call sequence (after fix):
bsd_create_socket()
→ socket() // creates BLOCKING socket on Windows
→ bsd_set_nonblocking() // NOW sets non-blocking via ioctlsocket(FIONBIO) ← FIX
→ returns NON-BLOCKING fd
bsd_create_connect_socket()
→ bsd_create_socket()
→ connect(fd, ...) // returns immediately with WSAEWOULDBLOCK ← NO MORE 21s BLOCK
→ returns fd
us_socket_context_connect()
→ bsd_create_connect_socket()
→ us_poll_init()
→ us_poll_start() // libuv monitors socket asynchronously
// Event loop continues running — timers, I/O all work
Testing
Tested on Windows 10/11:
- Before fix: Connecting to an unreachable server (SYN dropped by firewall) blocks
connect()for ~21 seconds. Event loop is frozen. No timers fire. Application appears hung. - After fix:
connect()returns immediately (WSAEWOULDBLOCK). Event loop continues running. Connection result (success or timeout) is reported asynchronously via theon_open/on_connect_errorcallback.
Also verified on Linux: Behavior is unchanged — SOCK_NONBLOCK at socket creation already ensures non-blocking behavior, and fcntl(F_SETFL, O_NONBLOCK) in bsd_set_nonblocking provides the safety net.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/bsd.c, then trace bsd_create_socket and bsd_create_connect_socket to confirm when connect() runs relative to non-blocking setup. Validate on Windows 10 or 11 by connecting to an unreachable server and checking that the event loop remains responsive; Linux behavior should remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- networking, operating-systems
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100