From b46cd8a7860604fe3dee06e8a2738c8f2845321b Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 16 Jan 2026 14:51:33 +0000 Subject: [PATCH] fix(udp): prevent socket from dying on Windows ICMP port unreachable On Windows, when a UDP socket sends a packet to an unreachable port, the Windows TCP/IP stack receives an ICMP "Port Unreachable" message and by default forwards this as WSAECONNRESET (error code 10054) to subsequent recv calls. This caused Bun's UDP socket to close completely. This fix disables the behavior using SIO_UDP_CONNRESET, matching Node.js/libuv behavior. Co-Authored-By: Claude Opus 4.5 --- packages/bun-usockets/src/bsd.c | 12 ++++++ test/regression/issue/26166.test.ts | 59 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 test/regression/issue/26166.test.ts diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index 732d79559a..6ecd0f144f 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -1231,6 +1231,18 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op return LIBUS_SOCKET_ERROR; } +#ifdef _WIN32 + /* Disable WSAECONNRESET errors from ICMP "Port Unreachable" messages. + * Without this, sending to a closed port causes subsequent recv calls to fail, + * which would close the entire UDP socket. This matches Node.js/libuv behavior. */ + { + BOOL bNewBehavior = FALSE; + DWORD dwBytesReturned = 0; + WSAIoctl(listenFd, SIO_UDP_CONNRESET, &bNewBehavior, sizeof(bNewBehavior), + NULL, 0, &dwBytesReturned, NULL, NULL); + } +#endif + if (port != 0) { /* Should this also go for UDP? */ int enabled = 1; diff --git a/test/regression/issue/26166.test.ts b/test/regression/issue/26166.test.ts new file mode 100644 index 0000000000..f1edcb299e --- /dev/null +++ b/test/regression/issue/26166.test.ts @@ -0,0 +1,59 @@ +import { udpSocket } from "bun"; +import { expect, test } from "bun:test"; + +// https://github.com/oven-sh/bun/issues/26166 +// On Windows, ICMP "Port Unreachable" messages cause WSAECONNRESET errors which +// would close the UDP socket entirely. This test verifies the socket stays alive. +test("UDP socket survives sending to unreachable port", async () => { + let receivedCount = 0; + const receivedData: string[] = []; + const { resolve, reject, promise } = Promise.withResolvers(); + + // Create a server socket + const server = await udpSocket({ + socket: { + data(socket, data, port, address) { + receivedCount++; + receivedData.push(new TextDecoder().decode(data)); + if (receivedCount === 2) { + resolve(); + } + }, + }, + }); + + // Create a client + const client = await udpSocket({}); + + // Send first message to the server + client.send(Buffer.from("message1"), server.port, "127.0.0.1"); + + // Wait a bit for message to be received + await Bun.sleep(50); + + // Now send to an unreachable port (a port that's definitely not listening) + // This would trigger an ICMP Port Unreachable on Windows + const unreachablePort = 59999; + server.send(Buffer.from("this will fail"), unreachablePort, "127.0.0.1"); + + // Wait a bit for the ICMP error to potentially be processed + await Bun.sleep(100); + + // Send second message - this should still work if the socket survived + client.send(Buffer.from("message2"), server.port, "127.0.0.1"); + + // Wait for messages with timeout + const timeout = setTimeout(() => reject(new Error("Timeout waiting for messages")), 2000); + + try { + await promise; + clearTimeout(timeout); + } finally { + server.close(); + client.close(); + } + + expect(receivedCount).toBe(2); + expect(receivedData).toContain("message1"); + expect(receivedData).toContain("message2"); +});