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
6 changes: 3 additions & 3 deletions packages/bun-usockets/src/bsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int
struct addrinfo *listenAddr;
for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
if (a->ai_family == AF_INET6) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL);
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, error);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}
Expand All @@ -1385,7 +1385,7 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int

for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
if (a->ai_family == AF_INET) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL);
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, error);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}
Expand Down Expand Up @@ -1545,7 +1545,7 @@ static LIBUS_SOCKET_DESCRIPTOR bsd_create_unix_socket_address(const char *path,
static LIBUS_SOCKET_DESCRIPTOR internal_bsd_create_listen_socket_unix(const char* path, int options, struct sockaddr_un* server_address, size_t addrlen, int* error) {
LIBUS_SOCKET_DESCRIPTOR listenFd = LIBUS_SOCKET_ERROR;

listenFd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0, NULL);
listenFd = bsd_create_socket(AF_UNIX, SOCK_STREAM, 0, error);

if (listenFd == LIBUS_SOCKET_ERROR) {
return LIBUS_SOCKET_ERROR;
Expand Down
24 changes: 17 additions & 7 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3908,11 +3908,9 @@ Server.prototype.listen = function listen(port, hostname, onListen) {
);
} catch (err) {
const isUnix = path != null;
process.nextTick(
emitErrorNextTick,
this,
formatListenError(err, isUnix ? path : hostname, isUnix ? undefined : port),
);
// Bun.listen reports the address it tried. After the IPv4 fallback that is not `hostname`.
const address = isUnix ? path : typeof err?.address === "string" && err.address ? err.address : hostname;
process.nextTick(emitErrorNextTick, this, formatListenError(err, address, isUnix ? undefined : port));
}
return this;
};
Expand All @@ -3930,6 +3928,7 @@ Server.prototype[kRealListen] = function (
contexts,
_onListen,
fd,
hostDefaulted,
) {
// NOTE: accepted sockets are always allowHalfOpen:true at the native layer
// (hardcoded below); the stream layer implements allowHalfOpen=false
Expand Down Expand Up @@ -3985,7 +3984,7 @@ Server.prototype[kRealListen] = function (
pauseOnConnect: this.pauseOnConnect,
});
} else {
this._handle = Bun.listen({
const listenOptions = {
port,
hostname,
tls,
Expand All @@ -3996,7 +3995,15 @@ Server.prototype[kRealListen] = function (
socket: serverHandlersFor(this),
data: this,
pauseOnConnect: this.pauseOnConnect,
});
};
try {
this._handle = Bun.listen(listenOptions);
} catch (err) {
// Node binds "::" when no host is given and falls back to "0.0.0.0" on a kernel without IPv6.
if (!hostDefaulted || err?.code !== "EAFNOSUPPORT") throw err;
Comment thread
robobun marked this conversation as resolved.
listenOptions.hostname = "0.0.0.0";
this._handle = Bun.listen(listenOptions);
Comment thread
robobun marked this conversation as resolved.
}
}

this._handle[owner_symbol] = this;
Expand Down Expand Up @@ -4152,6 +4159,7 @@ function listenInCluster(
contexts,
onListen,
fd,
address == null,
);
return;
}
Expand Down Expand Up @@ -4368,6 +4376,8 @@ function uvListenErrorDescription(code) {
return "address not available";
case "EINVAL":
return "invalid argument";
case "EAFNOSUPPORT":
return "address family not supported";
default:
return undefined;
}
Expand Down
124 changes: 124 additions & 0 deletions test/js/node/net/node-net-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,130 @@ describe("net.createServer listen", () => {
);
});

// A kernel without IPv6 (ipv6.disable=1) fails socket(AF_INET6) with EAFNOSUPPORT. Node binds
// "::" when listen() gets no host and falls back to "0.0.0.0" there. A seccomp filter in the
// child gives the same socket() result without a special kernel.
it.skipIf(process.platform !== "linux" || (process.arch !== "x64" && process.arch !== "arm64"))(
"should fall back to 0.0.0.0 when no host is given and the kernel has no IPv6",
async () => {
using dir = tempDir("net-listen-no-ipv6", {
"deny-socket.c": `
typedef unsigned short u16; typedef unsigned char u8; typedef unsigned int u32;
struct sock_filter { u16 code; u8 jt; u8 jf; u32 k; };
struct sock_fprog { u16 len; struct sock_filter *filter; };
int prctl(int option, ...);
#if defined(__x86_64__)
#define NR_SOCKET 41
#else
#define NR_SOCKET 198
#endif
/* Every later socket(family, ...) fails with err. Filters stack, so this can run twice. */
int deny_socket_family(int family, int err) {
struct sock_filter f[] = {
{ 0x20, 0, 0, 0 }, /* A = nr (BPF_LD|BPF_W|BPF_ABS) */
{ 0x15, 0, 3, NR_SOCKET }, /* if nr != socket: allow (BPF_JMP|BPF_JEQ|BPF_K) */
{ 0x20, 0, 0, 16 }, /* A = args[0] (family) */
{ 0x15, 0, 1, (u32)family }, /* if A != family: allow */
{ 0x06, 0, 0, 0x00050000 | (u32)err }, /* SECCOMP_RET_ERRNO | err (BPF_RET|BPF_K) */
{ 0x06, 0, 0, 0x7fff0000 }, /* SECCOMP_RET_ALLOW */
};
struct sock_fprog prog = { sizeof(f) / sizeof(f[0]), f };
if (prctl(38 /* PR_SET_NO_NEW_PRIVS */, 1, 0, 0, 0) != 0) return -1;
if (prctl(22 /* PR_SET_SECCOMP */, 2 /* SECCOMP_MODE_FILTER */, &prog) != 0) return -2;
return 0;
}
`,
"fixture.ts": `
import { cc } from "bun:ffi";
import { once } from "node:events";
import { createServer, type AddressInfo } from "node:net";
import { join } from "node:path";

const { symbols } = cc({
source: join(import.meta.dir, "deny-socket.c"),
symbols: { deny_socket_family: { args: ["i32", "i32"], returns: "i32" } },
});
const AF_UNIX = 1, AF_INET6 = 10, EMFILE = 24, EAFNOSUPPORT = 97;
const result: Record<string, unknown> = { seccomp: symbols.deny_socket_family(AF_INET6, EAFNOSUPPORT) };
if (result.seccomp !== 0) {
console.log(JSON.stringify(result));
process.exit(0);
}
// once("listening") rejects with the "error" event, so this settles either way.
const listenError = async (...args: any[]) => {
const server = createServer().listen(...args);
try {
await once(server, "listening");
} catch (err: any) {
return { code: err.code, syscall: err.syscall, address: err.address, message: err.message };
}
const listening = server.address();
server.close();
return { listening };
};

const noHost = createServer().listen(0);
await once(noHost, "listening");
const { address, family, port } = noHost.address() as AddressInfo;
result.noHost = { address, family };
// The fallback's own failure names the address that failed.
const busy = await listenError(port);
result.noHostPortBusy = { ...busy, message: busy.message.replace(String(port), "PORT") };
noHost.close();

result.explicitIPv6 = await listenError(0, "::");
try {
Bun.listen({ hostname: "::", port: 0, socket: { data() {} } }).stop();
result.bunListen = "listening";
} catch (e: any) {
result.bunListen = { code: e.code, syscall: e.syscall, errno: typeof e.errno };
}

// The same lost errno on the unix socket path.
result.seccompUnix = symbols.deny_socket_family(AF_UNIX, EMFILE);
const { code, syscall } = await listenError(join(import.meta.dir, "listen.sock"));
result.unix = { code, syscall };
console.log(JSON.stringify(result));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const result = JSON.parse(stdout);
if (result.seccomp !== 0) {
// bun:test has no runtime skip. Say so, to tell this apart from a pass.
console.warn("SKIP net listen without IPv6: seccomp filter not permitted here (" + result.seccomp + ")");
return;
}
expect(result).toEqual({
seccomp: 0,
noHost: { address: "0.0.0.0", family: "IPv4" },
Comment thread
robobun marked this conversation as resolved.
noHostPortBusy: {
code: "EADDRINUSE",
syscall: "listen",
address: "0.0.0.0",
message: "listen EADDRINUSE: address already in use 0.0.0.0:PORT",
},
explicitIPv6: {
code: "EAFNOSUPPORT",
syscall: "listen",
address: "::",
message: "listen EAFNOSUPPORT: address family not supported ::",
},
bunListen: { code: "EAFNOSUPPORT", syscall: "listen", errno: "number" },
seccompUnix: 0,
unix: { code: "EMFILE", syscall: "listen" },
});
expect(exitCode).toBe(0);
},
);

it("should call listening", done => {
const { mustCall } = createCallCheckCtx(done);

Expand Down
Loading