Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/bun-usockets/src/bsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,7 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd
) {

if (bsd_set_reuse(listenFd, options) != 0) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}

Expand All @@ -1163,6 +1164,7 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd
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) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}
}
Expand Down Expand Up @@ -1193,6 +1195,23 @@ int bsd_set_defer_accept(LIBUS_SOCKET_DESCRIPTOR listenFd) {
#endif
}

#ifdef _WIN32
#define LIBUS_ADDRINUSE WSAEADDRINUSE
#define LIBUS_ACCES WSAEACCES
#else
#define LIBUS_ADDRINUSE EADDRINUSE
#define LIBUS_ACCES EACCES
#endif

/* bind() failures that mean "this port is owned by someone else" rather than
* "this address family is unusable on this host". The IPv6->IPv4 fallback below
* exists for the latter (EAFNOSUPPORT / EADDRNOTAVAIL when IPv6 is disabled),
* and must not swallow the former: a second listen(port, "localhost") would
* otherwise quietly bind 127.0.0.1 after ::1 returned EADDRINUSE. */
static int bsd_bind_error_is_fatal(int err) {
return err == LIBUS_ADDRINUSE || err == LIBUS_ACCES;
}

// return LIBUS_SOCKET_ERROR or the fd that represents listen socket
// listen both on ipv6 and ipv4
LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error) {
Expand Down Expand Up @@ -1220,12 +1239,17 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int
}

listenAddr = a;
*error = 0;
if (bsd_bind_listen_fd(listenFd, listenAddr, port, options, error) != LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return listenFd;
}

bsd_close_socket(listenFd);
if (bsd_bind_error_is_fatal(*error)) {
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}

Expand All @@ -1237,12 +1261,17 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int
}

listenAddr = a;
*error = 0;
if (bsd_bind_listen_fd(listenFd, listenAddr, port, options, error) != LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return listenFd;
}

bsd_close_socket(listenFd);
if (bsd_bind_error_is_fatal(*error)) {
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/js/internal/cluster/RoundRobinHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
// Currently, net module only supports `ipv6Only` option in `flags`.
ipv6Only: !!(flags & UV_TCP_IPV6ONLY),
backlog,
// Workers bind this port directly; open the reuse group for them.
reusePort: true,

Check warning on line 41 in src/js/internal/cluster/RoundRobinHandle.ts

View check run for this annotation

Claude / Claude Code Review

reusePort:true on RoundRobinHandle lets a second cluster primary silently share the port on Linux

Worth noting (not blocking): with `reusePort: true` on both the primary's RoundRobinHandle and the worker's bind, two *independent* clustered `net.Server` processes started on the same port by the same UID on Linux will now both bind successfully and the kernel will silently split traffic between them, instead of the second seeing `EADDRINUSE` as Node does. This is the same tradeoff Bun already accepts for `http.Server` in a cluster (`_http_server.ts:655`) and there's no in-PR fix short of the #
Comment thread
robobun marked this conversation as resolved.
Resolved
});
} else
this.server.listen({
Expand Down
3 changes: 2 additions & 1 deletion src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4032,7 +4032,8 @@ function listenInCluster(
hostname,
exclusive,
ipv6Only,
reusePort,
// Workers share the port via SO_REUSEPORT; matches _http_server.ts.
true /* reusePort */,
readableAll,
writableAll,
tls,
Expand Down
22 changes: 22 additions & 0 deletions test/js/node/http/node-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,28 @@ describe("node:http", () => {
const err = await promise;
expect(err.code).toBe("EADDRINUSE");
});

// https://github.com/oven-sh/bun/issues/15560
test("listen(port, 'localhost') reports EADDRINUSE instead of silently binding the other loopback family", async () => {
const a = createServer();
const b = createServer();
try {
await new Promise<void>((resolve, reject) => {
a.on("error", reject);
a.listen(0, "localhost", () => resolve());
});
const port = a.address().port;

const err = await new Promise<NodeJS.ErrnoException>((resolve, reject) => {
b.on("error", resolve);
b.listen(port, "localhost", () => reject(new Error(`second listen bound ${JSON.stringify(b.address())}`)));
});
expect(err.code).toBe("EADDRINUSE");
} finally {
b.close();
a.close();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});
});

describe("node https server", async () => {
Expand Down
22 changes: 22 additions & 0 deletions test/js/node/net/node-net.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1870,3 +1870,25 @@ it.skipIf(isWindows)("connect({ localPort }) succeeds when the local port has TI
target.close();
}
});

// https://github.com/oven-sh/bun/issues/15560
it('listen(port, "localhost") reports EADDRINUSE when the port is already held on localhost', async () => {
const a = createServer();
const b = createServer();
try {
await new Promise<void>((resolve, reject) => {
a.on("error", reject);
a.listen(0, "localhost", resolve);
});
const port = (a.address() as import("node:net").AddressInfo).port;

const err = await new Promise<NodeJS.ErrnoException>((resolve, reject) => {
b.on("error", resolve);
b.listen(port, "localhost", () => reject(new Error(`second listen bound ${JSON.stringify(b.address())}`)));
});
expect(err.code).toBe("EADDRINUSE");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
} finally {
b.close();
a.close();
}
});
Loading