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
25 changes: 14 additions & 11 deletions src/lib/libsockfs_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
151 changes: 151 additions & 0 deletions test/sockets/test_tcp_connect_getsockname.c
Original file line number Diff line number Diff line change
@@ -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 <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#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;
}
6 changes: 6 additions & 0 deletions test/test_sockets_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading