uNetworking / uNetworking/uSockets

`on_open` incorrectly called on Windows when TCP connection is refused (RST)

Open Beginner friendly
#258 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 client attempts to connect to a server that is not listening on the target port, the server's TCP stack responds with an RST packet. In a correct implementation, the connection callback on_connect_error should be invoked, not on_open.

However, on Windows, when using the libuv eventing backend (LIBUS_USE_LIBUV), on_open is incorrectly called instead of on_connect_error when the server sends RST in response to a SYN.

Root Cause

The issue has two layers:

1. libuv on Windows does not monitor FD_CONNECT events

In src/eventing/libuv.c, poll_cb receives the status parameter from libuv's uv_poll_t callback:

static void poll_cb(uv_poll_t *p, int status, int events) {
    us_internal_dispatch_ready_poll((struct us_poll_t *) p->data, status < 0, events);
}

On Windows, libuv's uv_poll_t implementation uses WSAEventSelect under the hood. However, uv_poll_start registers for FD_READ | FD_WRITE | FD_OOB | FD_CLOSE — it does not register for FD_CONNECT. When a connection attempt fails (RST received), Windows signals FD_CONNECT with an error code (e.g., WSAECONNREFUSED), but since libuv is not listening for FD_CONNECT, this error is never delivered through the status parameter.

Meanwhile, the socket still becomes writable (FD_WRITE) after the connection completes (even with failure). libuv sees FD_WRITE, calls poll_cb with status = 0 and events = UV_WRITABLE, making uSockets believe the connection succeeded.

2. No getsockopt(SO_ERROR) verification before calling on_open

In src/loop.c, the POLL_TYPE_SEMI_SOCKET handler in us_internal_dispatch_ready_poll relies solely on libuv's error parameter to decide between success and failure:

case POLL_TYPE_SEMI_SOCKET: {
    if (us_poll_events(p) == LIBUS_SOCKET_WRITABLE) {
        struct us_socket_t *s = (struct us_socket_t *) p;
        if (error) {
            s->context->on_connect_error(s, 0);
            us_socket_close_connecting(0, s);
        } else {
            // No SO_ERROR check — assumes success unconditionally
            us_poll_change(p, s->context->loop, LIBUS_SOCKET_READABLE);
            bsd_socket_nodelay(us_poll_fd(p), 1);
            us_internal_poll_set_type(p, POLL_TYPE_SOCKET);
            s->context->on_open(s, 1, 0, 0);
        }
    }
}

The code does not call getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) to verify that the connection actually succeeded. On Windows, where libuv's status is unreliable for connection errors, this missing check leads to on_open being called for failed connections.

Wireshark Evidence

A packet capture on Windows confirms the behavior:

111.635702    Client → Server    [SYN]              (initial SYN)
111.635723    Client → Server    [SYN] Out-Of-Order  (duplicate SYN, 21µs later)
111.636519    Server → Client    [RST, ACK]          (server refuses, port not open)
112.137081    Client → Server    [SYN] Retransmission (client retransmits despite RST)
112.137827    Server → Client    [RST, ACK]          (server refuses again)

Despite receiving RST, the application's on_open callback (equivalent to OnSocketOpen) is invoked, connected_ is set to true, and the application proceeds to send application-level data — which never gets a response, eventually leading to a timeout.

Affected Platforms
  • Windows (primary): libuv's uv_poll_t does not monitor FD_CONNECT, so connection errors are reliably missed.
  • Linux/macOS (latent): Even on platforms where libuv reports errors correctly, the missing SO_ERROR check is a defense-in-depth gap.

Solution

Add a getsockopt(SO_ERROR) check in the else branch of the POLL_TYPE_SEMI_SOCKET handler in us_internal_dispatch_ready_poll (src/loop.c). If SO_ERROR is non-zero, call on_connect_error and close the connecting socket instead of calling on_open.

Patch

In src/loop.c, modify the POLL_TYPE_SEMI_SOCKET case:

                 } else {
+                    /* Verify connection actually succeeded via SO_ERROR.
+                     * On Windows, libuv's uv_poll_t does not monitor FD_CONNECT,
+                     * so connection errors (RST/ECONNREFUSED) are not reported
+                     * through the error parameter. getsockopt(SO_ERROR) catches
+                     * these missed errors before calling on_open. */
+                    int so_error = 0;
+                    LIBUS_SOCKET_DESCRIPTOR fd = us_poll_fd(p);
+#ifdef _WIN32
+                    int so_error_len = sizeof(so_error);
+                    getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *) &so_error, &so_error_len);
+#else
+                    socklen_t so_error_len = sizeof(so_error);
+                    getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_error_len);
+#endif
+                    if (so_error != 0) {
+                        /* Connection failed (missed by libuv on Windows) */
+                        s->context->on_connect_error(s, so_error);
+                        us_socket_close_connecting(0, s);
+                    } else {
                         /* All sockets poll for readable */
                         us_poll_change(p, s->context->loop, LIBUS_SOCKET_READABLE);

                         /* We always use nodelay */
                         bsd_socket_nodelay(us_poll_fd(p), 1);

                         /* We are now a proper socket */
                         us_internal_poll_set_type(p, POLL_TYPE_SOCKET);

                         /* If we used a connection timeout we have to reset it here */
                         us_socket_timeout(0, s, 0);

                         s->context->on_open(s, 1, 0, 0);
+                    }
                 }
How It Works
  1. After libuv reports the socket as writable with status = 0, call getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) to retrieve any pending socket-level error.
  2. If SO_ERROR is non-zero (e.g., WSAECONNREFUSED on Windows, ECONNREFUSED on Linux), call s->context->on_connect_error(s, so_error) with the actual error code, then close the connecting socket via us_socket_close_connecting.
  3. If SO_ERROR is zero, proceed with the normal success path (change poll type, set nodelay, call on_open).

This fix is safe on all platforms:

  • On Linux/macOS, SO_ERROR will be 0 when the connection succeeds, so the behavior is unchanged.
  • On Windows, SO_ERROR will contain WSAECONNREFUSED (or similar) when the server sends RST, correctly routing to on_connect_error.
  • The fix is defense-in-depth — even if libuv or another eventing backend has a similar blind spot on any platform, SO_ERROR provides a reliable second opinion.

Testing

Tested on Windows 10/11 with the libuv backend:

  • Before fix: Connecting to an unresponsive server (RST) triggers on_open → application sends login request → 3-second timeout → on_close.
  • After fix: Connecting to an unresponsive server (RST) correctly triggers on_connect_error → socket is closed → application can retry immediately.

Also verified on Linux (where libuv correctly reports errors via status): behavior is unchanged — SO_ERROR is 0 for successful connections, and the fix is a no-op.

Image

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/loop.c at the POLL_TYPE_SEMI_SOCKET branch of us_internal_dispatch_ready_poll, then review how the existing error path calls on_connect_error and closes the connecting socket. Verify the writable connection path checks SO_ERROR before on_open and preserves the successful setup. Test the refused-connection case with the libuv backend on Windows, and confirm Linux behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
networking
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.