uNetworking / uNetworking/uSockets
`on_open` incorrectly called on Windows when TCP connection is refused (RST)
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_tdoes not monitorFD_CONNECT, so connection errors are reliably missed. - Linux/macOS (latent): Even on platforms where libuv reports errors correctly, the missing
SO_ERRORcheck 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
- After libuv reports the socket as writable with
status = 0, callgetsockopt(fd, SOL_SOCKET, SO_ERROR, ...)to retrieve any pending socket-level error. - If
SO_ERRORis non-zero (e.g.,WSAECONNREFUSEDon Windows,ECONNREFUSEDon Linux), calls->context->on_connect_error(s, so_error)with the actual error code, then close the connecting socket viaus_socket_close_connecting. - If
SO_ERRORis zero, proceed with the normal success path (change poll type, set nodelay, callon_open).
This fix is safe on all platforms:
- On Linux/macOS,
SO_ERRORwill be 0 when the connection succeeds, so the behavior is unchanged. - On Windows,
SO_ERRORwill containWSAECONNREFUSED(or similar) when the server sends RST, correctly routing toon_connect_error. - The fix is defense-in-depth — even if libuv or another eventing backend has a similar blind spot on any platform,
SO_ERRORprovides 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.
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/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