diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index f939744f41bf5..b031e4fd03dc2 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -22,8 +22,10 @@ // net.BoundSocket when the runtime offers it, else the private tcp_wrap binding // as a fallback (net.Server's listen is async and cannot report an assigned // ephemeral port up front, so it can't drive bind on its own). connect() goes -// through net.Socket, adopting the bound handle when one exists so an explicit -// source address/port is honored, and otherwise letting the kernel assign one. +// through net.Socket, adopting the bound handle so an explicit source +// address/port is honored; an unbound client binds an ephemeral port first, +// since the kernel assigns the source port synchronously at connect() and +// getsockname() must report it immediately. // // UDP uses the public node:dgram socket when it exposes a synchronous bindSync // (a recent node addition that ships alongside connectSync), giving the @@ -573,15 +575,16 @@ var NodeSockFSLibrary = { sock.dport = port; sock.state = {{{ SOCK_STATE_CONNECTING }}}; var net = nodeSockHelpers.getNet(); - var conn; - if (sock.bound) { - // A prior bind() produced a real, already-bound handle; connect through - // it so the bound source address/port is honored by the kernel. - conn = new net.Socket({ handle: sock.bound, pauseOnCreate: true, allowHalfOpen: true }); - } else { - // Unbound client: let the kernel assign the source address/port. - conn = new net.Socket({ allowHalfOpen: true }); - } + if (!sock.bound) { + // The kernel assigns the ephemeral source port synchronously at + // connect(), so an unbound client binds an ephemeral port first (the + // same eager bindHandle path an explicit bind() takes) and getsockname() + // is correct immediately, not only once the async connect completes. + nodeSockHelpers.bindHandle(sock, addr.includes(':') ? '::' : '0.0.0.0', 0); + } + // Connect through the bound handle so the bound source address/port is + // honored by the kernel. + var conn = new net.Socket({ handle: sock.bound, pauseOnCreate: true, allowHalfOpen: true }); conn.once('connect', () => { sock.state = {{{ SOCK_STATE_CONNECTED }}}; sock.saddr = conn.localAddress; diff --git a/test/sockets/test_tcp_connect_getsockname.c b/test/sockets/test_tcp_connect_getsockname.c new file mode 100644 index 0000000000000..0502a477922d2 --- /dev/null +++ b/test/sockets/test_tcp_connect_getsockname.c @@ -0,0 +1,151 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * getsockname() immediately after a non-blocking connect() on an unbound + * client must report the kernel-assigned ephemeral source port: the kernel + * assigns it synchronously at connect(), not when the connection completes. + * The port must then stay the same once connected. This is plain POSIX and + * also builds and runs natively, so the same code can be checked against the + * host stack. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int listen_fd = -1; +int client_fd = -1; +int peer_fd = -1; +struct sockaddr_in dest; +bool connected = false; +uint16_t client_port = 0; // network order, recorded right after connect() + +void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, O_NONBLOCK); +} + +void test_success(void) { + printf("done\n"); + if (listen_fd >= 0) close(listen_fd); + if (client_fd >= 0) close(client_fd); + if (peer_fd >= 0) close(peer_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); +#else + exit(0); +#endif +} + +void start_client(void) { + if (client_fd >= 0) close(client_fd); + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + set_nonblocking(client_fd); + connected = false; + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + assert((r == 0 || errno == EINPROGRESS) && "connect"); + + // The ephemeral source port is assigned synchronously at connect(): it must + // be visible here, before the connection completes or any event turn runs. + struct sockaddr_in sa; + socklen_t sl = sizeof(sa); + int g = getsockname(client_fd, (struct sockaddr*)&sa, &sl); + assert(g == 0 && "getsockname after connect"); + assert(ntohs(sa.sin_port) != 0 && "ephemeral port assigned at connect()"); + client_port = sa.sin_port; + printf("connecting from port %u\n", (unsigned)ntohs(client_port)); +} + +void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(listen_fd, &fdr); + FD_SET(client_fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + if (peer_fd < 0 && FD_ISSET(listen_fd, &fdr)) { + peer_fd = accept(listen_fd, NULL, NULL); + if (peer_fd >= 0) set_nonblocking(peer_fd); + } + + if (!connected && FD_ISSET(client_fd, &fdw)) { + int err = 0; + socklen_t l = sizeof(err); + getsockopt(client_fd, SOL_SOCKET, SO_ERROR, &err, &l); + if (err == ECONNREFUSED || err == ECONNRESET) { + start_client(); + return; + } + assert(err == 0 && "connect failed"); + connected = true; + + // The name must not change when the connection completes. + struct sockaddr_in sa; + socklen_t sl = sizeof(sa); + assert(getsockname(client_fd, (struct sockaddr*)&sa, &sl) == 0); + assert(sa.sin_port == client_port && "port stable across connect completion"); + test_success(); + } +} + +int main(void) { + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + if (listen(listen_fd, 4) != 0) { + perror("listen"); + return 1; + } + + struct sockaddr_in la; + socklen_t ll = sizeof(la); + if (getsockname(listen_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + set_nonblocking(listen_fd); + + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = la.sin_port; + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + start_client(); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/test_sockets_node.py b/test/test_sockets_node.py index 97750c862641c..16ec7db2815aa 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -126,6 +126,12 @@ def test_noderawsockets_client_bind(self): server.server_close() thread.join() + def test_noderawsockets_connect_getsockname(self): + # getsockname() immediately after a non-blocking connect() on an unbound + # client reports the ephemeral source port synchronously (kernel semantics: + # the port is assigned at connect(), not when the connection completes). + self.do_runf('sockets/test_tcp_connect_getsockname.c', 'done\n', cflags=['-sNODERAWSOCKETS']) + def test_noderawsockets_client_semantics(self): # EISCONN on a second connect, shutdown(SHUT_WR) leaving reads working, # EPIPE on a write after that, and POLLHUP after a full shutdown(SHUT_RDWR).