From c46260f328c5adf7e149f30728d5ec73f821194e Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 9 Jun 2026 16:22:13 -0700 Subject: [PATCH 1/7] Add -sNODERAWSOCKETS backend for real TCP and UDP sockets via node:net Adds a new NODERAWSOCKETS setting that backs the POSIX sockets API directly with Node.js's node:net and node:dgram, giving real, non-blocking TCP and UDP sockets without WebSockets, an external proxy process, or pthreads. This is the sockets counterpart to NODERAWFS: where NODERAWFS gives direct access to the host filesystem, this gives direct access to host sockets. Unlike PROXY_POSIX_SOCKETS this is single-threaded and event-driven: socket readiness is delivered through the same emscripten_set_socket_*_callback hooks the default WebSocket backend uses, so it drops into existing readiness reactors unchanged. Under -pthread the socket syscalls are proxied to the main thread, so the backend always runs on node's event loop and a SharedArrayBuffer heap is safe. Supported: * TCP clients: connect, send, recv, shutdown and close, with non-blocking semantics and backpressure (send reports EAGAIN rather than buffering unboundedly). * TCP servers: bind, listen, accept, getsockname/getpeername. * UDP: bind, connect, sendto/recvfrom, with connected-peer filtering. * IPv4 and IPv6 (AF_INET6): TCP and UDP over v6, including IPV6_V6ONLY. * get/setsockopt: SO_ERROR, SO_KEEPALIVE and TCP_KEEPIDLE, TCP_NODELAY, SO_RCVBUF/SO_SNDBUF, SO_BROADCAST, IP_TTL, SO_REUSEPORT and IPV6_V6ONLY. Options are mirrored to a cache (the getsockopt source of truth) and projected onto the live socket; we only report options we can actually honor (e.g. SO_REUSEADDR reads back as 1 since libuv forces it on, and IPV6_V6ONLY returns EINVAL if changed after bind). Binding is eager and synchronous, so a conflict surfaces as EADDRINUSE at bind() and getsockname() reports the kernel-assigned ephemeral port immediately - there is no deferred-bind or lazy-handle promotion. A bound socket is a role-neutral handle, adopted as-is by listen() (server.listen) or connect() (net.Socket), and released by close() only if it was never adopted. Bind-time options (ipv6Only, reusePort) are passed to the handle at construction. The bind primitive is selected once per capability: * the public, synchronous net.BoundHandle (and dgram bindSync/connectSync) when the Node.js runtime provides them; and * the private tcp_wrap/udp_wrap bindings as a fallback on Node.js versions that do not (bind6/send6 for IPv6). Details: * new node backend in src/lib/libsockfs_node.js, pulled in only under -sNODERAWSOCKETS, implementing the sock_ops contract * __syscall_setsockopt and __syscall_shutdown now live in JS, routing to the backend under NODERAWSOCKETS (else reporting the option/feature as unsupported), avoiding a libstubs variation * tests under test/sockets exercise TCP echo, server accept/echo (including listen-without-bind autobind), client source-port bind plus synchronous EADDRINUSE, client semantics (EISCONN, half-close, EPIPE), backpressure, connection refused, UDP echo/connect, and IPv6 TCP/UDP over ::1 (including IPV6_V6ONLY before/after bind); all build and run natively against the host stack and run under node, including PROXY_TO_PTHREAD variants --- .../tools_reference/settings_reference.rst | 31 + src/lib/libsigs.js | 1 + src/lib/libsockfs.js | 18 +- src/lib/libsockfs_node.js | 723 ++++++++++++++++++ src/lib/libsyscall.js | 24 +- src/modules.mjs | 4 + src/settings.js | 26 + src/struct_info.json | 26 +- src/struct_info_generated.json | 4 + src/struct_info_generated_wasm64.json | 4 + system/lib/libc/emscripten_syscall_stubs.c | 6 - system/lib/wasmfs/syscalls.cpp | 18 + .../test_codesize_hello_dylink_all.json | 16 +- test/sockets/test_tcp_backpressure.c | 120 +++ test/sockets/test_tcp_client_bind.c | 147 ++++ test/sockets/test_tcp_client_semantics.c | 132 ++++ test/sockets/test_tcp_echo.c | 140 ++++ test/sockets/test_tcp_ipv6.c | 188 +++++ test/sockets/test_tcp_refused.c | 95 +++ test/sockets/test_tcp_server.c | 190 +++++ test/sockets/test_udp_connect.c | 137 ++++ test/sockets/test_udp_echo.c | 149 ++++ test/sockets/test_udp_ipv6.c | 132 ++++ test/test_sockets.py | 132 ++++ tools/link.py | 2 + tools/settings.py | 3 + 26 files changed, 2439 insertions(+), 29 deletions(-) create mode 100644 src/lib/libsockfs_node.js create mode 100644 test/sockets/test_tcp_backpressure.c create mode 100644 test/sockets/test_tcp_client_bind.c create mode 100644 test/sockets/test_tcp_client_semantics.c create mode 100644 test/sockets/test_tcp_echo.c create mode 100644 test/sockets/test_tcp_ipv6.c create mode 100644 test/sockets/test_tcp_refused.c create mode 100644 test/sockets/test_tcp_server.c create mode 100644 test/sockets/test_udp_connect.c create mode 100644 test/sockets/test_udp_echo.c create mode 100644 test/sockets/test_udp_ipv6.c diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index ce2494edbd483..74a8aa3672644 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -588,6 +588,37 @@ sockets calls from browser to native world. Default value: false +.. _noderawsockets: + +NODERAWSOCKETS +============== + +If enabled, the POSIX sockets API is backed by Node.js's ``node:net`` +module, giving real non-blocking outgoing TCP sockets with no WebSockets, +proxy process or pthreads. This is the sockets counterpart to NODERAWFS: +where NODERAWFS gives direct access to the host filesystem, this gives +direct access to host sockets. It only works under node and is ignored +elsewhere. + +It supports full TCP (outgoing connect plus bind, listen and accept for +servers) and UDP. TCP clients use the public node:net API. bind needs a +synchronous bind() + getsockname(), so it uses the public node APIs that +provide them when present - net.BoundHandle for TCP and dgram +bindSync/connectSync for UDP - and falls back to the private tcp_wrap/udp_wrap +handles on older Node.js versions that lack them. + +It is event-driven. Socket readiness comes through the same +``emscripten_set_socket_*_callback`` hooks the WebSocket backend uses, so it +works with existing readiness reactors. It cannot be combined with the +WebSocket emulation, PROXY_POSIX_SOCKETS or SOCKET_WEBRTC. + +It works under -pthread with PROXY_TO_PTHREAD, where main() and every socket +syscall run on a single worker alongside the node handles and their event +loop. As with the WebSocket backend, sharing a socket across threads under a +plain -pthread build (without PROXY_TO_PTHREAD) is not supported. + +Default value: false + .. _websocket_subprotocol: WEBSOCKET_SUBPROTOCOL diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 3159fcf42c9f1..746ee98ff5cef 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -271,6 +271,7 @@ sigs = { __syscall_rmdir__sig: 'ip', __syscall_sendmsg__sig: 'iipiiii', __syscall_sendto__sig: 'iippipi', + __syscall_setsockopt__sig: 'iiiipii', __syscall_shutdown__sig: 'iiiiiii', __syscall_socket__sig: 'iiiiiii', __syscall_stat64__sig: 'ipp', diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index a9e99be7729ee..66bcdcb162a42 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -8,7 +8,11 @@ addToLibrary({ $SOCKFS__postset: () => { addAtInit('SOCKFS.root = FS.mount(SOCKFS, {}, null);'); }, - $SOCKFS__deps: ['$FS'], + $SOCKFS__deps: ['$FS', +#if NODERAWSOCKETS + '$nodeSockOps', +#endif + ], $SOCKFS: { #if expectToReceiveOnModule('websocket') websocketArgs: {}, @@ -44,8 +48,12 @@ addToLibrary({ return FS.createNode(null, '/', {{{ cDefs.S_IFDIR | 0o777 }}}, 0); }, createSocket(family, type, protocol) { - // Emscripten only supports AF_INET - if (family != {{{ cDefs.AF_INET }}}) { + if (family != {{{ cDefs.AF_INET }}} +#if NODERAWSOCKETS + // The node:net backend supports IPv6; other backends are IPv4 only. + && family != {{{ cDefs.AF_INET6 }}} +#endif + ) { throw new FS.ErrnoError({{{ cDefs.EAFNOSUPPORT }}}); } type &= ~{{{ cDefs.SOCK_CLOEXEC | cDefs.SOCK_NONBLOCK }}}; // Some applications may pass it; it makes no sense for a single process. @@ -69,6 +77,8 @@ addToLibrary({ pending: [], recv_queue: [], #if SOCKET_WEBRTC +#elif NODERAWSOCKETS + sock_ops: nodeSockOps #else sock_ops: SOCKFS.websocket_sock_ops #endif @@ -726,7 +736,7 @@ addToLibrary({ return res; } - } + }, }, /* diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js new file mode 100644 index 0000000000000..584fd96654910 --- /dev/null +++ b/src/lib/libsockfs_node.js @@ -0,0 +1,723 @@ +/** + * @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. +// +// 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.BoundHandle 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. + +var NodeSockFSLibrary = { + $nodeSockOps__deps: ['$SOCKFS', '$ERRNO_CODES'], + $nodeSockOps: { + // node builtins, resolved once each. getBuiltinModule works in both + // CommonJS and ESM output, with require as the fallback. + getNet() { + return nodeSockOps.netModule ??= (process.getBuiltinModule || require)('net'); + }, + getUtil() { + return nodeSockOps.utilModule ??= (process.getBuiltinModule || require)('util'); + }, + getDgram() { + return nodeSockOps.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 = nodeSockOps.getDgram().Socket.prototype; + return nodeSockOps.dgramSync ??= + typeof proto.bindSync == 'function' && typeof proto.connectSync == 'function'; + }, + // Queue a received datagram and signal readiness. A connected datagram + // socket only accepts datagrams from its peer. Shared by both backends. + udpDeliver(sock, address, port, data) { + 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. + errnoForNode(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. + errnoForCode(code) { + var name = nodeSockOps.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.BoundHandle when node offers it, + // else the private tcp_wrap binding. Chosen once, like useDgram(). + useBoundHandle() { + return nodeSockOps.boundHandleOk ??= + typeof nodeSockOps.getNet().BoundHandle == 'function'; + }, + // 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.BoundHandle, 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 (nodeSockOps.useBoundHandle()) { + var bh; + // 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 { + bh = new (nodeSockOps.getNet().BoundHandle)({ + host: addr, port, ipv6Only: o.ipv6Only, reusePort: o.reusePort, + }); + } + catch (e) { throw new FS.ErrnoError(nodeSockOps.errnoForNode(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(nodeSockOps.errnoForCode(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 (nodeSockOps.useDgram()) { + var socket = nodeSockOps.getDgram().createSocket(sock.family === {{{ cDefs.AF_INET6 }}} ? 'udp6' : 'udp4'); + socket.on('message', (msg, rinfo) => { + var data = new Uint8Array(msg.length); + data.set(msg); + nodeSockOps.udpDeliver(sock, rinfo.address, rinfo.port, data); + }); + socket.on('error', (e) => { + sock.error = nodeSockOps.errnoForNode(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 = nodeSockOps.errnoForCode(nread); + SOCKFS.emit('error', [sock.stream.fd, sock.error, 'udp error']); + return; + } + var data = new Uint8Array(buf.length); + data.set(buf); + nodeSockOps.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. + nodeSockOps.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; + 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) {} } + } 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) {} } + } + }, + // 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) { return undefined; } + }, + // 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) {} + } + nodeSockOps.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 = '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 = nodeSockOps.errnoForNode(e); + // Let a failed connect resolve so SO_ERROR can be read. + if (sock.state === 'connecting') sock.state = 'connected'; + SOCKFS.emit('error', [sock.stream.fd, sock.error, (e && e.message) || 'socket error']); + }); + }, + 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) { + // Mark writable on error so SO_ERROR can be read. + mask |= {{{ cDefs.POLLOUT }}}; + } else if (sock.connection && sock.state === 'connected' && !sock.writeBlocked) { + mask |= {{{ cDefs.POLLOUT }}}; + } + if (sock.readClosed) 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 = '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) { try { sock.connection.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) { + 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 = nodeSockOps.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(nodeSockOps.errnoForNode(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(nodeSockOps.errnoForCode(ucode)); + var uname = {}; + ucode = udp.getsockname(uname); + if (ucode) throw new FS.ErrnoError(nodeSockOps.errnoForCode(ucode)); + sock.saddr = uname.address; + sock.sport = uname.port; + } + sock.state = 'bound'; + nodeSockOps.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. + nodeSockOps.bindHandle(sock, addr, port); + sock.state = 'bound'; + }, + connect(sock, addr, port) { + if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) { + sock.daddr = addr; + sock.dport = port; + var udp = nodeSockOps.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(nodeSockOps.errnoForNode(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 + nodeSockOps.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 === 'connecting' ? {{{ cDefs.EALREADY }}} : {{{ cDefs.EISCONN }}}); + } + sock.daddr = addr; + sock.dport = port; + sock.state = 'connecting'; + var net = nodeSockOps.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 = 'connected'; + sock.saddr = conn.localAddress; + sock.sport = conn.localPort; + sock.daddr = conn.remoteAddress || addr; + sock.dport = conn.remotePort || port; + try { conn.resume(); } catch (e) {} + nodeSockOps.applyOptions(sock); + SOCKFS.emit('open', sock.stream.fd); + }); + nodeSockOps.wireConnection(sock, conn); + conn.connect({ host: addr, port, lookup: nodeSockOps.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) { + nodeSockOps.bindHandle(sock, '0.0.0.0', 0); + sock.state = 'bound'; + } + var server = new (nodeSockOps.getNet().Server)({ pauseOnConnect: true, allowHalfOpen: true }); + sock.server = server; + sock.state = 'listen'; + server.on('connection', (conn) => { + var newsock = SOCKFS.createSocket(sock.family, sock.type, sock.protocol); + newsock.state = 'connected'; + newsock.saddr = conn.localAddress; + newsock.sport = conn.localPort; + newsock.daddr = conn.remoteAddress; + newsock.dport = conn.remotePort; + nodeSockOps.wireConnection(newsock, conn); + try { conn.resume(); } catch (e) {} // paused by pauseOnConnect + sock.pending.push(newsock); + SOCKFS.emit('connection', newsock.stream.fd); + }); + server.on('error', (e) => { + sock.error = nodeSockOps.errnoForNode(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 = nodeSockOps.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) nodeSockOps.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(nodeSockOps.errnoForCode(code)); + // The send auto-bound an unbound socket, so replies can be received. + nodeSockOps.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 === '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(nodeSockOps.errnoForNode(e)); + } + if (!ok) sock.writeBlocked = true; // cleared on 'drain', gates poll's POLLOUT + return length; + }, + recvmsg(sock, length) { + if (sock.type === {{{ cDefs.SOCK_DGRAM }}}) { + var dgram = sock.recv_queue.shift(); + 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; + return { buffer: dd.subarray(0, Math.min(length, dd.length)), addr: dgram.addr, port: dgram.port }; + } + var queued = sock.recv_queue.shift(); + 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 (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; + nodeSockOps.applyKeepAlive(sock); + return 0; + case 8: // SO_RCVBUF. Applied to the udp_wrap handle; Node TCP cannot. + sock.opts.recvBuf = val; + nodeSockOps.applyUdpOptions(sock); + return 0; + case 7: // SO_SNDBUF. Applied to the udp_wrap handle; Node TCP cannot. + sock.opts.sendBuf = val; + nodeSockOps.applyUdpOptions(sock); + return 0; + case 6: // SO_BROADCAST (datagram sockets) + sock.opts.broadcast = !!val; + nodeSockOps.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 BoundHandle at bind. Set after bind has no effect. + sock.opts.reusePort = !!val; + return 0; + } + } else if (level === {{{ cDefs.IPPROTO_IP }}}) { + if (optname === 2 /* IP_TTL */) { + sock.opts.ttl = val; + nodeSockOps.applyUdpOptions(sock); + return 0; + } + } else if (level === {{{ cDefs.IPPROTO_IPV6 }}}) { + if (optname === {{{ 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 BoundHandle constructor. + if (sock.state) return -{{{ cDefs.EINVAL }}}; + sock.opts.ipv6Only = !!val; + 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; + nodeSockOps.applyKeepAlive(sock); + return 0; + case 5: // TCP_KEEPINTVL (seconds) + sock.opts.keepAliveIntvl = val; + nodeSockOps.applyKeepAlive(sock); + return 0; + case 6: // TCP_KEEPCNT (probe count) + sock.opts.keepAliveCnt = val; + nodeSockOps.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 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 = nodeSockOps.udpBufferSize(sock, true) ?? (sock.opts.recvBuf || 65536); break; + case 7: val = nodeSockOps.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 }}}) { + if (optname !== 2 /* IP_TTL */) return -{{{ cDefs.ENOPROTOOPT }}}; + val = sock.opts.ttl || 64; + } else if (level === {{{ cDefs.IPPROTO_IPV6 }}}) { + if (optname !== {{{ cDefs.IPV6_V6ONLY }}}) return -{{{ cDefs.ENOPROTOOPT }}}; + val = sock.opts.ipv6Only ? 1 : 0; + } else if (level === {{{ cDefs.IPPROTO_TCP }}}) { + switch (optname) { + 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); diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index afcec470a58f2..5ce677a1d27e7 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -389,8 +389,12 @@ var SyscallsLibrary = { }, __syscall_shutdown__deps: ['$getSocketFromFD'], __syscall_shutdown: (fd, how, u1, u2, u3, u4) => { - getSocketFromFD(fd); + var sock = getSocketFromFD(fd); +#if NODERAWSOCKETS + return sock.sock_ops.shutdown(sock, how); +#else return -{{{ cDefs.ENOSYS }}}; // unsupported feature +#endif }, __syscall_accept4__deps: ['$getSocketFromFD', '$writeSockaddr', '$DNS'], __syscall_accept4: (fd, addr, len, flags, u1, u2) => { @@ -445,6 +449,10 @@ var SyscallsLibrary = { __syscall_getsockopt__deps: ['$getSocketFromFD'], __syscall_getsockopt: (fd, level, optname, optval, optlen, unused) => { var sock = getSocketFromFD(fd); +#if NODERAWSOCKETS + // The node:net backend handles all socket options. + return sock.sock_ops.getsockopt(sock, level, optname, optval, optlen); +#else // Minimal getsockopt aimed at resolving https://github.com/emscripten-core/emscripten/issues/2211 // so only supports SOL_SOCKET with SO_ERROR. if (level === {{{ cDefs.SOL_SOCKET }}}) { @@ -456,6 +464,20 @@ var SyscallsLibrary = { } } return -{{{ cDefs.ENOPROTOOPT }}}; // The option is unknown at the level indicated. +#endif + }, + // Defined in JS rather than as a weak native stub so the node:net backend can + // provide it without a separate libstubs variation. Without that backend it + // just reports the option as unknown. + __syscall_setsockopt__deps: ['$getSocketFromFD'], + __syscall_setsockopt: (fd, level, optname, optval, optlen, unused) => { +#if NODERAWSOCKETS + var sock = getSocketFromFD(fd); + return sock.sock_ops.setsockopt(sock, level, optname, optval, optlen); +#else + getSocketFromFD(fd); // validate the fd (and keep this syscall's catch reachable) + return -{{{ cDefs.ENOPROTOOPT }}}; // The option is unknown at the level indicated. +#endif }, __syscall_sendmsg__deps: ['$getSocketFromFD', '$getSocketAddress'], __syscall_sendmsg: (fd, message, flags, u1, u2, u3) => { diff --git a/src/modules.mjs b/src/modules.mjs index 83fcf1d86d8b3..bc6aa5295f5fa 100644 --- a/src/modules.mjs +++ b/src/modules.mjs @@ -115,6 +115,10 @@ function calculateLibraries() { 'libsockfs.js', // ok to include it by default since it's only used if the syscall is used ); + if (NODERAWSOCKETS) { + libraries.push('libsockfs_node.js'); + } + if (NODERAWFS) { // NODERAWFS requires NODEFS libraries.push('libnodefs.js'); diff --git a/src/settings.js b/src/settings.js index 8d8de38121c19..e4a091d9cb4ca 100644 --- a/src/settings.js +++ b/src/settings.js @@ -419,6 +419,32 @@ var WEBSOCKET_URL = 'ws://'; // [link] var PROXY_POSIX_SOCKETS = false; +// If enabled, the POSIX sockets API is backed by Node.js's ``node:net`` +// module, giving real non-blocking outgoing TCP sockets with no WebSockets, +// proxy process or pthreads. This is the sockets counterpart to NODERAWFS: +// where NODERAWFS gives direct access to the host filesystem, this gives +// direct access to host sockets. It only works under node and is ignored +// elsewhere. +// +// It supports full TCP (outgoing connect plus bind, listen and accept for +// servers) and UDP. TCP clients use the public node:net API. bind needs a +// synchronous bind() + getsockname(), so it uses the public node APIs that +// provide them when present - net.BoundHandle for TCP and dgram +// bindSync/connectSync for UDP - and falls back to the private tcp_wrap/udp_wrap +// handles on older Node.js versions that lack them. +// +// It is event-driven. Socket readiness comes through the same +// ``emscripten_set_socket_*_callback`` hooks the WebSocket backend uses, so it +// works with existing readiness reactors. It cannot be combined with the +// WebSocket emulation, PROXY_POSIX_SOCKETS or SOCKET_WEBRTC. +// +// It works under -pthread with PROXY_TO_PTHREAD, where main() and every socket +// syscall run on a single worker alongside the node handles and their event +// loop. As with the WebSocket backend, sharing a socket across threads under a +// plain -pthread build (without PROXY_TO_PTHREAD) is not supported. +// [link] +var NODERAWSOCKETS = false; + // A string containing a comma separated list of WebSocket subprotocols // as would be present in the Sec-WebSocket-Protocol header. // You can set 'null', if you don't want to specify it. diff --git a/src/struct_info.json b/src/struct_info.json index f4ecbc84811a3..0362f1a08eecc 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -236,12 +236,15 @@ } }, { - "file": "netinet/in.h", - "defines": [ - "IPPROTO_UDP", - "IPPROTO_TCP", - "INADDR_LOOPBACK" - ] + "file": "netinet/in.h", + "defines": [ + "IPPROTO_IP", + "IPPROTO_IPV6", + "IPPROTO_UDP", + "IPPROTO_TCP", + "IPV6_V6ONLY", + "INADDR_LOOPBACK" + ] }, { "file": "bits/fcntl.h", @@ -289,11 +292,12 @@ "SOCK_STREAM", "SOCK_CLOEXEC", "SOCK_NONBLOCK", - "AF_INET", - "AF_UNSPEC", - "AF_INET6", - "SOL_SOCKET", - "SO_ERROR" + "AF_INET", + "AF_UNSPEC", + "AF_INET6", + "SOL_SOCKET", + "SO_ERROR", + "SO_REUSEPORT" ] }, { diff --git a/src/struct_info_generated.json b/src/struct_info_generated.json index c946883c44794..c0dded01e8e71 100644 --- a/src/struct_info_generated.json +++ b/src/struct_info_generated.json @@ -307,8 +307,11 @@ "IEXTEN": 32768, "IMAXBEL": 8192, "INADDR_LOOPBACK": 2130706433, + "IPPROTO_IP": 0, + "IPPROTO_IPV6": 41, "IPPROTO_TCP": 6, "IPPROTO_UDP": 17, + "IPV6_V6ONLY": 26, "ISIG": 1, "IUTF8": 16384, "IXON": 1024, @@ -414,6 +417,7 @@ "SOCK_STREAM": 1, "SOL_SOCKET": 1, "SO_ERROR": 4, + "SO_REUSEPORT": 15, "SYMLOOP_MAX": 40, "S_IALLUGO": 4095, "S_IFBLK": 24576, diff --git a/src/struct_info_generated_wasm64.json b/src/struct_info_generated_wasm64.json index 399843f16e941..036fe0ec64f20 100644 --- a/src/struct_info_generated_wasm64.json +++ b/src/struct_info_generated_wasm64.json @@ -307,8 +307,11 @@ "IEXTEN": 32768, "IMAXBEL": 8192, "INADDR_LOOPBACK": 2130706433, + "IPPROTO_IP": 0, + "IPPROTO_IPV6": 41, "IPPROTO_TCP": 6, "IPPROTO_UDP": 17, + "IPV6_V6ONLY": 26, "ISIG": 1, "IUTF8": 16384, "IXON": 1024, @@ -414,6 +417,7 @@ "SOCK_STREAM": 1, "SOL_SOCKET": 1, "SO_ERROR": 4, + "SO_REUSEPORT": 15, "SYMLOOP_MAX": 40, "S_IALLUGO": 4095, "S_IFBLK": 24576, diff --git a/system/lib/libc/emscripten_syscall_stubs.c b/system/lib/libc/emscripten_syscall_stubs.c index cf942294ad927..50625f0fd30d9 100644 --- a/system/lib/libc/emscripten_syscall_stubs.c +++ b/system/lib/libc/emscripten_syscall_stubs.c @@ -248,11 +248,6 @@ weak int __syscall_prlimit64(pid_t pid, int resource, intptr_t new_limit, intptr return 0; } -weak int __syscall_setsockopt(int sockfd, int level, int optname, intptr_t optval, socklen_t optlen, int unused) { - REPORT(setsockopt); - return -ENOPROTOOPT; // The option is unknown at the level indicated. -} - weak pid_t __syscall_wait4(pid_t pid, intptr_t wstatus, int options, int rusage) { REPORT(wait4); return -1; @@ -262,5 +257,4 @@ UNIMPLEMENTED(acct, (intptr_t filename)) UNIMPLEMENTED(mincore, (intptr_t addr, size_t length, intptr_t vec)) UNIMPLEMENTED(recvmmsg, (int sockfd, intptr_t msgvec, unsigned int vlen, unsigned int flags, intptr_t timeout)) UNIMPLEMENTED(sendmmsg, (int sockfd, intptr_t msgvec, unsigned int vlen, unsigned int flags)) -UNIMPLEMENTED(shutdown, (int sockfd, int how, int unused1, int unused2, int unused3, int unused4)) UNIMPLEMENTED(socketpair, (int domain, int type, int protocol, intptr_t fds, int unused1, int unused2)) diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 373b8ac036c78..da96533d66bc4 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -1767,6 +1767,24 @@ int __syscall_getsockopt(int sockfd, return -ENOSYS; } +int __syscall_setsockopt(int sockfd, + int level, + int optname, + intptr_t optval, + socklen_t optlen, + int unused) { + return -ENOSYS; +} + +int __syscall_shutdown(int sockfd, + int how, + int unused1, + int unused2, + int unused3, + int unused4) { + return -ENOSYS; +} + int __syscall_getsockname(int sockfd, intptr_t addr, intptr_t len, diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index bce01c35eb544..578484cbc597b 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": 268255, - "a.out.nodebug.wasm": 587563, - "total": 855818, + "a.out.js": 268380, + "a.out.nodebug.wasm": 587810, + "total": 856190, "sent": [ "IMG_Init", "IMG_Load", @@ -256,6 +256,8 @@ "__syscall_rmdir", "__syscall_sendmsg", "__syscall_sendto", + "__syscall_setsockopt", + "__syscall_shutdown", "__syscall_socket", "__syscall_stat64", "__syscall_statfs64", @@ -1775,6 +1777,8 @@ "env.__syscall_rmdir", "env.__syscall_sendmsg", "env.__syscall_sendto", + "env.__syscall_setsockopt", + "env.__syscall_shutdown", "env.__syscall_socket", "env.__syscall_stat64", "env.__syscall_statfs64", @@ -2227,8 +2231,6 @@ "__syscall_setpgid", "__syscall_setpriority", "__syscall_setsid", - "__syscall_setsockopt", - "__syscall_shutdown", "__syscall_socketpair", "__syscall_sync", "__syscall_uname", @@ -4096,8 +4098,7 @@ "$__syscall_setdomainname", "$__syscall_setpgid", "$__syscall_setpriority", - "$__syscall_setsockopt", - "$__syscall_shutdown", + "$__syscall_socketpair", "$__syscall_sync", "$__syscall_uname", "$__syscall_wait4", @@ -5089,6 +5090,7 @@ "$shm_open", "$shm_unlink", "$shr", + "$shutdown", "$sift", "$sigaddset", "$sigaltstack", diff --git a/test/sockets/test_tcp_backpressure.c b/test/sockets/test_tcp_backpressure.c new file mode 100644 index 0000000000000..f4db3e8ecb7fb --- /dev/null +++ b/test/sockets/test_tcp_backpressure.c @@ -0,0 +1,120 @@ +/* + * 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. + * + * Write-side backpressure. We connect to a sink server (argv[1]) that accepts + * but never reads, then send non-blocking until the kernel + node buffers fill + * and send() reports EAGAIN. That proves writes are bounded rather than + * buffered without limit. Plain POSIX, also runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int fd = -1; +bool connected = false; +static char chunk[65536]; +// Safety cap so a misbehaving stack that never backpressures can't run forever. +static long long sent_total = 0; +static const long long CAP = 512LL * 1024 * 1024; + +static void finish(int result) { + printf(result == 0 ? "BACKPRESSURE PASS\n" : "BACKPRESSURE FAIL\n"); + if (fd >= 0) close(fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The socket is closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdw; + struct timeval tv = {0}; + FD_ZERO(&fdw); + FD_SET(fd, &fdw); + select(64, NULL, &fdw, NULL, &tv); + + if (!connected && FD_ISSET(fd, &fdw)) { + int err = 0; + socklen_t l = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &l); + if (err != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + } + + if (!connected) return; + + // Push hard. The peer never reads, so this must eventually would-block. + while (sent_total < CAP) { + ssize_t n = send(fd, chunk, sizeof(chunk), 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + printf("backpressure after %lld bytes\n", sent_total); + finish(0); + } else { + printf("send failed: %s\n", strerror(errno)); + finish(1); + } + return; + } + sent_total += n; + } + printf("no backpressure after %lld bytes\n", sent_total); + finish(1); +} + +int main(int argc, char** argv) { + assert(argc > 1 && "usage: test_tcp_backpressure "); + + fd = socket(AF_INET, SOCK_STREAM, 0); + assert(fd >= 0); + fcntl(fd, F_SETFL, O_NONBLOCK); + + struct sockaddr_in dest; + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(atoi(argv[1])); + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + + int r = connect(fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_client_bind.c b/test/sockets/test_tcp_client_bind.c new file mode 100644 index 0000000000000..5a935d2f64bd3 --- /dev/null +++ b/test/sockets/test_tcp_client_bind.c @@ -0,0 +1,147 @@ +/* + * 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. + * + * Under -sNODERAWSOCKETS TCP binds eagerly and synchronously, so: a client that + * bind()s an explicit source port has it honored by connect() (getsockname + * reports it), and a bind() that conflicts with a port already in use fails + * synchronously with EADDRINUSE - exactly where POSIX reports it - rather than + * being deferred. argv: . The same code builds + * and runs natively against the host stack. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +static int client_fd = -1; +static struct sockaddr_in dest; +static uint16_t src_port = 0; +static bool connected = false; +static bool ping_sent = false; + +static void finish(int result) { + printf(result == 0 ? "CLIENT BIND PASS\n" : "CLIENT BIND FAIL\n"); + if (client_fd >= 0) close(client_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static struct sockaddr_in loopback(uint16_t port) { + struct sockaddr_in a; + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + a.sin_port = htons(port); + return a; +} + +static void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(client_fd, &fdr); + FD_SET(client_fd, &fdw); + select(client_fd + 1, &fdr, &fdw, NULL, &tv); + + 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 != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + + // The explicitly bound source port must be the one in use. + struct sockaddr_in sa; + socklen_t sl = sizeof(sa); + assert(getsockname(client_fd, (struct sockaddr*)&sa, &sl) == 0); + if (sa.sin_port != htons(src_port)) { + printf("source port not honored: bound %u, got %u\n", src_port, ntohs(sa.sin_port)); + finish(1); + return; + } + } + + if (connected && !ping_sent && FD_ISSET(client_fd, &fdw)) { + if (send(client_fd, "ping", 4, 0) == 4) ping_sent = true; + } + + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + finish(0); + } else if (n == 0) { + printf("peer closed unexpectedly\n"); + finish(1); + } + } +} + +int main(int argc, char** argv) { + assert(argc > 2 && "usage: test_tcp_client_bind "); + int port = atoi(argv[1]); + src_port = (uint16_t)atoi(argv[2]); + + // A bind() to the port the echo server is already listening on must fail + // synchronously with EADDRINUSE (eager bind, no deferral). + int busy = socket(AF_INET, SOCK_STREAM, 0); + assert(busy >= 0); + struct sockaddr_in inuse = loopback((uint16_t)port); + int br = bind(busy, (struct sockaddr*)&inuse, sizeof(inuse)); + if (!(br == -1 && errno == EADDRINUSE)) { + printf("expected EADDRINUSE binding a busy port, got r=%d errno=%d\n", br, errno); + finish(1); + return 0; + } + close(busy); + + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + fcntl(client_fd, F_SETFL, O_NONBLOCK); + + // Bind the client to the chosen free source port before connecting. + struct sockaddr_in src = loopback(src_port); + assert(bind(client_fd, (struct sockaddr*)&src, sizeof(src)) == 0); + + dest = loopback((uint16_t)port); + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_client_semantics.c b/test/sockets/test_tcp_client_semantics.c new file mode 100644 index 0000000000000..bb45554f06090 --- /dev/null +++ b/test/sockets/test_tcp_client_semantics.c @@ -0,0 +1,132 @@ +/* + * 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. + * + * Outgoing TCP client error/state semantics against a loopback echo server + * started by the test harness (port in argv[1]). Checks connecting twice gives + * EISCONN, that shutdown(SHUT_WR) half-closes the write side while reads still + * work, and that writing after that gives EPIPE. Plain POSIX, also runs + * natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int fd = -1; +struct sockaddr_in dest; +bool connected = false; +bool ping_sent = false; +bool echoed = false; + +static void finish(int result) { + printf(result == 0 ? "CLIENT SEMANTICS PASS\n" : "CLIENT SEMANTICS FAIL\n"); + if (fd >= 0) close(fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The socket is closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(fd, &fdr); + FD_SET(fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + if (!connected && FD_ISSET(fd, &fdw)) { + int err = 0; + socklen_t l = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &l); + if (err != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + + // Connecting an already-connected socket must report EISCONN. + int r = connect(fd, (struct sockaddr*)&dest, sizeof(dest)); + assert(r == -1 && errno == EISCONN); + } + + if (connected && !ping_sent && FD_ISSET(fd, &fdw)) { + if (send(fd, "ping", 4, 0) == 4) ping_sent = true; + } + + if (ping_sent && !echoed && FD_ISSET(fd, &fdr)) { + char buf[4]; + ssize_t n = recv(fd, buf, sizeof(buf), 0); + if (n != 4 || memcmp(buf, "ping", 4) != 0) { + printf("unexpected echo n=%zd\n", n); + finish(1); + return; + } + echoed = true; + + // Half-close the write side. The read side must still be usable, so this + // returns 0 rather than tearing the socket down. + assert(shutdown(fd, SHUT_WR) == 0); + + // Writing after a write-shutdown is a broken pipe. + ssize_t w = send(fd, "more", 4, 0); + assert(w == -1 && errno == EPIPE); + + finish(0); + } +} + +int main(int argc, char** argv) { + assert(argc > 1 && "usage: test_tcp_client_semantics "); + signal(SIGPIPE, SIG_IGN); // so the EPIPE write does not kill us natively + + fd = socket(AF_INET, SOCK_STREAM, 0); + assert(fd >= 0); + fcntl(fd, F_SETFL, O_NONBLOCK); + + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(atoi(argv[1])); + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + + int r = connect(fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_echo.c b/test/sockets/test_tcp_echo.c new file mode 100644 index 0000000000000..c560cc254505b --- /dev/null +++ b/test/sockets/test_tcp_echo.c @@ -0,0 +1,140 @@ +/* + * 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. + * + * Outgoing TCP echo client. We connect to a loopback echo server started by + * the test harness, whose port arrives as argv[1], then do a non-blocking + * connect, send "ping" and recv the echo, all driven by select in the main + * loop. 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 +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int client_fd = -1; +struct sockaddr_in dest; +bool connected = false; +bool ping_sent = false; + +static void finish(int result) { + printf(result == 0 ? "TCP ECHO PASS\n" : "TCP ECHO FAIL\n"); + if (client_fd >= 0) close(client_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The socket is closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(client_fd, &fdr); + FD_SET(client_fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + // connect completion + 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 != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + printf("connected\n"); + + // getpeername goes through emscripten's own address layer, reading the + // backend's sock fields. Check it reports the server we connected to. + struct sockaddr_in pa; + socklen_t pl = sizeof(pa); + assert(getpeername(client_fd, (struct sockaddr*)&pa, &pl) == 0); + assert(pa.sin_port == dest.sin_port); + assert(pa.sin_addr.s_addr == dest.sin_addr.s_addr); + } + + // send ping + if (connected && !ping_sent && FD_ISSET(client_fd, &fdw)) { + if (send(client_fd, "ping", 4, 0) == 4) ping_sent = true; + } + + // receive the echoed ping + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + finish(0); + } else if (n == 0) { + printf("peer closed unexpectedly\n"); + finish(1); + } + } +} + +int main(int argc, char** argv) { + assert(argc > 1 && "usage: test_tcp_echo "); + int port = atoi(argv[1]); + + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + fcntl(client_fd, F_SETFL, O_NONBLOCK); + + // Exercise the setsockopt/getsockopt path and check a round-trip. + int one = 1; + assert(setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) == 0); + assert(setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one)) == 0); + int got = 0; + socklen_t gl = sizeof(got); + // POSIX only promises a nonzero value for a set boolean option, not exactly 1 + // (macOS reports the internal flag bit, for example). + assert(getsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &got, &gl) == 0 && got != 0); + + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(port); + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + printf("connecting to 127.0.0.1:%d\n", port); + + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_ipv6.c b/test/sockets/test_tcp_ipv6.c new file mode 100644 index 0000000000000..2edfac63fbed8 --- /dev/null +++ b/test/sockets/test_tcp_ipv6.c @@ -0,0 +1,188 @@ +/* + * 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. + * + * Self-contained IPv6 TCP loopback accept+echo over ::1. Mirrors the IPv4 + * server test but with AF_INET6/sockaddr_in6, exercising bind(:0)+getsockname, + * listen, accept, non-blocking connect, send and recv over a real IPv6 socket. + * Plain POSIX; also builds and runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +static int listen_fd = -1; +static int client_fd = -1; +static int peer_fd = -1; +static struct sockaddr_in6 dest; +static bool connected = false; +static bool ping_sent = false; +static bool pong_sent = false; + +static void set_nonblocking(int fd) { fcntl(fd, F_SETFL, O_NONBLOCK); } + +static void finish(int result) { + printf(result == 0 ? "TCP IPV6 PASS\n" : "TCP IPV6 FAIL\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(); + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void start_client(void) { + if (client_fd >= 0) close(client_fd); + client_fd = socket(AF_INET6, SOCK_STREAM, 0); + assert(client_fd >= 0); + set_nonblocking(client_fd); + connected = false; + ping_sent = false; + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + finish(1); + } +} + +static 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, &fdr); + FD_SET(client_fd, &fdw); + if (peer_fd >= 0) FD_SET(peer_fd, &fdr); + select(64, &fdr, &fdw, NULL, &tv); + + if (peer_fd < 0 && FD_ISSET(listen_fd, &fdr)) { + struct sockaddr_in6 ca; + socklen_t cl = sizeof(ca); + peer_fd = accept(listen_fd, (struct sockaddr*)&ca, &cl); + if (peer_fd >= 0) { + char ip[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &ca.sin6_addr, ip, sizeof(ip)); + printf("accepted from [%s]:%u\n", ip, (unsigned)ntohs(ca.sin6_port)); + 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; + } + if (err != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + printf("connected\n"); + } + + if (connected && !ping_sent && FD_ISSET(client_fd, &fdw)) { + if (send(client_fd, "ping", 4, 0) == 4) ping_sent = true; + } + + if (peer_fd >= 0 && !pong_sent && FD_ISSET(peer_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(peer_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + send(peer_fd, "pong", 4, 0); + pong_sent = true; + } + } + + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "pong", 4) == 0) { + finish(0); + } else if (n == 0) { + printf("peer closed unexpectedly\n"); + finish(1); + } + } +} + +int main(void) { + listen_fd = socket(AF_INET6, SOCK_STREAM, 0); + assert(listen_fd >= 0); + + // IPV6_V6ONLY is a bind-time option: settable (and readable) before bind. + int v6only = 1, got = 0; + socklen_t gl = sizeof(got); + assert(setsockopt(listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) == 0); + assert(getsockopt(listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &got, &gl) == 0 && got == 1); + + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(0); // ephemeral + assert(inet_pton(AF_INET6, "::1", &addr.sin6_addr) == 1); + if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + + // After bind, IPV6_V6ONLY is immutable: POSIX reports EINVAL. + errno = 0; + assert(setsockopt(listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) == -1 && + errno == EINVAL); + if (listen(listen_fd, 4) != 0) { + perror("listen"); + return 1; + } + + struct sockaddr_in6 la; + socklen_t ll = sizeof(la); + if (getsockname(listen_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + assert(la.sin6_family == AF_INET6); + assert(ntohs(la.sin6_port) != 0); + printf("listening on [::1]:%u\n", (unsigned)ntohs(la.sin6_port)); + set_nonblocking(listen_fd); + + memset(&dest, 0, sizeof(dest)); + dest.sin6_family = AF_INET6; + dest.sin6_port = la.sin6_port; + assert(inet_pton(AF_INET6, "::1", &dest.sin6_addr) == 1); + 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/sockets/test_tcp_refused.c b/test/sockets/test_tcp_refused.c new file mode 100644 index 0000000000000..e445fe8b4062f --- /dev/null +++ b/test/sockets/test_tcp_refused.c @@ -0,0 +1,95 @@ +/* + * 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. + * + * A non-blocking connect to a loopback port with nothing listening must + * surface ECONNREFUSED via SO_ERROR. Self-contained and also runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int fd = -1; + +static void finish(int result) { + printf(result == 0 ? "REFUSED PASS\n" : "REFUSED FAIL\n"); + if (fd >= 0) close(fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The socket is closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdw; + struct timeval tv = {0}; + FD_ZERO(&fdw); + FD_SET(fd, &fdw); + select(64, NULL, &fdw, NULL, &tv); + + if (FD_ISSET(fd, &fdw)) { + int err = 0; + socklen_t l = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &l); + if (err == 0) return; // not resolved yet + printf("connect resolved with errno %d (%s)\n", err, strerror(err)); + finish(err == ECONNREFUSED ? 0 : 1); + } +} + +int main(void) { + fd = socket(AF_INET, SOCK_STREAM, 0); + assert(fd >= 0); + fcntl(fd, F_SETFL, O_NONBLOCK); + + struct sockaddr_in dest; + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = htons(1); // nothing listens on loopback port 1 + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + + // A non-blocking connect may return 0 (emscripten) or -1/EINPROGRESS + // (native), or refuse synchronously. The async failure is checked via + // SO_ERROR in the main loop below. + int r = connect(fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r == -1 && errno == ECONNREFUSED) { + printf("connect resolved with errno %d (%s)\n", errno, strerror(errno)); + printf("REFUSED PASS\n"); + return 0; + } + if (r == -1 && errno != EINPROGRESS) { + perror("connect"); + return 1; + } + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_tcp_server.c b/test/sockets/test_tcp_server.c new file mode 100644 index 0000000000000..88a67e92c4f3e --- /dev/null +++ b/test/sockets/test_tcp_server.c @@ -0,0 +1,190 @@ +/* + * 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. + * + * Self-contained TCP loopback accept+echo. A listener and a client live in one + * process, both non-blocking, driven by select in the main loop. Exercises + * bind(:0) + getsockname (synchronous ephemeral port), listen, accept, + * non-blocking connect, send and recv. 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; // accepted (server-side) connection +struct sockaddr_in dest; +bool connected = false; +bool ping_sent = false; +bool pong_sent = false; + +static void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, O_NONBLOCK); +} + +static void finish(int result) { + printf(result == 0 ? "TCP SERVER PASS\n" : "TCP SERVER FAIL\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(); + // The sockets are closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static 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; + ping_sent = false; + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + if (r != 0 && errno != EINPROGRESS) { + perror("connect"); + finish(1); + } +} + +static 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, &fdr); + FD_SET(client_fd, &fdw); + if (peer_fd >= 0) FD_SET(peer_fd, &fdr); + select(64, &fdr, &fdw, NULL, &tv); + + // server: accept the incoming connection + if (peer_fd < 0 && FD_ISSET(listen_fd, &fdr)) { + struct sockaddr_in ca; + socklen_t cl = sizeof(ca); + peer_fd = accept(listen_fd, (struct sockaddr*)&ca, &cl); + if (peer_fd >= 0) { + set_nonblocking(peer_fd); + printf("accepted from %s:%u\n", inet_ntoa(ca.sin_addr), (unsigned)ntohs(ca.sin_port)); + } + } + + // client: connect completion (retry while the listener is coming up) + 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; + } + if (err != 0) { + printf("connect failed: %s\n", strerror(err)); + finish(1); + return; + } + connected = true; + printf("connected\n"); + } + + // client: send ping + if (connected && !ping_sent && FD_ISSET(client_fd, &fdw)) { + if (send(client_fd, "ping", 4, 0) == 4) ping_sent = true; + } + + // server: echo ping -> pong + if (peer_fd >= 0 && !pong_sent && FD_ISSET(peer_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(peer_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + send(peer_fd, "pong", 4, 0); + pong_sent = true; + } + } + + // client: receive pong + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "pong", 4) == 0) { + finish(0); + } else if (n == 0) { + printf("peer closed unexpectedly\n"); + finish(1); + } + } +} + +int main(void) { + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + +#ifndef NO_EXPLICIT_BIND + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(0); // ephemeral + 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; + } +#endif + // With NO_EXPLICIT_BIND, listen() must auto-bind an ephemeral port (POSIX), + // and getsockname() below must still report it. + if (listen(listen_fd, 4) != 0) { + perror("listen"); + return 1; + } + + // The OS-assigned ephemeral port must be readable synchronously. + struct sockaddr_in la; + socklen_t ll = sizeof(la); + if (getsockname(listen_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + assert(ntohs(la.sin_port) != 0); + printf("listening on 127.0.0.1:%u\n", (unsigned)ntohs(la.sin_port)); + 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/sockets/test_udp_connect.c b/test/sockets/test_udp_connect.c new file mode 100644 index 0000000000000..47b3247daa8e8 --- /dev/null +++ b/test/sockets/test_udp_connect.c @@ -0,0 +1,137 @@ +/* + * 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. + * + * Connected UDP semantics. A client connect()s to a loopback server, which + * means: sendto() with an explicit address must fail with EISCONN, send() + * without an address goes to the peer, and datagrams from anyone other than + * the peer are not delivered. A third "other" socket sends junk to the client + * to prove that filtering. Plain POSIX, also runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int server_fd = -1; +int client_fd = -1; +int other_fd = -1; +struct sockaddr_in server_addr; +bool echoed = false; + +static void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, O_NONBLOCK); +} + +static void finish(int result) { + printf(result == 0 ? "UDP CONNECT PASS\n" : "UDP CONNECT FAIL\n"); + if (server_fd >= 0) close(server_fd); + if (client_fd >= 0) close(client_fd); + if (other_fd >= 0) close(other_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The sockets are closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(server_fd, &fdr); + FD_SET(client_fd, &fdr); + select(64, &fdr, NULL, NULL, &tv); + + // server: receive the peer's ping and echo a pong back to it + if (!echoed && FD_ISSET(server_fd, &fdr)) { + char buf[8]; + struct sockaddr_in src; + socklen_t sl = sizeof(src); + ssize_t n = recvfrom(server_fd, buf, sizeof(buf), 0, (struct sockaddr*)&src, &sl); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + sendto(server_fd, "pong", 4, 0, (struct sockaddr*)&src, sl); + echoed = true; + } + } + + // client: the only datagram it should ever see is the peer's pong, never the + // "junk" sent by the unrelated socket. + if (FD_ISSET(client_fd, &fdr)) { + char buf[8]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "pong", 4) == 0) { + finish(0); + } else if (n > 0) { + printf("client received non-peer datagram (%.*s)\n", (int)n, buf); + finish(1); + } + } +} + +int main(void) { + server_fd = socket(AF_INET, SOCK_DGRAM, 0); + assert(server_fd >= 0); + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr); + assert(bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0); + socklen_t sl = sizeof(server_addr); + assert(getsockname(server_fd, (struct sockaddr*)&server_addr, &sl) == 0); + set_nonblocking(server_fd); + + client_fd = socket(AF_INET, SOCK_DGRAM, 0); + assert(client_fd >= 0); + assert(connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == 0); + + // sendto() with an explicit destination on a connected datagram socket fails. + ssize_t r = sendto(client_fd, "x", 1, 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); + assert(r == -1 && errno == EISCONN); + + set_nonblocking(client_fd); + assert(send(client_fd, "ping", 4, 0) == 4); + + // Learn the client's auto-bound port so the "other" socket can target it. + struct sockaddr_in client_addr; + socklen_t cl = sizeof(client_addr); + assert(getsockname(client_fd, (struct sockaddr*)&client_addr, &cl) == 0); + assert(ntohs(client_addr.sin_port) != 0); + + other_fd = socket(AF_INET, SOCK_DGRAM, 0); + assert(other_fd >= 0); + sendto(other_fd, "junk", 4, 0, (struct sockaddr*)&client_addr, sizeof(client_addr)); + + printf("connected to 127.0.0.1:%u, client port %u\n", + (unsigned)ntohs(server_addr.sin_port), (unsigned)ntohs(client_addr.sin_port)); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_udp_echo.c b/test/sockets/test_udp_echo.c new file mode 100644 index 0000000000000..8f5aa355b532c --- /dev/null +++ b/test/sockets/test_udp_echo.c @@ -0,0 +1,149 @@ +/* + * 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. + * + * Self-contained UDP loopback echo. A server and a client live in one process, + * both non-blocking, driven by select in the main loop. The server binds(:0) + * and reads its assigned port via getsockname (synchronous), the client sends + * a datagram to it, the server echoes it back to the sender. This is plain + * POSIX and also builds and runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int server_fd = -1; +int client_fd = -1; +struct sockaddr_in dest; +bool ping_sent = false; +bool pong_sent = false; + +static void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, O_NONBLOCK); +} + +static void finish(int result) { + printf(result == 0 ? "UDP ECHO PASS\n" : "UDP ECHO FAIL\n"); + if (server_fd >= 0) close(server_fd); + if (client_fd >= 0) close(client_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + // The sockets are closed and the main loop cancelled, so node's event loop + // drains and the process exits naturally with status 0. On failure abort() + // to surface a non-zero exit code to the test harness. + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(server_fd, &fdr); + FD_SET(client_fd, &fdr); + FD_SET(client_fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + // client: send ping + if (!ping_sent && FD_ISSET(client_fd, &fdw)) { + if (sendto(client_fd, "ping", 4, 0, (struct sockaddr*)&dest, sizeof(dest)) == 4) { + ping_sent = true; + } + } + + // server: echo ping -> pong back to the sender + if (!pong_sent && FD_ISSET(server_fd, &fdr)) { + char buf[4]; + struct sockaddr_in src; + socklen_t sl = sizeof(src); + ssize_t n = recvfrom(server_fd, buf, sizeof(buf), 0, (struct sockaddr*)&src, &sl); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + printf("server got ping from %s:%u\n", inet_ntoa(src.sin_addr), (unsigned)ntohs(src.sin_port)); + sendto(server_fd, "pong", 4, 0, (struct sockaddr*)&src, sl); + pong_sent = true; + } + } + + // client: receive pong + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "pong", 4) == 0) { + finish(0); + } + } +} + +int main(void) { + server_fd = socket(AF_INET, SOCK_DGRAM, 0); + client_fd = socket(AF_INET, SOCK_DGRAM, 0); + assert(server_fd >= 0 && client_fd >= 0); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(0); // ephemeral + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + + // The OS-assigned ephemeral port must be readable synchronously. + struct sockaddr_in la; + socklen_t ll = sizeof(la); + if (getsockname(server_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + assert(ntohs(la.sin_port) != 0); + printf("listening on 127.0.0.1:%u\n", (unsigned)ntohs(la.sin_port)); + + // Datagram socket options round-trip on the bound socket. + int ttl = 64, on = 1, rcv = 131072, got; + socklen_t gl = sizeof(got); + assert(setsockopt(server_fd, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) == 0); + assert(setsockopt(server_fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) == 0); + assert(setsockopt(server_fd, SOL_SOCKET, SO_RCVBUF, &rcv, sizeof(rcv)) == 0); + assert(getsockopt(server_fd, IPPROTO_IP, IP_TTL, &got, &gl) == 0 && got == 64); + assert(getsockopt(server_fd, SOL_SOCKET, SO_BROADCAST, &got, &gl) == 0 && got != 0); + assert(getsockopt(server_fd, SOL_SOCKET, SO_RCVBUF, &got, &gl) == 0 && got > 0); + + set_nonblocking(server_fd); + set_nonblocking(client_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); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/sockets/test_udp_ipv6.c b/test/sockets/test_udp_ipv6.c new file mode 100644 index 0000000000000..8028e44440789 --- /dev/null +++ b/test/sockets/test_udp_ipv6.c @@ -0,0 +1,132 @@ +/* + * 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. + * + * Self-contained IPv6 UDP loopback echo over ::1. Mirrors the IPv4 UDP test but + * with AF_INET6/sockaddr_in6: the server binds(:0)+getsockname, the client + * sends a datagram, the server echoes it back to the sender. Plain POSIX; also + * builds and runs natively. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +static int server_fd = -1; +static int client_fd = -1; +static struct sockaddr_in6 dest; +static bool ping_sent = false; +static bool pong_sent = false; + +static void set_nonblocking(int fd) { fcntl(fd, F_SETFL, O_NONBLOCK); } + +static void finish(int result) { + printf(result == 0 ? "UDP IPV6 PASS\n" : "UDP IPV6 FAIL\n"); + if (server_fd >= 0) close(server_fd); + if (client_fd >= 0) close(client_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); + if (result != 0) abort(); +#else + exit(result); +#endif +} + +static void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(server_fd, &fdr); + FD_SET(client_fd, &fdr); + FD_SET(client_fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + if (!ping_sent && FD_ISSET(client_fd, &fdw)) { + if (sendto(client_fd, "ping", 4, 0, (struct sockaddr*)&dest, sizeof(dest)) == 4) { + ping_sent = true; + } + } + + if (!pong_sent && FD_ISSET(server_fd, &fdr)) { + char buf[4]; + struct sockaddr_in6 src; + socklen_t sl = sizeof(src); + ssize_t n = recvfrom(server_fd, buf, sizeof(buf), 0, (struct sockaddr*)&src, &sl); + if (n == 4 && memcmp(buf, "ping", 4) == 0) { + char ip[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, &src.sin6_addr, ip, sizeof(ip)); + printf("server got ping from [%s]:%u\n", ip, (unsigned)ntohs(src.sin6_port)); + sendto(server_fd, "pong", 4, 0, (struct sockaddr*)&src, sl); + pong_sent = true; + } + } + + if (ping_sent && FD_ISSET(client_fd, &fdr)) { + char buf[4]; + ssize_t n = recv(client_fd, buf, sizeof(buf), 0); + if (n == 4 && memcmp(buf, "pong", 4) == 0) { + finish(0); + } + } +} + +int main(void) { + server_fd = socket(AF_INET6, SOCK_DGRAM, 0); + client_fd = socket(AF_INET6, SOCK_DGRAM, 0); + assert(server_fd >= 0 && client_fd >= 0); + + struct sockaddr_in6 addr; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(0); // ephemeral + assert(inet_pton(AF_INET6, "::1", &addr.sin6_addr) == 1); + if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + + struct sockaddr_in6 la; + socklen_t ll = sizeof(la); + if (getsockname(server_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + assert(la.sin6_family == AF_INET6); + assert(ntohs(la.sin6_port) != 0); + printf("listening on [::1]:%u\n", (unsigned)ntohs(la.sin6_port)); + + set_nonblocking(server_fd); + set_nonblocking(client_fd); + + memset(&dest, 0, sizeof(dest)); + dest.sin6_family = AF_INET6; + dest.sin6_port = la.sin6_port; + assert(inet_pton(AF_INET6, "::1", &dest.sin6_addr) == 1); + +#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.py b/test/test_sockets.py index 8226f48df0dd1..d5b23204ced9e 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -347,6 +347,138 @@ def test_nodejs_sockets_echo(self, harness_class, port, args): def test_nodejs_sockets_connect_failure(self): self.do_runf('sockets/test_sockets_echo_client.c', r'connect failed: (Connection refused|Host is unreachable)', regex=True, cflags=['-DSOCKK=666'], assert_returncode=NON_ZERO) + def _run_against_echo_server(self, src, expected, extra=None): + # Start a loopback TCP echo server on an ephemeral port and run the test + # against it, passing the port as argv[1]. + import socketserver + import threading + + class EchoHandler(socketserver.BaseRequestHandler): + def handle(self): + data = self.request.recv(64) + if data: + self.request.sendall(data) + + server = socketserver.TCPServer(('127.0.0.1', 0), EchoHandler) + port = server.server_address[1] + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + self.do_runf(src, expected, cflags=['-sNODERAWSOCKETS'] + (extra or []), args=[str(port)]) + finally: + server.shutdown() + server.server_close() + thread.join() + + # The 'pthread' variant proves the backend works when socket syscalls are + # proxied to the main thread: with PROXY_TO_PTHREAD, main() runs on a worker + # and every socket call funnels to the main thread where node:net lives. + @parameterized({'': [[]], 'pthread': [['-pthread', '-sPROXY_TO_PTHREAD']]}) + def test_noderawsockets_echo(self, args): + # With -sNODERAWSOCKETS the client does a non-blocking connect, send and + # recv over a real OS socket against a loopback echo server we run here. + self._run_against_echo_server('sockets/test_tcp_echo.c', 'TCP ECHO PASS', args) + + def test_noderawsockets_client_bind(self): + # A client that bind()s an explicit source port has it honored by connect(), + # and the plain client path never realizes a private tcp_wrap handle. We + # allocate a free source port here and pass it alongside the echo server's. + import socket + import socketserver + import threading + + class EchoHandler(socketserver.BaseRequestHandler): + def handle(self): + data = self.request.recv(64) + if data: + self.request.sendall(data) + + # Reserve a free loopback port for the client's bound source port. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('127.0.0.1', 0)) + src_port = s.getsockname()[1] + s.close() + + server = socketserver.TCPServer(('127.0.0.1', 0), EchoHandler) + port = server.server_address[1] + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + self.do_runf('sockets/test_tcp_client_bind.c', 'CLIENT BIND PASS', + cflags=['-sNODERAWSOCKETS'], args=[str(port), str(src_port)]) + finally: + server.shutdown() + server.server_close() + thread.join() + + def test_noderawsockets_client_semantics(self): + # EISCONN on a second connect, shutdown(SHUT_WR) leaving reads working, and + # EPIPE on a write after that. + self._run_against_echo_server('sockets/test_tcp_client_semantics.c', 'CLIENT SEMANTICS PASS') + + def test_noderawsockets_refused(self): + # A connect to a loopback port with nothing listening reports ECONNREFUSED. + self.do_runf('sockets/test_tcp_refused.c', 'REFUSED PASS', cflags=['-sNODERAWSOCKETS']) + + def test_noderawsockets_backpressure(self): + # A sink server that accepts but never reads, so the client's writes fill + # the buffers and send() reports EAGAIN rather than buffering unboundedly. + import socketserver + import threading + + done = threading.Event() + + class SinkHandler(socketserver.BaseRequestHandler): + def handle(self): + done.wait(30) # hold the connection open without ever reading + + server = socketserver.TCPServer(('127.0.0.1', 0), SinkHandler) + port = server.server_address[1] + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + self.do_runf('sockets/test_tcp_backpressure.c', 'BACKPRESSURE PASS', + cflags=['-sNODERAWSOCKETS'], args=[str(port)]) + finally: + done.set() + server.shutdown() + server.server_close() + thread.join() + + @parameterized({'': [[]], 'pthread': [['-pthread', '-sPROXY_TO_PTHREAD']]}) + def test_noderawsockets_server(self, args): + # Self-contained loopback accept+echo, exercising bind(:0)+getsockname + # (synchronous ephemeral port), listen, accept, non-blocking connect, send + # and recv over real OS sockets via the tcp_wrap server path. + self.do_runf('sockets/test_tcp_server.c', 'TCP SERVER PASS', cflags=['-sNODERAWSOCKETS'] + args) + + def test_noderawsockets_server_autobind(self): + # listen() without a prior bind() must auto-bind an ephemeral port and + # getsockname() must report it (POSIX), then accept+echo as usual. + self.do_runf('sockets/test_tcp_server.c', 'TCP SERVER PASS', + cflags=['-sNODERAWSOCKETS', '-DNO_EXPLICIT_BIND']) + + def test_noderawsockets_tcp_ipv6(self): + # Self-contained IPv6 TCP loopback accept+echo over ::1: bind(:0)+getsockname, + # listen, accept, non-blocking connect, send/recv on AF_INET6 sockets. + self.do_runf('sockets/test_tcp_ipv6.c', 'TCP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) + + def test_noderawsockets_udp_ipv6(self): + # Self-contained IPv6 UDP loopback echo over ::1 on AF_INET6 sockets. + self.do_runf('sockets/test_udp_ipv6.c', 'UDP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) + + @parameterized({'': [[]], 'pthread': [['-pthread', '-sPROXY_TO_PTHREAD']]}) + def test_noderawsockets_udp(self, args): + # Self-contained loopback UDP echo: the server binds(:0)+getsockname for its + # ephemeral port, the client sends a datagram, the server echoes it back. + self.do_runf('sockets/test_udp_echo.c', 'UDP ECHO PASS', cflags=['-sNODERAWSOCKETS'] + args) + + @parameterized({'': [[]], 'pthread': [['-pthread', '-sPROXY_TO_PTHREAD']]}) + def test_noderawsockets_udp_connect(self, args): + # Connected UDP: sendto() with an address gives EISCONN, send() reaches the + # peer, and datagrams from a non-peer socket are filtered out. + self.do_runf('sockets/test_udp_connect.c', 'UDP CONNECT PASS', cflags=['-sNODERAWSOCKETS'] + args) + @requires_native_clang @requires_python_dev_packages def test_nodejs_sockets_echo_subprotocol(self): diff --git a/tools/link.py b/tools/link.py index 0a01e7544c008..746d530fc38e2 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1818,6 +1818,8 @@ def get_full_import_name(name): # Node-specific settings only make sense if ENVIRONMENT_MAY_BE_NODE if settings.NODERAWFS: diagnostics.warning('unused-command-line-argument', 'NODERAWFS ignored since `node` not in `ENVIRONMENT`') + if settings.NODERAWSOCKETS: + diagnostics.warning('unused-command-line-argument', 'NODERAWSOCKETS ignored since `node` not in `ENVIRONMENT`') if settings.NODE_CODE_CACHING: diagnostics.warning('unused-command-line-argument', 'NODE_CODE_CACHING ignored since `node` not in `ENVIRONMENT`') diff --git a/tools/settings.py b/tools/settings.py index fcdd6a2a419f1..8b75d5cd5bb09 100644 --- a/tools/settings.py +++ b/tools/settings.py @@ -153,6 +153,9 @@ ('CROSS_ORIGIN_STORAGE', 'SINGLE_FILE', 'the .wasm binary is inlined directly into the JS output and has no fetchable URL to key the hash on'), ('CROSS_ORIGIN_STORAGE', 'NO_WASM_ASYNC_COMPILATION', 'synchronous instantiation does not use the COS fetch path'), ('CROSS_ORIGIN_STORAGE', 'SIDE_MODULE', 'no JS glue is emitted to carry the hash or perform the COS lookup'), + ('NODERAWSOCKETS', 'WASMFS', 'the node:net backend is not wired into WASMFS sockets'), + ('NODERAWSOCKETS', 'PROXY_POSIX_SOCKETS', 'they are alternative socket backends'), + ('NODERAWSOCKETS', 'SOCKET_WEBRTC', 'they are alternative socket backends'), ] EXPERIMENTAL_SETTINGS = { From b14cc531a872c0bbe6de722e8bb508e591dda163 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 17 Jun 2026 12:32:14 -0700 Subject: [PATCH 2/7] fix ci --- test/test_sockets.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/test_sockets.py b/test/test_sockets.py index d5b23204ced9e..3689bee8862aa 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -49,6 +49,22 @@ def decorated(self, *args, **kwargs): return decorated +def has_ipv6_loopback(): + # Some CI containers have no IPv6 loopback, so bind(::1) fails with + # EADDRNOTAVAIL. Probe once so the IPv6 tests can skip there. + if not socket.has_ipv6: + return False + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + s.bind(('::1', 0)) + finally: + s.close() + return True + except OSError: + return False + + def clean_process(p): if getattr(p, 'exitcode', None) is None and getattr(p, 'returncode', None) is None: # ask nicely (to try and catch the children) @@ -461,10 +477,14 @@ def test_noderawsockets_server_autobind(self): def test_noderawsockets_tcp_ipv6(self): # Self-contained IPv6 TCP loopback accept+echo over ::1: bind(:0)+getsockname, # listen, accept, non-blocking connect, send/recv on AF_INET6 sockets. + if not has_ipv6_loopback(): + self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_tcp_ipv6.c', 'TCP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) def test_noderawsockets_udp_ipv6(self): # Self-contained IPv6 UDP loopback echo over ::1 on AF_INET6 sockets. + if not has_ipv6_loopback(): + self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_udp_ipv6.c', 'UDP IPV6 PASS', cflags=['-sNODERAWSOCKETS']) @parameterized({'': [[]], 'pthread': [['-pthread', '-sPROXY_TO_PTHREAD']]}) From 131b198085fc6afbf38a41700bf20b2a0b91a349 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 17 Jun 2026 12:36:45 -0700 Subject: [PATCH 3/7] Add -sNODERAWSOCKETS backend for real TCP and UDP sockets via node:net Adds a new NODERAWSOCKETS setting that backs the POSIX sockets API directly with Node.js's node:net and node:dgram, giving real, non-blocking TCP and UDP sockets without WebSockets, an external proxy process, or pthreads. This is the sockets counterpart to NODERAWFS: where NODERAWFS gives direct access to the host filesystem, this gives direct access to host sockets. Unlike PROXY_POSIX_SOCKETS this is single-threaded and event-driven: socket readiness is delivered through the same emscripten_set_socket_*_callback hooks the default WebSocket backend uses, so it drops into existing readiness reactors unchanged. Under -pthread the socket syscalls are proxied to the main thread, so the backend always runs on node's event loop and a SharedArrayBuffer heap is safe. Supported: * TCP clients: connect, send, recv, shutdown and close, with non-blocking semantics and backpressure (send reports EAGAIN rather than buffering unboundedly). * TCP servers: bind, listen, accept, getsockname/getpeername. * UDP: bind, connect, sendto/recvfrom, with connected-peer filtering. * IPv4 and IPv6 (AF_INET6): TCP and UDP over v6, including IPV6_V6ONLY. * get/setsockopt: SO_ERROR, SO_KEEPALIVE and TCP_KEEPIDLE, TCP_NODELAY, SO_RCVBUF/SO_SNDBUF, SO_BROADCAST, IP_TTL, SO_REUSEPORT and IPV6_V6ONLY. Options are mirrored to a cache (the getsockopt source of truth) and projected onto the live socket; we only report options we can actually honor (e.g. SO_REUSEADDR reads back as 1 since libuv forces it on, and IPV6_V6ONLY returns EINVAL if changed after bind). Binding is eager and synchronous, so a conflict surfaces as EADDRINUSE at bind() and getsockname() reports the kernel-assigned ephemeral port immediately - there is no deferred-bind or lazy-handle promotion. A bound socket is a role-neutral handle, adopted as-is by listen() (server.listen) or connect() (net.Socket), and released by close() only if it was never adopted. Bind-time options (ipv6Only, reusePort) are passed to the handle at construction. The bind primitive is selected once per capability: * the public, synchronous net.BoundHandle (and dgram bindSync/connectSync) when the Node.js runtime provides them; and * the private tcp_wrap/udp_wrap bindings as a fallback on Node.js versions that do not (bind6/send6 for IPv6). Details: * new node backend in src/lib/libsockfs_node.js, pulled in only under -sNODERAWSOCKETS, implementing the sock_ops contract * __syscall_setsockopt and __syscall_shutdown now live in JS, routing to the backend under NODERAWSOCKETS (else reporting the option/feature as unsupported), avoiding a libstubs variation * tests under test/sockets exercise TCP echo, server accept/echo (including listen-without-bind autobind), client source-port bind plus synchronous EADDRINUSE, client semantics (EISCONN, half-close, EPIPE), backpressure, connection refused, UDP echo/connect, and IPv6 TCP/UDP over ::1 (including IPV6_V6ONLY before/after bind); all build and run natively against the host stack and run under node, including PROXY_TO_PTHREAD variants --- test/test_sockets.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_sockets.py b/test/test_sockets.py index 3689bee8862aa..d6358d03bedfc 100644 --- a/test/test_sockets.py +++ b/test/test_sockets.py @@ -55,11 +55,8 @@ def has_ipv6_loopback(): if not socket.has_ipv6: return False try: - s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - try: + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: s.bind(('::1', 0)) - finally: - s.close() return True except OSError: return False From 5b8848e285c93bcc1213413bcbd8819b0ededa1a Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 19 Jun 2026 13:27:26 -0700 Subject: [PATCH 4/7] codesize --- test/codesize/test_codesize_hello_dylink_all.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 578484cbc597b..3c83583293eb9 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": 268380, - "a.out.nodebug.wasm": 587810, - "total": 856190, + "a.out.js": 268405, + "a.out.nodebug.wasm": 587815, + "total": 856220, "sent": [ "IMG_Init", "IMG_Load", From 2e7d187ba88357e2ef16be26cfbe1f614576ff6c Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 19 Jun 2026 14:10:17 -0700 Subject: [PATCH 5/7] code size --- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 3c83583293eb9..4e8d2cb0e5754 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": 268405, - "a.out.nodebug.wasm": 587815, - "total": 856220, + "a.out.nodebug.wasm": 587733, + "total": 856138, "sent": [ "IMG_Init", "IMG_Load", From df520dbdc696c4402b5f86181f7ae5c6eefa7b20 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 19 Jun 2026 16:33:38 -0700 Subject: [PATCH 6/7] BoundHandle -> BoundSocket --- .../tools_reference/settings_reference.rst | 7 ++----- src/lib/libsockfs_node.js | 20 +++++++++---------- src/settings.js | 7 ++----- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 74a8aa3672644..2a7ed01a2b631 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -601,11 +601,8 @@ direct access to host sockets. It only works under node and is ignored elsewhere. It supports full TCP (outgoing connect plus bind, listen and accept for -servers) and UDP. TCP clients use the public node:net API. bind needs a -synchronous bind() + getsockname(), so it uses the public node APIs that -provide them when present - net.BoundHandle for TCP and dgram -bindSync/connectSync for UDP - and falls back to the private tcp_wrap/udp_wrap -handles on older Node.js versions that lack them. +servers) and UDP. TCP clients use the public node:net API when possible, +falling back to the private tcp_wrap/udp_wrap handles on older Node.js. It is event-driven. Socket readiness comes through the same ``emscripten_set_socket_*_callback`` hooks the WebSocket backend uses, so it diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index 584fd96654910..50efa719a4955 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -13,7 +13,7 @@ // 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.BoundHandle when the runtime offers it, else the private tcp_wrap binding +// 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 @@ -83,27 +83,27 @@ var NodeSockFSLibrary = { }, // 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.BoundHandle when node offers it, + // a bound handle is produced: the public net.BoundSocket when node offers it, // else the private tcp_wrap binding. Chosen once, like useDgram(). - useBoundHandle() { - return nodeSockOps.boundHandleOk ??= - typeof nodeSockOps.getNet().BoundHandle == 'function'; + useBoundSocket() { + return nodeSockOps.boundSocketOk ??= + typeof nodeSockOps.getNet().BoundSocket == 'function'; }, // 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.BoundHandle, or a raw tcp_wrap handle - adopted as-is by + // 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 (nodeSockOps.useBoundHandle()) { + if (nodeSockOps.useBoundSocket()) { var bh; // 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 { - bh = new (nodeSockOps.getNet().BoundHandle)({ + bh = new (nodeSockOps.getNet().BoundSocket)({ host: addr, port, ipv6Only: o.ipv6Only, reusePort: o.reusePort, }); } @@ -634,7 +634,7 @@ var NodeSockFSLibrary = { // reports 1). It cannot be turned off. return 0; case {{{ cDefs.SO_REUSEPORT }}}: // SO_REUSEPORT. Bind-time: cached and - // passed to the BoundHandle at bind. Set after bind has no effect. + // passed to the BoundSocket at bind. Set after bind has no effect. sock.opts.reusePort = !!val; return 0; } @@ -648,7 +648,7 @@ var NodeSockFSLibrary = { if (optname === {{{ 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 BoundHandle constructor. + // bind/connect/listen we cache it for the BoundSocket constructor. if (sock.state) return -{{{ cDefs.EINVAL }}}; sock.opts.ipv6Only = !!val; return 0; diff --git a/src/settings.js b/src/settings.js index e4a091d9cb4ca..eed2a145001eb 100644 --- a/src/settings.js +++ b/src/settings.js @@ -427,11 +427,8 @@ var PROXY_POSIX_SOCKETS = false; // elsewhere. // // It supports full TCP (outgoing connect plus bind, listen and accept for -// servers) and UDP. TCP clients use the public node:net API. bind needs a -// synchronous bind() + getsockname(), so it uses the public node APIs that -// provide them when present - net.BoundHandle for TCP and dgram -// bindSync/connectSync for UDP - and falls back to the private tcp_wrap/udp_wrap -// handles on older Node.js versions that lack them. +// servers) and UDP. TCP clients use the public node:net API when possible, +// falling back to the private tcp_wrap/udp_wrap handles on older Node.js. // // It is event-driven. Socket readiness comes through the same // ``emscripten_set_socket_*_callback`` hooks the WebSocket backend uses, so it From cb6e6cdccfe685832a6a707941b694186b2ae33f Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 19 Jun 2026 17:43:10 -0700 Subject: [PATCH 7/7] ci: rerun