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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-16 14:51:33 +00:00
parent f01467d3dc
commit b46cd8a786
2 changed files with 71 additions and 0 deletions

View File

@@ -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;

View File

@@ -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<void>();
// 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");
});