diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 23f1c6d3ba98df..a4c2cd1fe64a36 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3734,7 +3734,7 @@ declare module "bun" { port?: string | number; /** - * If the `SO_REUSEPORT` flag should be set. + * Whether the `SO_REUSEPORT` flag should be set. * * This allows multiple processes to bind to the same port, which is useful for load balancing. * @@ -3742,6 +3742,12 @@ declare module "bun" { */ reusePort?: boolean; + /** + * Whether the `IPV6_V6ONLY` flag should be set. + * @default false + */ + ipv6Only?: boolean; + /** * What hostname should the server listen on? * diff --git a/packages/bun-usockets/src/bsd.c b/packages/bun-usockets/src/bsd.c index c11d88495b1c79..8bc993bf945ef3 100644 --- a/packages/bun-usockets/src/bsd.c +++ b/packages/bun-usockets/src/bsd.c @@ -801,6 +801,66 @@ static int us_internal_bind_and_listen(LIBUS_SOCKET_DESCRIPTOR listenFd, struct return result; } +static int bsd_set_reuseaddr(LIBUS_SOCKET_DESCRIPTOR listenFd) { + const int one = 1; +#if defined(SO_REUSEPORT) && !defined(__linux__) && !defined(__GNU__) + return setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); +#else + return setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); +#endif +} + +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. + const int one = 1; + return setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); +#else +#if _WIN32 + WSASetLastError(WSAEOPNOTSUPP); +#endif + errno = ENOTSUP; + return -1; +#endif +} + +static int bsd_set_reuse(LIBUS_SOCKET_DESCRIPTOR listenFd, int options) { + int result = 0; + + if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { +#if _WIN32 + const int one = 1; + result = setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &one, sizeof(one)); + if (result != 0) { + return result; + } +#endif + } + + if ((options & LIBUS_LISTEN_REUSE_ADDR)) { + result = bsd_set_reuseaddr(listenFd); + if (result != 0) { + return result; + } + } + + if ((options & LIBUS_LISTEN_REUSE_PORT)) { + result = bsd_set_reuseport(listenFd); + if (result != 0) { + if (errno == ENOTSUP) { + if ((options & LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE) == 0) { + errno = 0; + return 0; + } + } + + return result; + } + } + + return 0; +} + inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd( LIBUS_SOCKET_DESCRIPTOR listenFd, struct addrinfo *listenAddr, @@ -809,44 +869,26 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd int* error ) { - if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { -#if _WIN32 - int optval2 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &optval2, sizeof(optval2)); -#endif - } else { - if((options & LIBUS_LISTEN_REUSE_PORT)) { - int optval2 = 1; -#if defined(SO_REUSEPORT) - setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &optval2, sizeof(optval2)); -#else - setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval2, sizeof(optval2)); -#endif - } + if (bsd_set_reuse(listenFd, options) != 0) { + return LIBUS_SOCKET_ERROR; } -#if defined(SO_REUSEADDR) - #ifndef _WIN32 - +#if defined(SO_REUSEADDR) && !_WIN32 // Unlike on Unix, here we don't set SO_REUSEADDR, because it doesn't just // allow binding to addresses that are in use by sockets in TIME_WAIT, it // effectively allows 'stealing' a port which is in use by another application. // See libuv issue #1360. - - - int optval3 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval3, sizeof(optval3)); - #endif + int one = 1; + setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); #endif #ifdef IPV6_V6ONLY - // TODO: revise support to match node.js - // if (listenAddr->ai_family == AF_INET6) { - // int disabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; - // setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); - // } - int disabled = 0; - setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); + if (listenAddr->ai_family == AF_INET6) { + int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; + if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) { + return LIBUS_SOCKET_ERROR; + } + } #endif if (us_internal_bind_and_listen(listenFd, listenAddr->ai_addr, (socklen_t) listenAddr->ai_addrlen, 512, error)) { @@ -1105,26 +1147,17 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)); } - if ((options & LIBUS_LISTEN_EXCLUSIVE_PORT)) { -#if _WIN32 - int optval2 = 1; - setsockopt(listenFd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, &optval2, sizeof(optval2)); -#endif - } else { - if((options & LIBUS_LISTEN_REUSE_PORT)) { - int optval2 = 1; -#if defined(SO_REUSEPORT) - setsockopt(listenFd, SOL_SOCKET, SO_REUSEPORT, &optval2, sizeof(optval2)); -#else - setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &optval2, sizeof(optval2)); -#endif - } + if (bsd_set_reuse(listenFd, options) != 0) { + freeaddrinfo(result); + return LIBUS_SOCKET_ERROR; } #ifdef IPV6_V6ONLY if (listenAddr->ai_family == AF_INET6) { - int disabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; - setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &disabled, sizeof(disabled)); + int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0; + if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) { + return LIBUS_SOCKET_ERROR; + } } #endif diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index dd27d70eeebc56..25707af5327b91 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -100,6 +100,8 @@ enum { LIBUS_LISTEN_REUSE_PORT = 4, /* Setting ipv6Only will disable dual-stack support, i.e., binding to host :: won't make 0.0.0.0 be bound.*/ LIBUS_SOCKET_IPV6_ONLY = 8, + LIBUS_LISTEN_REUSE_ADDR = 16, + LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE = 32, }; /* Library types publicly available */ diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 5b35669156203e..2c1ef86079ae1b 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -614,7 +614,13 @@ pub const Listener = struct { const ssl_enabled = ssl != null; - var socket_flags: i32 = if (exclusive) uws.LIBUS_LISTEN_EXCLUSIVE_PORT else (if (socket_config.reusePort) uws.LIBUS_SOCKET_REUSE_PORT else uws.LIBUS_LISTEN_DEFAULT); + var socket_flags: i32 = if (exclusive) + uws.LIBUS_LISTEN_EXCLUSIVE_PORT + else if (socket_config.reusePort) + uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR + else + uws.LIBUS_LISTEN_DEFAULT; + if (socket_config.allowHalfOpen) { socket_flags |= uws.LIBUS_SOCKET_ALLOW_HALF_OPEN; } diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index f6f95f30a95162..c658d4e0bcfc47 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -270,6 +270,7 @@ pub const ServerConfig = struct { reuse_port: bool = false, id: []const u8 = "", allow_hot: bool = true, + ipv6_only: bool = false, static_routes: std.ArrayList(StaticRouteEntry) = std.ArrayList(StaticRouteEntry).init(bun.default_allocator), @@ -449,6 +450,20 @@ pub const ServerConfig = struct { return arraylist.items; } + pub fn getUsocketsOptions(this: *const ServerConfig) i32 { + // Unlike Node.js, we set exclusive port in case reuse port is not set + var out: i32 = if (this.reuse_port) + uws.LIBUS_LISTEN_REUSE_PORT | uws.LIBUS_LISTEN_REUSE_ADDR + else + uws.LIBUS_LISTEN_EXCLUSIVE_PORT; + + if (this.ipv6_only) { + out |= uws.LIBUS_SOCKET_IPV6_ONLY; + } + + return out; + } + pub const SSLConfig = struct { requires_custom_request_ctx: bool = false, server_name: [*c]const u8 = null, @@ -1279,6 +1294,11 @@ pub const ServerConfig = struct { } if (global.hasException()) return error.JSError; + if (try arg.get(global, "ipv6Only")) |dev| { + args.ipv6_only = dev.coerce(bool, global); + } + if (global.hasException()) return error.JSError; + if (try arg.get(global, "inspector")) |inspector| { args.inspector = inspector.coerce(bool, global); @@ -7554,8 +7574,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp app.listenWithConfig(*ThisServer, this, onListen, .{ .port = tcp.port, .host = host, - // IPV6_ONLY is the default for bun, different from node it also set exclusive port in case reuse port is not set - .options = (if (this.config.reuse_port) uws.LIBUS_SOCKET_REUSE_PORT else uws.LIBUS_LISTEN_EXCLUSIVE_PORT) | uws.LIBUS_SOCKET_IPV6_ONLY, + .options = this.config.getUsocketsOptions(), }); }, @@ -7565,8 +7584,7 @@ pub fn NewServer(comptime NamespaceType: type, comptime ssl_enabled_: bool, comp this, onListen, unix, - // IPV6_ONLY is the default for bun, different from node it also set exclusive port in case reuse port is not set - (if (this.config.reuse_port) uws.LIBUS_SOCKET_REUSE_PORT else uws.LIBUS_LISTEN_EXCLUSIVE_PORT) | uws.LIBUS_SOCKET_IPV6_ONLY, + this.config.getUsocketsOptions(), ); }, } diff --git a/src/deps/uws.zig b/src/deps/uws.zig index 30b90a74048551..b5d02fc1c195f5 100644 --- a/src/deps/uws.zig +++ b/src/deps/uws.zig @@ -10,8 +10,10 @@ pub const u_int64_t = c_ulonglong; pub const LIBUS_LISTEN_DEFAULT: i32 = 0; pub const LIBUS_LISTEN_EXCLUSIVE_PORT: i32 = 1; pub const LIBUS_SOCKET_ALLOW_HALF_OPEN: i32 = 2; -pub const LIBUS_SOCKET_REUSE_PORT: i32 = 4; +pub const LIBUS_LISTEN_REUSE_PORT: i32 = 4; pub const LIBUS_SOCKET_IPV6_ONLY: i32 = 8; +pub const LIBUS_LISTEN_REUSE_ADDR: i32 = 16; +pub const LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE: i32 = 32; pub const Socket = opaque { pub fn write2(this: *Socket, first: []const u8, second: []const u8) i32 { diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index e68678c2eab263..8fdc4c6e11158d 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -33,8 +33,10 @@ const SEND_BUFFER = false; const LIBUS_LISTEN_DEFAULT = 0; const LIBUS_LISTEN_EXCLUSIVE_PORT = 1; const LIBUS_SOCKET_ALLOW_HALF_OPEN = 2; -const LIBUS_SOCKET_REUSE_PORT = 4; +const LIBUS_LISTEN_REUSE_PORT = 4; const LIBUS_SOCKET_IPV6_ONLY = 8; +const LIBUS_LISTEN_REUSE_ADDR = 16; +const LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE = 32; const kStateSymbol = Symbol("state symbol"); const kOwnerSymbol = Symbol("owner symbol"); @@ -165,7 +167,7 @@ function Socket(type, listener) { bindState: BIND_STATE_UNBOUND, connectState: CONNECT_STATE_DISCONNECTED, queue: undefined, - reuseAddr: options && options.reuseAddr, // Use UV_UDP_REUSEADDR if true. + reuseAddr: options && options.reuseAddr, reusePort: options && options.reusePort, ipv6Only: options && options.ipv6Only, recvBufferSize, @@ -301,10 +303,10 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { return; } - let flags = 0; + let flags = LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE; if (state.reuseAddr) { - flags |= 0; //UV_UDP_REUSEADDR; + flags |= LIBUS_LISTEN_REUSE_ADDR; } if (state.ipv6Only) { @@ -313,9 +315,7 @@ Socket.prototype.bind = function (port_, address_ /* , callback */) { if (state.reusePort) { exclusive = true; // TODO: cluster support - flags |= LIBUS_SOCKET_REUSE_PORT; - } else { - flags |= LIBUS_LISTEN_EXCLUSIVE_PORT; + flags |= LIBUS_LISTEN_REUSE_PORT; } // TODO flags diff --git a/test/js/node/test/parallel/test-dgram-reuseport.js b/test/js/node/test/parallel/test-dgram-reuseport.js index e5fd6965818d4c..c9b6f7964ab666 100644 --- a/test/js/node/test/parallel/test-dgram-reuseport.js +++ b/test/js/node/test/parallel/test-dgram-reuseport.js @@ -12,6 +12,8 @@ function test() { socket2.close(); })); })); + socket1.on('error', common.mustNotCall()); + socket2.on('error', common.mustNotCall()); } checkSupportReusePort().then(test, () => {