diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index 1eaefcd768de..a9a1cdcf97d2 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -901,8 +901,7 @@ static int bsd_set_reuseaddr(LIBUS_SOCKET_DESCRIPTOR listenFd) { } static int bsd_set_reuseport(LIBUS_SOCKET_DESCRIPTOR listenFd) { -#if defined(__linux__) - // Among Bun's supported platforms, only Linux does load balancing with SO_REUSEPORT. +#if defined(SO_REUSEPORT) && !defined(_WIN32) const int one = 1; return setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); #else @@ -1232,6 +1231,14 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op } if (bsd_set_reuse(listenFd, options) != 0) { + if (err != NULL) { +#ifdef _WIN32 + *err = WSAGetLastError(); +#else + *err = errno; +#endif + } + bsd_close_socket(listenFd); freeaddrinfo(result); return LIBUS_SOCKET_ERROR; } @@ -1254,23 +1261,15 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op int enabled = 1; if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &enabled, sizeof(enabled)) == -1) { - if (errno == 92) { - if (setsockopt(listenFd, IPPROTO_IP, IP_PKTINFO, &enabled, sizeof(enabled)) != 0) { - //printf("Error setting IPv4 pktinfo!\n"); - } - } else { - //printf("Error setting IPv6 pktinfo!\n"); + if (errno == ENOPROTOOPT || errno == EINVAL) { + setsockopt(listenFd, IPPROTO_IP, IP_PKTINFO, &enabled, sizeof(enabled)); } } /* These are used for getting the ECN */ if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_RECVTCLASS, &enabled, sizeof(enabled)) == -1) { - if (errno == 92) { - if (setsockopt(listenFd, IPPROTO_IP, IP_RECVTOS, &enabled, sizeof(enabled)) != 0) { - //printf("Error setting IPv4 ECN!\n"); - } - } else { - //printf("Error setting IPv6 ECN!\n"); + if (errno == ENOPROTOOPT || errno == EINVAL) { + setsockopt(listenFd, IPPROTO_IP, IP_RECVTOS, &enabled, sizeof(enabled)); } } diff --git a/src/errno/windows_errno.zig b/src/errno/windows_errno.zig index 3e729f77e792..25f7641b66dc 100644 --- a/src/errno/windows_errno.zig +++ b/src/errno/windows_errno.zig @@ -1073,6 +1073,7 @@ pub const SystemErrno = enum(u16) { Win32Error.DIR_NOT_EMPTY => SystemErrno.ENOTEMPTY, Win32Error.WSAENOTSOCK => SystemErrno.ENOTSOCK, Win32Error.NOT_SUPPORTED => SystemErrno.ENOTSUP, + Win32Error.WSAEOPNOTSUPP => SystemErrno.ENOTSUP, Win32Error.BROKEN_PIPE => SystemErrno.EPIPE, Win32Error.ACCESS_DENIED => SystemErrno.EPERM, Win32Error.PRIVILEGE_NOT_HELD => SystemErrno.EPERM, diff --git a/test/regression/issue/28083.test.ts b/test/regression/issue/28083.test.ts new file mode 100644 index 000000000000..088414731f39 --- /dev/null +++ b/test/regression/issue/28083.test.ts @@ -0,0 +1,268 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows } from "harness"; + +describe("dgram implicit bind on send", () => { + test("reusePort option works with dgram sockets", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const isWin = process.platform === "win32"; + + // Create two sockets with reusePort: true bound to the same port + const s1 = dgram.createSocket({ type: "udp4", reusePort: !isWin }); + const s2 = dgram.createSocket({ type: "udp4", reusePort: !isWin }); + + s1.bind(0, "127.0.0.1", () => { + const port = s1.address().port; + process.stdout.write("s1:" + port + "\\n"); + + if (isWin) { + // On Windows, reusePort is not supported - just verify s1 works + process.stdout.write("ok\\n"); + s1.close(); + s2.close(); + return; + } + + // On non-Windows, second socket should be able to bind to the same port + s2.bind(port, "127.0.0.1", () => { + const port2 = s2.address().port; + process.stdout.write("s2:" + port2 + "\\n"); + process.stdout.write("same:" + String(port === port2) + "\\n"); + s1.close(); + s2.close(); + }); + + s2.on("error", (err) => { + process.stdout.write("s2error:" + err.message + "\\n"); + s1.close(); + s2.close(); + process.exit(1); + }); + }); + + s1.on("error", (err) => { + process.stdout.write("s1error:" + err.message + "\\n"); + s1.close(); + process.exit(1); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + if (isWindows) { + expect(stdout).toContain("ok\n"); + } else { + expect(stdout).toContain("same:true\n"); + } + expect(exitCode).toBe(0); + }); + + test("send() without bind() implicitly binds and delivers the message", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const receiver = dgram.createSocket("udp4"); + const sender = dgram.createSocket("udp4"); + + receiver.bind(0, "127.0.0.1", () => { + const port = receiver.address().port; + + receiver.on("message", (msg, rinfo) => { + process.stdout.write(msg.toString() + "\\n"); + process.stdout.write(String(rinfo.port > 0) + "\\n"); + sender.close(); + receiver.close(); + }); + + sender.send(Buffer.from("hello"), 0, 5, port, "127.0.0.1", (err) => { + if (err) { + process.stdout.write("ERROR:" + err.message + "\\n"); + process.exit(1); + } + const addr = sender.address(); + process.stdout.write(addr.address + "\\n"); + process.stdout.write(String(addr.port > 0) + "\\n"); + }); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("0.0.0.0\ntrue\nhello\ntrue\n"); + expect(exitCode).toBe(0); + }); + + test("listening event fires after implicit bind", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const socket = dgram.createSocket("udp4"); + const target = dgram.createSocket("udp4"); + let listeningFired = false; + + socket.on("listening", () => { + listeningFired = true; + }); + + target.bind(0, "127.0.0.1", () => { + const port = target.address().port; + socket.send(Buffer.from("test"), 0, 4, port, "127.0.0.1", (err) => { + process.nextTick(() => { + process.stdout.write(String(listeningFired) + "\\n"); + socket.close(); + target.close(); + }); + }); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("true\n"); + expect(exitCode).toBe(0); + }); + + test("multiple sends without bind() are all delivered", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const receiver = dgram.createSocket("udp4"); + const sender = dgram.createSocket("udp4"); + const messages = []; + + receiver.bind(0, "127.0.0.1", () => { + const port = receiver.address().port; + + receiver.on("message", (msg) => { + messages.push(msg.toString()); + if (messages.length === 3) { + messages.sort(); + process.stdout.write(messages.join(",") + "\\n"); + sender.close(); + receiver.close(); + } + }); + + sender.send(Buffer.from("aaa"), 0, 3, port, "127.0.0.1"); + sender.send(Buffer.from("bbb"), 0, 3, port, "127.0.0.1"); + sender.send(Buffer.from("ccc"), 0, 3, port, "127.0.0.1"); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("aaa,bbb,ccc\n"); + expect(exitCode).toBe(0); + }); + + test("send(buffer, port, address, callback) short form works without bind", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const receiver = dgram.createSocket("udp4"); + const sender = dgram.createSocket("udp4"); + + receiver.bind(0, "127.0.0.1", () => { + const port = receiver.address().port; + + receiver.on("message", (msg) => { + process.stdout.write(msg.toString() + "\\n"); + sender.close(); + receiver.close(); + }); + + sender.send(Buffer.from("short-form"), port, "127.0.0.1", (err) => { + if (err) { + process.stdout.write("ERROR:" + err.message + "\\n"); + process.exit(1); + } + }); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("short-form\n"); + expect(exitCode).toBe(0); + }); + + test("bidirectional communication works with implicit bind (k-rpc pattern)", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const dgram = require("dgram"); + const receiver = dgram.createSocket("udp4"); + const sender = dgram.createSocket("udp4"); + + sender.on("message", (msg) => { + process.stdout.write("reply:" + msg.toString() + "\\n"); + sender.close(); + receiver.close(); + }); + + receiver.bind(0, "127.0.0.1", () => { + const port = receiver.address().port; + + receiver.on("message", (msg, rinfo) => { + process.stdout.write("request:" + msg.toString() + "\\n"); + receiver.send(Buffer.from("pong"), 0, 4, rinfo.port, rinfo.address); + }); + + sender.send(Buffer.from("ping"), 0, 4, port, "127.0.0.1"); + }); + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(stdout).toBe("request:ping\nreply:pong\n"); + expect(exitCode).toBe(0); + }); +});