blob: 4fe9f5c15f1bcbc8cdc490026758137bfbcc0d05 [file]
/**
* @license
* Copyright 2026 The Emscripten Authors
* SPDX-License-Identifier: MIT
*/
// TCP and UDP over node:net / node:dgram (-sNODERAWSOCKETS). This implements
// the same sock_ops contract and SOCKFS.emit readiness callbacks as the
// WebSocket backend, so existing readiness reactors work unchanged.
//
// The code is split in two: nodeSockHelpers holds the node plumbing (module
// loading, handle creation, errno mapping, event wiring) and nodeSockOps is the
// sock_ops interface the rest of emscripten calls (poll/bind/connect/...). The
// interface methods only ever delegate to helpers; helpers never call back into
// the interface.
//
// bind() is eager and synchronous: it produces a role-neutral bound handle and
// records the kernel-assigned name immediately, so getsockname() needs no
// promotion, a conflict surfaces right here as EADDRINUSE, and the handle is
// adopted as-is by listen() (server.listen) or connect() (net.Socket). The bind
// primitive is chosen once per capability: the public, synchronous
// 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.
//
// UDP uses the public node:dgram socket when it exposes a synchronous bindSync
// (a recent node addition that ships alongside connectSync), giving the
// bind(:0) + getsockname() and a real connect() that libc needs up front. There
// connect() is a real kernel connect, so the OS filters non-peer datagrams and
// surfaces async errors (e.g. ICMP ECONNREFUSED). Older node has no synchronous
// dgram bind/connect, so it falls back to a low-level udp_wrap handle and a
// connect() emulated in JS (record the peer, filter in udpDeliver). The choice
// is made per socket via useDgram().
//
// Under -pthread with PROXY_TO_PTHREAD, main() and every socket syscall run on
// the same worker, so the node handles, their event loop and the readiness
// callbacks all live on that one thread (a socket is not shared across threads,
// just as in the WebSocket backend). Payloads are copied out of (possibly
// shared) wasm memory before being handed to node, so a SharedArrayBuffer heap
// is safe.
{{{
// sock.state lifecycle, substituted as numbers at build time. A fresh socket
// has an undefined (falsy) state until its first bind/connect.
const SOCK_STATE_CONNECTING = 1;
const SOCK_STATE_CONNECTED = 2;
const SOCK_STATE_BOUND = 3;
const SOCK_STATE_LISTEN = 4;
const SOCK_STATE_CLOSED = 5;
null;
}}}
var NodeSockFSLibrary = {
// Node plumbing shared by the interface methods below.
$nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES'],
$nodeSockHelpers: {
// node builtins, resolved once each. getBuiltinModule works in both
// CommonJS and ESM output, with require as the fallback.
getNet() {
return nodeSockHelpers.netModule ??= (process.getBuiltinModule || require)('net');
},
getUtil() {
return nodeSockHelpers.utilModule ??= (process.getBuiltinModule || require)('util');
},
getDgram() {
return nodeSockHelpers.dgramModule ??= (process.getBuiltinModule || require)('dgram');
},
// True when node:dgram exposes both synchronous bindSync and connectSync
// (a recent addition), letting UDP run entirely on the public API. A runtime
// missing either falls back to the private udp_wrap handle, which provides
// both, so we never end up on a half-supported public path.
useDgram() {
var proto = nodeSockHelpers.getDgram().Socket.prototype;
return nodeSockHelpers.dgramSync ??= !!(proto.bindSync && proto.connectSync);
},
// Queue a received datagram and signal readiness. Shared by both backends.
udpDeliver(sock, address, port, data) {
// A connected datagram socket (sock.daddr set) must only see datagrams
// from its peer; drop anything from another source, matching the kernel
// filtering a real connect() would do (the udp_wrap fallback has no real
// connect, so we enforce it here).
if (sock.daddr !== undefined && (address !== sock.daddr || port !== sock.dport)) {
return;
}
sock.recv_queue.push({ addr: address, port, data });
SOCKFS.emit('message', sock.stream.fd);
},
// Map a node error (its `.code` string) to an emscripten errno. Most node
// codes are errno names already; a few are node-specific and aliased here.
nodeErrToErrno(e) {
var code = e && e.code;
if (code === 'ERR_SOCKET_DGRAM_NOT_CONNECTED') return {{{ cDefs.ENOTCONN }}};
if (code === 'ERR_SOCKET_BAD_PORT') return {{{ cDefs.EINVAL }}};
return (code && ERRNO_CODES[code]) || {{{ cDefs.EIO }}};
},
// Map a libuv result code (negative errno, as returned by the low-level
// handle's bind/getsockname) to an emscripten errno.
codeToErrno(code) {
var name = nodeSockHelpers.getUtil().getSystemErrorName(code);
return (name && ERRNO_CODES[name]) || {{{ cDefs.EINVAL }}};
},
// TCP binds eagerly and synchronously, so there is no deferred bind and no
// lazy handle promotion - the only difference between the two backends is how
// a bound handle is produced: the public net.BoundSocket when node offers it,
// else the private tcp_wrap binding. Chosen once, like useDgram().
useBoundSocket() {
return nodeSockHelpers.boundSocketOk ??= !!nodeSockHelpers.getNet().BoundSocket;
},
// Synchronously bind a TCP socket to addr:port (0 = ephemeral) and record the
// kernel-assigned name immediately. sock.bound is the resulting role-neutral
// handle - a net.BoundSocket, or a raw tcp_wrap handle - adopted as-is by
// listen() (server.listen) and connect() (net.Socket). So getsockname() needs
// no promotion, a conflict surfaces here as EADDRINUSE (exactly when POSIX
// bind() would), and close() releases it if unadopted.
bindHandle(sock, addr, port) {
var o = sock.opts || {};
if (nodeSockHelpers.useBoundSocket()) {
// The constructor binds synchronously and throws a bind conflict
// (EADDRINUSE etc.) right here; address() on the bound handle is safe.
// ipv6Only/reusePort are bind-time options, applied here from the cache.
try {
var bh = new (nodeSockHelpers.getNet().BoundSocket)({
host: addr, port, ipv6Only: o.ipv6Only, reusePort: o.reusePort,
});
}
catch (e) { throw new FS.ErrnoError(nodeSockHelpers.nodeErrToErrno(e)); }
var n = bh.address();
sock.bound = bh;
sock.saddr = n.address;
sock.sport = n.port;
return;
}
var tcp;
try {
tcp = process.binding('tcp_wrap');
} catch (e) {
throw new FS.ErrnoError({{{ cDefs.EOPNOTSUPP }}});
}
var handle = new tcp.TCP(tcp.constants.SOCKET);
// bind6 for IPv6 literals, honoring IPV6_V6ONLY via the bind flags.
var code = addr.includes(':')
? handle.bind6(addr, port, o.ipv6Only ? 1 /* UV_TCP_IPV6ONLY */ : 0)
: handle.bind(addr, port);
if (!code) {
var name = {};
code = handle.getsockname(name);
if (!code) {
sock.bound = handle;
sock.saddr = name.address;
sock.sport = name.port;
return;
}
}
try { handle.close(); } catch (e) {}
throw new FS.ErrnoError(nodeSockHelpers.codeToErrno(code));
},
// The peer address is already a numeric IP (emscripten resolves names in
// its own DNS layer), so skip node's async DNS lookup. The family follows
// the literal: a colon means IPv6.
noLookup(host, _opts, cb) {
cb(null, host, host.includes(':') ? 6 : 4);
},
// The UDP backing object. With a synchronous dgram bindSync available we use
// a public node:dgram socket (sock.udpPublic); otherwise we fall back to a
// private udp_wrap handle, which is the only older-node way to get a
// synchronous bind() + getsockname(). Either way recv wiring funnels through
// udpDeliver, so bind/send/recv/poll/close stay backend-agnostic.
ensureUdpHandle(sock) {
if (sock.udp) return sock.udp;
if (nodeSockHelpers.useDgram()) {
var socket = nodeSockHelpers.getDgram().createSocket(sock.family === {{{ cDefs.AF_INET6 }}} ? 'udp6' : 'udp4');
socket.on('message', (msg, rinfo) => {
var data = new Uint8Array(msg.length);
data.set(msg);
nodeSockHelpers.udpDeliver(sock, rinfo.address, rinfo.port, data);
});
socket.on('error', (e) => {
sock.error = nodeSockHelpers.nodeErrToErrno(e);
SOCKFS.emit('error', [sock.stream.fd, sock.error, (e && e.message) || 'udp error']);
});
sock.udpPublic = true;
return sock.udp = socket;
}
var udp = process.binding('udp_wrap');
var handle = new udp.UDP();
sock.sendWrap = udp.SendWrap;
handle.onmessage = (nread, _h, buf, rinfo) => {
if (nread < 0) {
sock.error = nodeSockHelpers.codeToErrno(nread);
SOCKFS.emit('error', [sock.stream.fd, sock.error, 'udp error']);
return;
}
var data = new Uint8Array(buf.length);
data.set(buf);
nodeSockHelpers.udpDeliver(sock, rinfo.address, rinfo.port, data);
};
return sock.udp = handle;
},
// Begin receiving exactly once. A udp_wrap handle needs an explicit
// recvStart after it is bound; a public dgram socket receives automatically
// once bound, so we only need to ensure a bind. An outgoing socket that
// never called bind() auto-binds to an ephemeral port here so getsockname
// reports the assigned local address.
startUdpRecv(sock) {
if (!sock.udp || sock.udpReceiving) return;
if (sock.udpPublic) {
if (sock.sport === undefined) {
var a = sock.udp.bindSync({ address: sock.family === {{{ cDefs.AF_INET6 }}} ? '::' : '0.0.0.0', port: 0 });
sock.saddr = a.address;
sock.sport = a.port;
}
} else {
sock.udp.recvStart();
if (sock.sport === undefined) {
var name = {};
if (sock.udp.getsockname(name) === 0) {
sock.saddr = name.address;
sock.sport = name.port;
}
}
}
sock.udpReceiving = true;
// node only honors these once the socket is bound, so (re)apply any
// options that were set earlier.
nodeSockHelpers.applyUdpOptions(sock);
},
// Apply the buffered datagram options to a bound UDP socket.
applyUdpOptions(sock) {
var h = sock.udp;
var o = sock.opts;
if (!h || !o || !sock.udpReceiving) return;
// libuv's multicast TTL/loopback setters apply to whichever family the
// socket is, so IP_MULTICAST_TTL/IPV6_MULTICAST_HOPS and the two
// *_MULTICAST_LOOP options funnel through the same handle methods.
var mcastTtl = o.multicastTtl ?? o.multicastHops;
var mcastLoop = o.multicastLoop6 ?? o.multicastLoop;
if (sock.udpPublic) {
if (o.ttl !== undefined) { try { h.setTTL(o.ttl); } catch (e) {} }
if (o.broadcast !== undefined) { try { h.setBroadcast(!!o.broadcast); } catch (e) {} }
if (o.recvBuf !== undefined) { try { h.setRecvBufferSize(o.recvBuf); } catch (e) {} }
if (o.sendBuf !== undefined) { try { h.setSendBufferSize(o.sendBuf); } catch (e) {} }
if (mcastTtl !== undefined) { try { h.setMulticastTTL(mcastTtl); } catch (e) {} }
if (mcastLoop !== undefined) { try { h.setMulticastLoopback(!!mcastLoop); } catch (e) {} }
} else {
if (o.ttl !== undefined) { try { h.setTTL(o.ttl); } catch (e) {} }
if (o.broadcast !== undefined) { try { h.setBroadcast(o.broadcast ? 1 : 0); } catch (e) {} }
if (o.recvBuf !== undefined) { try { h.bufferSize(o.recvBuf, true, {}); } catch (e) {} }
if (o.sendBuf !== undefined) { try { h.bufferSize(o.sendBuf, false, {}); } catch (e) {} }
if (mcastTtl !== undefined) { try { h.setMulticastTTL(mcastTtl); } catch (e) {} }
if (mcastLoop !== undefined) { try { h.setMulticastLoopback(mcastLoop ? 1 : 0); } catch (e) {} }
}
},
// The live OS buffer size from a bound UDP socket, or undefined.
udpBufferSize(sock, recv) {
if (!sock.udp || !sock.udpReceiving) return undefined;
try {
if (sock.udpPublic) return recv ? sock.udp.getRecvBufferSize() : sock.udp.getSendBufferSize();
return sock.udp.bufferSize(0, recv, {});
} catch (e) {}
},
// Replay buffered opts once the socket is live.
applyOptions(sock) {
var conn = sock.connection;
var o = sock.opts;
if (!conn || !o) return;
if (o.noDelay !== undefined) {
try { conn.setNoDelay(!!o.noDelay); } catch (e) {}
}
nodeSockHelpers.applyKeepAlive(sock);
},
// The keepalive tunables arrive from C in seconds, but node wants
// milliseconds, so we scale by 1000. A non-positive value keeps node's
// default for that field.
applyKeepAlive(sock) {
var conn = sock.connection;
var o = sock.opts;
if (!conn || !o || o.keepAlive === undefined) return;
try {
conn.setKeepAlive(
!!o.keepAlive,
(o.keepAliveIdle || 0) * 1000,
(o.keepAliveIntvl || 0) * 1000,
o.keepAliveCnt || 0);
} catch (e) {}
},
// Forward a connected node socket's events onto sock.
wireConnection(sock, conn) {
sock.connection = conn;
conn.on('data', (buf) => {
var data = new Uint8Array(buf.length);
data.set(buf);
sock.recv_queue.push({ addr: sock.daddr, port: sock.dport, data });
sock.recv_bytes = (sock.recv_bytes || 0) + data.length;
// If the peer outruns the reader, pause node and resume in recvmsg.
if (sock.recv_bytes >= 262144 /* 256 KiB */) {
try { conn.pause(); } catch (e) {}
sock.paused = true;
}
SOCKFS.emit('message', sock.stream.fd);
});
// A peer FIN surfaces as EOF to the reader.
conn.on('end', () => {
sock.readClosed = true;
SOCKFS.emit('message', sock.stream.fd);
});
conn.on('close', () => {
sock.readClosed = true;
sock.state = {{{ SOCK_STATE_CLOSED }}};
SOCKFS.emit('close', sock.stream.fd);
});
// Backpressure relieved, so we are writable again.
conn.on('drain', () => {
sock.writeBlocked = false;
SOCKFS.emit('open', sock.stream.fd);
});
conn.on('error', (e) => {
sock.error = nodeSockHelpers.nodeErrToErrno(e);
// Let a failed connect resolve so SO_ERROR can be read.
if (sock.state === {{{ SOCK_STATE_CONNECTING }}}) sock.state = {{{ SOCK_STATE_CONNECTED }}};
SOCKFS.emit('error', [sock.stream.fd, sock.error, (e && e.message) || 'socket error']);
});
},
},
$nodeSockOps__deps: ['$nodeSockHelpers', '$SOCKFS', '$ERRNO_CODES'],
$nodeSockOps__postset: `
if (!ENVIRONMENT_IS_NODE) {
throw new Error('NODERAWSOCKETS is currently only supported on Node.js environment.')
}`,
$nodeSockOps: {
poll(sock) {
// A listener is readable when a connection is waiting to be accepted.
if (sock.server) {
return sock.pending.length ? ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}) : 0;
}
// UDP is connectionless: always writable, readable when a datagram waits.
if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) {
var dmask = {{{ cDefs.POLLOUT }}};
if (sock.recv_queue.length || sock.error) dmask |= ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}});
return dmask;
}
var mask = 0;
if (sock.recv_queue.length || sock.readClosed || sock.error) {
mask |= ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}});
}
if (sock.error) {
// A pending socket error (e.g. a refused connect) is Linux's
// POLLERR|POLLHUP, plus writable so SO_ERROR can be read. POLLOUT|POLLERR
// also satisfies epoll's is_write_closed() mapping.
mask |= {{{ cDefs.POLLOUT }}} | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}};
} else if (sock.connection && sock.state === {{{ SOCK_STATE_CONNECTED }}} && !sock.writeBlocked) {
mask |= {{{ cDefs.POLLOUT }}};
}
// A peer FIN / read-side hangup (recv will see EOF) is POLLRDHUP. POLLHUP
// means both halves are hung up: either the connection is fully closed, or
// we locally shut down both directions (shutdown(SHUT_RDWR)), which Linux
// epoll reports as a hangup even though the node connection is still live.
if (sock.readClosed) mask |= {{{ cDefs.POLLRDHUP }}};
if (sock.state === {{{ SOCK_STATE_CLOSED }}} || (sock.readClosed && sock.writeShutdown)) {
mask |= {{{ cDefs.POLLHUP }}};
}
return mask;
},
ioctl(sock, request, arg) {
switch (request) {
case {{{ cDefs.FIONREAD }}}:
var bytes = sock.recv_queue.length ? sock.recv_queue[0].data.length : 0;
{{{ makeSetValue('arg', '0', 'bytes', 'i32') }}};
return 0;
case {{{ cDefs.FIONBIO }}}:
var on = {{{ makeGetValue('arg', '0', 'i32') }}};
if (on) sock.stream.flags |= {{{ cDefs.O_NONBLOCK }}};
else sock.stream.flags &= ~{{{ cDefs.O_NONBLOCK }}};
return 0;
default:
return {{{ cDefs.EINVAL }}};
}
},
close(sock) {
sock.state = {{{ SOCK_STATE_CLOSED }}};
if (sock.udp) {
try {
if (sock.udpPublic) sock.udp.close();
else { sock.udp.recvStop(); sock.udp.close(); }
} catch (e) {}
sock.udp = null;
}
if (sock.server) { try { sock.server.close(); } catch (e) {} sock.server = null; }
if (sock.connection) {
var conn = sock.connection;
var linger = sock.opts?.linger;
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(() => conn.destroy(), linger.linger * 1000);
timer.unref?.();
} else {
conn.destroy();
}
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) {
try { sock.bound.close(); } catch (e) {}
}
sock.bound = null;
return 0;
},
// how: SHUT_RD 0, SHUT_WR 1, SHUT_RDWR 2 (musl sys/socket.h).
shutdown(sock, how) {
if (!sock.connection) throw new FS.ErrnoError({{{ cDefs.ENOTCONN }}});
if (how === 0 || how === 2) {
// No more reads: subsequent recv returns EOF.
sock.readClosed = true;
}
if (how === 1 || how === 2) {
// Half-close the write side (sends FIN); later sends fail with EPIPE.
sock.writeShutdown = true;
try { sock.connection.end(); } catch (e) {}
}
SOCKFS.emit('message', sock.stream.fd);
return 0;
},
bind(sock, addr, port) {
if (sock.saddr !== undefined || sock.sport !== undefined) {
throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // already bound
}
if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) {
var udp = nodeSockHelpers.ensureUdpHandle(sock);
if (sock.udpPublic) {
var a;
// bindSync throws synchronously (e.g. EADDRINUSE) and returns the
// bound address, including the OS-assigned port for port 0.
try { a = udp.bindSync({ address: addr, port }); }
catch (e) { throw new FS.ErrnoError(nodeSockHelpers.nodeErrToErrno(e)); }
sock.saddr = a.address;
sock.sport = a.port;
} else {
var ucode = addr.includes(':') ? udp.bind6(addr, port, 0) : udp.bind(addr, port, 0);
if (ucode) throw new FS.ErrnoError(nodeSockHelpers.codeToErrno(ucode));
var uname = {};
ucode = udp.getsockname(uname);
if (ucode) throw new FS.ErrnoError(nodeSockHelpers.codeToErrno(ucode));
sock.saddr = uname.address;
sock.sport = uname.port;
}
sock.state = {{{ SOCK_STATE_BOUND }}};
nodeSockHelpers.startUdpRecv(sock);
return;
}
// TCP binds eagerly and synchronously: the kernel-assigned port (even for
// a bind(:0)) is known immediately, getsockname() needs no promotion, and a
// conflict surfaces right here as EADDRINUSE.
nodeSockHelpers.bindHandle(sock, addr, port);
sock.state = {{{ SOCK_STATE_BOUND }}};
},
connect(sock, addr, port) {
if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) {
sock.daddr = addr;
sock.dport = port;
var udp = nodeSockHelpers.ensureUdpHandle(sock);
if (sock.udpPublic) {
// Real kernel connect: the OS filters non-peer datagrams and reports
// async errors (e.g. ICMP ECONNREFUSED) on the socket. connectSync
// binds first if needed and throws synchronously; a re-connect just
// replaces the peer.
if (sock.udpConnected) { try { udp.disconnect(); } catch (e) {} }
try { udp.connectSync(port, addr); }
catch (e) { throw new FS.ErrnoError(nodeSockHelpers.nodeErrToErrno(e)); }
sock.udpConnected = true;
var a = udp.address();
sock.saddr = a.address;
sock.sport = a.port;
sock.udpReceiving = true; // a bound dgram socket already receives
nodeSockHelpers.applyUdpOptions(sock);
return;
}
// Older node has no synchronous dgram connect, so just record the peer
// and enforce it in JS (see udpDeliver and sendmsg); replies arrive once
// the socket is bound (an explicit bind or the auto-bind on first send).
return;
}
if (sock.server) throw new FS.ErrnoError({{{ cDefs.EOPNOTSUPP }}});
if (sock.connection) {
throw new FS.ErrnoError(sock.state === {{{ SOCK_STATE_CONNECTING }}} ? {{{ cDefs.EALREADY }}} : {{{ cDefs.EISCONN }}});
}
sock.daddr = addr;
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 });
}
conn.once('connect', () => {
sock.state = {{{ SOCK_STATE_CONNECTED }}};
sock.saddr = conn.localAddress;
sock.sport = conn.localPort;
sock.daddr = conn.remoteAddress || addr;
sock.dport = conn.remotePort || port;
try { conn.resume(); } catch (e) {}
nodeSockHelpers.applyOptions(sock);
SOCKFS.emit('open', sock.stream.fd);
});
nodeSockHelpers.wireConnection(sock, conn);
conn.connect({ host: addr, port, lookup: nodeSockHelpers.noLookup });
},
listen(sock, backlog) {
if (sock.type !== {{{ cDefs.SOCK_STREAM }}}) throw new FS.ErrnoError({{{ cDefs.EOPNOTSUPP }}}); // not a stream socket
if (sock.server) throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // already listening
if (sock.connection) throw new FS.ErrnoError({{{ cDefs.EINVAL }}}); // a connected socket cannot listen
// POSIX listen without a prior bind auto-binds an ephemeral port. The bind
// is eager and synchronous (bindHandle), so the assigned port is known and
// any conflict surfaces before we listen.
if (!sock.bound) {
nodeSockHelpers.bindHandle(sock, '0.0.0.0', 0);
sock.state = {{{ SOCK_STATE_BOUND }}};
}
var server = new (nodeSockHelpers.getNet().Server)({ pauseOnConnect: true, allowHalfOpen: true });
sock.server = server;
sock.state = {{{ SOCK_STATE_LISTEN }}};
server.on('connection', (conn) => {
var newsock = SOCKFS.createSocket(sock.family, sock.type, sock.protocol);
newsock.state = {{{ SOCK_STATE_CONNECTED }}};
newsock.saddr = conn.localAddress;
newsock.sport = conn.localPort;
newsock.daddr = conn.remoteAddress;
newsock.dport = conn.remotePort;
nodeSockHelpers.wireConnection(newsock, conn);
try { conn.resume(); } catch (e) {} // paused by pauseOnConnect
sock.pending.push(newsock);
SOCKFS.emit('connection', newsock.stream.fd);
// A queued client makes the listening socket readable (POLLIN).
sock.stream.node.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}});
});
server.on('error', (e) => {
sock.error = nodeSockHelpers.nodeErrToErrno(e);
SOCKFS.emit('error', [sock.stream.fd, sock.error, (e && e.message) || 'listen error']);
});
// listen on the already-bound handle: accept would-blocks until a
// connection arrives, surfaced through poll/accept.
server.listen(sock.bound, backlog || 511);
},
accept(listensock) {
if (!listensock.server) throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
// Surface a real listen error (e.g. late address-in-use) rather than
// masking it as would-block.
if (listensock.error) {
var e = listensock.error;
listensock.error = null;
throw new FS.ErrnoError(e);
}
if (!listensock.pending.length) throw new FS.ErrnoError({{{ cDefs.EAGAIN }}});
var newsock = listensock.pending.shift();
newsock.stream.flags = listensock.stream.flags;
return newsock;
},
sendmsg(sock, buffer, offset, length, addr, port) {
if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) {
// A connected datagram socket rejects an explicit destination.
if (sock.daddr !== undefined && addr !== undefined) {
throw new FS.ErrnoError({{{ cDefs.EISCONN }}});
}
if (addr === undefined || port === undefined) {
addr = sock.daddr;
port = sock.dport;
if (addr === undefined || port === undefined) throw new FS.ErrnoError({{{ cDefs.EDESTADDRREQ }}});
}
var handle = nodeSockHelpers.ensureUdpHandle(sock);
// A public dgram send() would do an async implicit bind, so bind (and
// start receiving) synchronously up front; udp_wrap auto-binds on send,
// so it starts receiving afterwards.
if (sock.udpPublic) nodeSockHelpers.startUdpRecv(sock);
offset += buffer.byteOffset;
buffer = buffer.buffer;
// Copy out of (possibly shared) wasm memory: the datagram must stay
// stable until the asynchronous send completes.
var msg = Buffer.from(buffer.slice(offset, offset + length));
if (sock.udpPublic) {
// Async errors surface on the 'error' event (read via SO_ERROR). A
// real-connected socket sends to its kernel peer with no address.
if (sock.udpConnected) handle.send(msg);
else handle.send(msg, port, addr);
} else {
var code = addr.includes(':')
? handle.send6(new sock.sendWrap(), [msg], 1, port, addr, false)
: handle.send(new sock.sendWrap(), [msg], 1, port, addr, false);
if (code < 0) throw new FS.ErrnoError(nodeSockHelpers.codeToErrno(code));
// The send auto-bound an unbound socket, so replies can be received.
nodeSockHelpers.startUdpRecv(sock);
}
return length;
}
// Writing after a write-shutdown is a broken pipe, regardless of peer.
if (sock.writeShutdown) {
throw new FS.ErrnoError({{{ cDefs.EPIPE }}});
}
var conn = sock.connection;
if (!conn || sock.state === {{{ SOCK_STATE_CLOSED }}}) {
throw new FS.ErrnoError({{{ cDefs.ENOTCONN }}});
}
// Bound node's write buffer to its high-water mark: a non-blocking socket
// only accepts up to the remaining headroom, would-blocking when there is
// none, and short-writes the rest (which POSIX send() is allowed to do).
if (sock.stream.flags & {{{ cDefs.O_NONBLOCK }}}) {
var headroom = conn.writableHighWaterMark - conn.writableLength;
if (headroom <= 0) throw new FS.ErrnoError({{{ cDefs.EAGAIN }}});
if (length > headroom) length = headroom;
}
offset += buffer.byteOffset;
buffer = buffer.buffer;
var data = new Uint8Array(buffer.slice(offset, offset + length));
var ok;
try {
ok = conn.write(data);
} catch (e) {
throw new FS.ErrnoError(nodeSockHelpers.nodeErrToErrno(e));
}
if (!ok) sock.writeBlocked = true; // cleared on 'drain', gates poll's POLLOUT
return length;
},
recvmsg(sock, length, flags) {
// MSG_PEEK returns the data from the head of the queue without consuming
// it: no shift, no recv_bytes/flow-control adjustment, so a later recv
// sees the same bytes and poll still reports the socket readable.
var peek = flags & {{{ cDefs.MSG_PEEK }}};
if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) {
var dgram = sock.recv_queue[0];
if (!dgram) {
// poll reports the socket readable on a pending error, so surface
// (and clear) it here rather than spinning on EAGAIN.
if (sock.error) {
var derr = sock.error;
sock.error = null;
throw new FS.ErrnoError(derr);
}
throw new FS.ErrnoError({{{ cDefs.EAGAIN }}});
}
// A datagram is atomic: return up to length bytes and drop the rest.
var dd = dgram.data;
var res = { buffer: dd.subarray(0, Math.min(length, dd.length)), addr: dgram.addr, port: dgram.port };
if (!peek) sock.recv_queue.shift();
return res;
}
var queued = sock.recv_queue[0];
if (!queued) {
if (sock.readClosed) return null; // EOF
if (!sock.connection) {
throw new FS.ErrnoError({{{ cDefs.ENOTCONN }}});
}
throw new FS.ErrnoError({{{ cDefs.EAGAIN }}});
}
var q = queued.data;
var bytesRead = Math.min(length, q.length);
var res = { buffer: q.subarray(0, bytesRead), addr: queued.addr, port: queued.port };
if (peek) return res;
sock.recv_queue.shift();
if (bytesRead < q.length) {
queued.data = q.subarray(bytesRead);
sock.recv_queue.unshift(queued);
}
sock.recv_bytes = Math.max(0, (sock.recv_bytes || 0) - bytesRead);
if (sock.paused && sock.recv_bytes < 262144 && sock.connection) {
sock.paused = false;
try { sock.connection.resume(); } catch (e) {}
}
return res;
},
setsockopt(sock, level, optname, optval, optlen) {
sock.opts ||= {};
var val = {{{ makeGetValue('optval', 0, 'i32') }}};
if (level === {{{ cDefs.SOL_SOCKET }}}) {
switch (optname) {
case 9: // SO_KEEPALIVE
sock.opts.keepAlive = !!val;
nodeSockHelpers.applyKeepAlive(sock);
return 0;
case 8: // SO_RCVBUF. Applied to the udp_wrap handle; Node TCP cannot.
sock.opts.recvBuf = val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 7: // SO_SNDBUF. Applied to the udp_wrap handle; Node TCP cannot.
sock.opts.sendBuf = val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 6: // SO_BROADCAST (datagram sockets)
sock.opts.broadcast = !!val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 2: // SO_REUSEADDR. libuv forces SO_REUSEADDR on at bind, so this
// is effectively always enabled; accept and ignore (getsockopt
// reports 1). It cannot be turned off.
return 0;
case {{{ cDefs.SO_REUSEPORT }}}: // SO_REUSEPORT. Bind-time: cached and
// 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) {
case 2: // IP_TTL
sock.opts.ttl = val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 33: // IP_MULTICAST_TTL
sock.opts.multicastTtl = val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 34: // IP_MULTICAST_LOOP
sock.opts.multicastLoop = !!val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
}
} else if (level === {{{ cDefs.IPPROTO_IPV6 }}}) {
switch (optname) {
case {{{ cDefs.IPV6_V6ONLY }}}:
// Bind-time only: IPV6_V6ONLY cannot change once the socket is bound,
// so reject a late change (POSIX returns EINVAL). Before any
// bind/connect/listen we cache it for the BoundSocket constructor.
if (sock.state) return -{{{ cDefs.EINVAL }}};
sock.opts.ipv6Only = !!val;
return 0;
case 18: // IPV6_MULTICAST_HOPS
sock.opts.multicastHops = val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
case 19: // IPV6_MULTICAST_LOOP
sock.opts.multicastLoop6 = !!val;
nodeSockHelpers.applyUdpOptions(sock);
return 0;
}
} else if (level === {{{ cDefs.IPPROTO_TCP }}}) {
switch (optname) {
case 1: // TCP_NODELAY
sock.opts.noDelay = !!val;
if (sock.connection) { try { sock.connection.setNoDelay(!!val); } catch (e) {} }
return 0;
case 4: // TCP_KEEPIDLE (seconds)
sock.opts.keepAliveIdle = val;
nodeSockHelpers.applyKeepAlive(sock);
return 0;
case 5: // TCP_KEEPINTVL (seconds)
sock.opts.keepAliveIntvl = val;
nodeSockHelpers.applyKeepAlive(sock);
return 0;
case 6: // TCP_KEEPCNT (probe count)
sock.opts.keepAliveCnt = val;
nodeSockHelpers.applyKeepAlive(sock);
return 0;
}
}
// Accept unknown options silently, like a permissive stack.
return 0;
},
getsockopt(sock, level, optname, optval, optlen) {
sock.opts ||= {};
var val;
if (level === {{{ cDefs.SOL_SOCKET }}}) {
switch (optname) {
case {{{ cDefs.SO_ERROR }}}:
{{{ makeSetValue('optval', 0, 'sock.error || 0', 'i32') }}};
{{{ 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.
case 8: val = nodeSockHelpers.udpBufferSize(sock, true) ?? (sock.opts.recvBuf || 65536); break;
case 7: val = nodeSockHelpers.udpBufferSize(sock, false) ?? (sock.opts.sendBuf || 65536); break;
case 6: val = sock.opts.broadcast ? 1 : 0; break; // SO_BROADCAST
case 2: val = 1; break; // SO_REUSEADDR: libuv forces it on at bind
case {{{ cDefs.SO_REUSEPORT }}}: val = sock.opts.reusePort ? 1 : 0; break;
default: return -{{{ cDefs.ENOPROTOOPT }}};
}
} else if (level === {{{ cDefs.IPPROTO_IP }}}) {
switch (optname) {
case 2: val = sock.opts.ttl || 64; break; // IP_TTL
case 33: val = sock.opts.multicastTtl ?? 1; break; // IP_MULTICAST_TTL
case 34: val = sock.opts.multicastLoop === undefined ? 1 : (sock.opts.multicastLoop ? 1 : 0); break; // IP_MULTICAST_LOOP
default: return -{{{ cDefs.ENOPROTOOPT }}};
}
} else if (level === {{{ cDefs.IPPROTO_IPV6 }}}) {
switch (optname) {
case {{{ cDefs.IPV6_V6ONLY }}}: val = sock.opts.ipv6Only ? 1 : 0; break;
case 18: val = sock.opts.multicastHops ?? 1; break; // IPV6_MULTICAST_HOPS
case 19: val = sock.opts.multicastLoop6 === undefined ? 1 : (sock.opts.multicastLoop6 ? 1 : 0); break; // IPV6_MULTICAST_LOOP
default: return -{{{ cDefs.ENOPROTOOPT }}};
}
} 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
case 6: val = sock.opts.keepAliveCnt || 0; break; // TCP_KEEPCNT
default: return -{{{ cDefs.ENOPROTOOPT }}};
}
} else {
return -{{{ cDefs.ENOPROTOOPT }}};
}
{{{ makeSetValue('optval', 0, 'val', 'i32') }}};
{{{ makeSetValue('optlen', 0, 4, 'i32') }}};
return 0;
}
},
};
addToLibrary(NodeSockFSLibrary);