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
18 changes: 18 additions & 0 deletions src/lib/libsockfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 38 additions & 1 deletion src/lib/libsockfs_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
66 changes: 66 additions & 0 deletions test/sockets/test_socket_options.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>

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;
}
7 changes: 7 additions & 0 deletions test/test_sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down