diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index c7ffe7520e975..b7dbf34a5a812 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -126,6 +126,24 @@ addToLibrary({ }, // node and stream ops are backend agnostic stream_ops: { + getattr(stream) { + var node = stream.node; + return { + dev: 1, + ino: node.id, + mode: {{{ cDefs.S_IFSOCK }}} | 0o777, + nlink: 1, + uid: 0, + gid: 0, + rdev: 0, + size: 0, + atime: new Date(0), + mtime: new Date(0), + ctime: new Date(0), + blksize: 4096, + blocks: 0, + }; + }, poll(stream) { var sock = stream.node.sock; return sock.sock_ops.poll(sock); diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index b36049e6d45fe..6622a55410ecd 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -387,7 +387,26 @@ var NodeSockFSLibrary = { sock.udp = null; } if (sock.server) { try { sock.server.close(); } catch (e) {} sock.server = null; } - if (sock.connection) { try { sock.connection.destroy(); } catch (e) {} sock.connection = null; } + if (sock.connection) { + var conn = sock.connection; + var linger = sock.opts?.linger; + try { + if (linger?.onoff && linger.linger === 0 && conn.resetAndDestroy) { + // SO_LINGER with a zero timeout: abortive close - send RST and + // discard any unsent data. + conn.resetAndDestroy(); + } else if (linger?.onoff && linger.linger > 0) { + // Positive timeout: flush gracefully, but node has no blocking + // close, so force the connection down once the interval elapses. + conn.end(); + var timer = setTimeout(() => { try { conn.destroy(); } catch (e) {} }, linger.linger * 1000); + timer.unref?.(); + } else { + conn.destroy(); + } + } catch (e) {} + sock.connection = null; + } // A bound handle that was never adopted by listen()/connect() is ours to // release; once adopted the server/connection owns it. if (sock.bound && !sock.server && !sock.connection) { @@ -690,6 +709,12 @@ var NodeSockFSLibrary = { // passed to the BoundSocket at bind. Set after bind has no effect. sock.opts.reusePort = !!val; return 0; + case 13: // SO_LINGER (struct linger: l_onoff, l_linger) + sock.opts.linger = { + onoff: val, + linger: {{{ makeGetValue('optval', 4, 'i32') }}}, + }; + return 0; } } else if (level === {{{ cDefs.IPPROTO_IP }}}) { switch (optname) { @@ -757,6 +782,14 @@ var NodeSockFSLibrary = { {{{ makeSetValue('optlen', 0, 4, 'i32') }}}; sock.error = null; // SO_ERROR reads and clears return 0; + case 3: val = sock.type; break; // SO_TYPE + case 13: { // SO_LINGER (struct linger: l_onoff, l_linger) + var linger = sock.opts.linger || { onoff: 0, linger: 0 }; + {{{ makeSetValue('optval', 0, 'linger.onoff', 'i32') }}}; + {{{ makeSetValue('optval', 4, 'linger.linger', 'i32') }}}; + {{{ makeSetValue('optlen', 0, 8, 'i32') }}}; + return 0; + } case 9: val = sock.opts.keepAlive ? 1 : 0; break; // SO_KEEPALIVE // SO_RCVBUF/SO_SNDBUF: report the live value from the udp_wrap handle // when bound, else the stored/default. @@ -783,6 +816,10 @@ var NodeSockFSLibrary = { } } else if (level === {{{ cDefs.IPPROTO_TCP }}}) { switch (optname) { + // TCP_MAXSEG: node exposes no MSS, so report RFC 879's 536-byte default + // before the handshake and the (large) loopback-negotiated value once + // connected. Enough for callers that only compare pre/post-connect MSS. + case 2: val = (sock.state === {{{ SOCK_STATE_CONNECTED }}}) ? 65483 : 536; break; case 1: val = sock.opts.noDelay ? 1 : 0; break; // TCP_NODELAY case 4: val = sock.opts.keepAliveIdle || 0; break; // TCP_KEEPIDLE case 5: val = sock.opts.keepAliveIntvl || 0; break;// TCP_KEEPINTVL diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 947626e9b936b..d9da687f4b92c 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268621, + "a.out.js": 268778, "a.out.nodebug.wasm": 587978, - "total": 856599, + "total": 856756, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/sockets/test_socket_options.c b/test/sockets/test_socket_options.c new file mode 100644 index 0000000000000..fe850bca5461c --- /dev/null +++ b/test/sockets/test_socket_options.c @@ -0,0 +1,66 @@ +/* + * 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. + * + * Socket metadata and SOL_SOCKET options under the node:net backend, on a + * freshly created (unconnected) socket: fstat() reports a socket (S_ISSOCK), + * SO_TYPE reports the socket type, and SO_LINGER round-trips a struct linger. + * Plain POSIX, also runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int tcp = socket(AF_INET, SOCK_STREAM, 0); + assert(tcp >= 0); + int udp = socket(AF_INET, SOCK_DGRAM, 0); + assert(udp >= 0); + + // fstat reports a socket for both types. + struct stat st; + assert(fstat(tcp, &st) == 0); + assert(S_ISSOCK(st.st_mode)); + assert(fstat(udp, &st) == 0); + assert(S_ISSOCK(st.st_mode)); + + // SO_TYPE reports the type the socket was created with. + int type = 0; + socklen_t len = sizeof(type); + assert(getsockopt(tcp, SOL_SOCKET, SO_TYPE, &type, &len) == 0); + assert(type == SOCK_STREAM); + type = 0; + len = sizeof(type); + assert(getsockopt(udp, SOL_SOCKET, SO_TYPE, &type, &len) == 0); + assert(type == SOCK_DGRAM); + + // SO_LINGER round-trips a struct linger. + struct linger set = {.l_onoff = 1, .l_linger = 5}; + assert(setsockopt(tcp, SOL_SOCKET, SO_LINGER, &set, sizeof(set)) == 0); + struct linger got; + memset(&got, 0, sizeof(got)); + len = sizeof(got); + assert(getsockopt(tcp, SOL_SOCKET, SO_LINGER, &got, &len) == 0); + assert(len == sizeof(got)); + assert(got.l_onoff == 1); + assert(got.l_linger == 5); + + // TCP_MAXSEG reports the RFC 879 default MSS before a connection is made. + int mss = 0; + len = sizeof(mss); + assert(getsockopt(tcp, IPPROTO_TCP, TCP_MAXSEG, &mss, &len) == 0); + assert(mss == 536); + + close(tcp); + close(udp); + printf("SOCKET OPTIONS PASS\n"); + return 0; +} diff --git a/test/test_sockets.py b/test/test_sockets.py index ed4e5f2e72034..e348762c31b95 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -519,6 +519,13 @@ def test_noderawsockets_udp_sockopts(self): self.do_runf('sockets/test_udp_sockopts.c', 'UDP SOCKOPTS PASS', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + @also_with_proxy_to_pthread + def test_noderawsockets_socket_options(self): + # Socket metadata/options on a fresh socket: fstat reports S_ISSOCK, SO_TYPE + # reports the socket type, and SO_LINGER round-trips a struct linger. + self.do_runf('sockets/test_socket_options.c', 'SOCKET OPTIONS PASS', + cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) + @requires_native_clang @requires_python_dev_packages def test_nodejs_sockets_echo_subprotocol(self):