Add -sNODERAWSOCKETS backend for real TCP & UDP on Node.js#27080
Add -sNODERAWSOCKETS backend for real TCP & UDP on Node.js#27080guybedford wants to merge 3 commits into
Conversation
sbc100
left a comment
There was a problem hiding this comment.
Nice! I've not yet reviewed the meat of libsockfs.js but looks good so far.
sbc100
left a comment
There was a problem hiding this comment.
I still need to review the details of libsockfs_node.js but the general shape here LGTM!
97ce010 to
43d6cd4
Compare
sbc100
left a comment
There was a problem hiding this comment.
Nice work on all the test cases. I can't say I've read all the test code yet though..
Can you confirm they all run on linux and/or macOS nativly too? (at least the ones where it makes sense that they could).
17092cd to
035cd77
Compare
This comment was marked as outdated.
This comment was marked as outdated.
d179b41 to
5f24cd5
Compare
|
@sbc100 this is ready for final review to land. The tests are comprehensive, and I've also tested it in real world end-to-end Rust applications. Any outstanding issues should be fairly straightforward guarded followups under this feature. The last topic that does also come up here is DNS, but I plan to make a follow-on PR for this instead. |
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
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
This adds a new
-sNODERAWSOCKETSsetting that for supporting direct full sockets on Node.js via thenode:netfor TCP andnode:dgramfor UDP modules, without needingws, an external proxy process, or pthreads.node:netAPIs.node:dgramAPIsTo support these embeddings without JSPI being mandatory requires using
process.binding('tcp_wrap')andprocess.binding('udp_wrap')in Node.js, which are used here to support older versions of Node.js.Further, to avoid having to rely on private APIs in modern Node.js I actually implemented upstream Node.js PRs to make public API surface area available for the full embedding in the following:
Then this PR conditionally checks these features and uses the public APIs as defined by them when it is able to, falling back to
tcp_wrapandudp_wraponly when not supported. While Node.js with these features has not yet been released, as soon as all three have landed, we can rely on this surface area for modern 26+ versions of Node.js compat.Note: AI was used to create this PR, under my review.