uNetworking / uNetworking/uSockets

`connect()` blocks for ~21 seconds on Windows when server is unreachable (SYN dropped)

Open Beginner friendly
#259 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. SOCK_NONBLOCK is not defined — the socket() call creates a blocking socket.
  2. The original bsd_set_nonblocking on Windows was either missing or did not actually set the socket to non-blocking mode before connect() was called.
  3. 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 after connect() has already been invoked on a blocking socket.
  4. When connect() is called on a blocking socket and the server is unreachable (SYN silently dropped by a firewall), Windows blocks inside connect() 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_NONBLOCK at socket creation time.
    • macOS/BSD also support SOCK_NONBLOCK.
    • Windows has no equivalent flag for socket().

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
  1. ioctlsocket(fd, FIONBIO, &nonblocking) is the Windows API to set a socket to non-blocking mode (equivalent to fcntl(..., O_NONBLOCK) on POSIX systems).
  2. The call is placed in bsd_set_nonblocking, which is called from bsd_create_socket before bsd_create_connect_socket calls connect().
  3. With a non-blocking socket, connect() returns immediately with WSAEWOULDBLOCK (equivalent to EINPROGRESS on POSIX). The actual connection happens asynchronously.
  4. 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 the on_open / on_connect_error callback.

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.