You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Sending/receiving ancillary data (FDs) over Unix domain sockets #53391
Ability to send/receive ancillary data over Unix domain sockets. This is necessary for eg. implementation of the Wayland protocol (which uses this feature for high-performance sharing of memory buffers between compositors and clients), and thereby allows implementing graphical applications for Wayland systems.
What is the feature you are proposing to solve the problem?
Some kind of extension to the net core module that allows for sending file descriptors over (Unix domain) sockets, those being sent as ancillary data. Support for this seems to already exist in libuv, from what I've been able to find; it just isn't wired to the net core module in any way.
What alternatives have you considered?
I've looked through all the relevant source code in core modules (both the JS and C++ side) to figure out if there is already a way to do this, but was unable to find one; it seems that the StreamBase:Writev method hard-sets the ancillary FD to a null pointer, for instance.
Sending/receiving FDs is a hard requirement for Wayland protocol implementation, and there are no alternative mechanisms. I'm unable to write C/C++ and therefore cannot talk to libuv directly myself.
I don't know if there's an equivalent functionality in Windows. I do not use Windows enough to know.
But I don't think this (particular form of this) feature being "UNIX-only" is necessarily a reason not to support it, either - my reason for opening an issue is because implementing it requires an integration with the existing net module (and libuv) for which there currently seem to be no suitable integration points. If I thought this could credibly done outside of core, I would not have opened an issue, as I subscribe to the idea of a 'minimal core'.
Indeed there are quite a few things in the core modules already which are in some way platform-specific, but which could not feasibly have been implemented externally (like named pipes and domain sockets themselves, which definitely are not cross-platform features and behave quite differently).
While it would technically be possible to implement as a native extension module, as far as I can tell it would pretty much require replicating the entire net module and tracking its development and porting any changes. That seems like a somewhat absurd amount of work (and not a particularly reliable approach) to achieve what seems to amount to "changing the net code so it passes through an extra argument" :)
Of course it's possible that I've overlooked something and that there is in fact a way to trivially do this externally. If so, please let me know! But if there is such a way, then I don't think anyone's found it, judging from the lack of solutions to this problem - with every past attempt being abandoned, except for one module that basically partially reimplements net as described above and which has an unclear maintenance state.
Libuv and node support SCM_RIGHTS and so does node but it's wired up in a way that makes the SCM_RIGHTS file descriptors show up as regular accepted incoming connections.
If you grep around the JS code in lib/, you'll see new Pipe(PipeConstants.IPC) calls - on Unices, that's a UNIX socket that can send and receive SCM_RIGHTS messages. To send file descriptors, you need to wrap them in a net.Socket first.
Regarding receiving ancillary messages, I've been looking into it since the past weekend (with no prior experience in low level programming so I might be missing a few things), and noticed that the FDs are accepted by libuv, but not passed through the "data" event; more specifically, it seems to be lost between the read_cb given to libuv's uv_read_start, and StreamBase::OnUvRead called by said callback
I'm guessing StreamBase is not unique to sockets, let alone UNIX domain sockets, so would it be better to send a separate event (something like "accepted_fd") that contains the relevant new fd which could be opened using new net.Socket({ fd })?
Similarly for sending, would it be possible to have a queue of FDs to be sent, which would maybe prevent having to interfere with StreamBase itself?
Libuv doesn't hand you SCM_RIGHTS file descriptors directly (because that doesn't translate well to Windows), instead they show up as regular incoming connections that you uv_accept().
That's why node wraps them in a StreamBase. From node's perspective they're indistinguishable from normal stream connections.
I spent a couple of days flailing about with a SCM_RIGHTS-based connection hand-off proxy for WebSocket backend servers, and this Github issue is about the only result in Google that talks about SCM_RIGHTS in Node.js, so in case others also struggle, here's a working example.
The bug that cost me most of that time was that I didn't realize that it must(?) be the "sender" process that creates the socket file, and the receiver listens to it as a client, rather than the other way around. That's not true for my Rust version of the code, so I didn't think to try that.
Anyway, here's the working code (and I've attached the Rust proxy code below for reference):
importnetfrom'node:net';constsocketPath='/tmp/worker_0.sock';const{ Pipe,constants: PipeConstants}=process.binding('pipe_wrap');// Caution: This is Node-internal, and it's technically possible for it to be changed/removed in future versions of Node.jsconstclient=net.createConnection(socketPath,()=>{constfd=client._handle.fd;console.log(`Worker is connected to proxy's Unix Domain Socket, fd:`,fd);constpipe=newPipe(PipeConstants.IPC);pipe.open(fd);pipe.onread=(arrayBuffer)=>{// Note: This arrayBuffer can be ignored. IIUC, it's just the obligatory 'dummy' data that goes along with the `SCM_RIGHTS` message.lethandle=pipe.pendingHandle;if(handle){pipe.pendingHandle=null;// Note: If using uWebSockets.js, you can remove the lines below and just call `app.adoptSocket(handle.fd)` here to let it take over the TCP request.letsocket=newnet.Socket({ handle,readable:true,writable:true});socket.on('data',(data)=>{console.log(data.toString());});}};pipe.pendingHandle=null;pipe.readStart();});
Rust hand-off proxy ("sender"), for reference:
Click to show code
use std::{
io::IoSlice,
os::fd::{AsRawFd,RawFd},
time::Duration,};use tokio::{
net::{TcpListener,TcpStream,UnixListener},// Added UnixListener
time::sleep,};use tokio::net::UnixStream;use nix::sys::socket::{
sendmsg,ControlMessage,MsgFlags,SockaddrStorage,};staticUNIX_SOCKET_PATH:&str = "/tmp/worker_0.sock";staticPORT:u16 = 9000;/// Sends a raw file descriptor across a Unix stream using SCM_RIGHTS.fnsend_fd(unix_stream:&UnixStream,fd_to_send:RawFd) -> nix::Result<()>{let iov = [IoSlice::new(b"fd")];let fds = [fd_to_send];let cmsg = [ControlMessage::ScmRights(&fds)];sendmsg::<SockaddrStorage>(
unix_stream.as_raw_fd(),&iov,&cmsg,MsgFlags::empty(),None,)?;Ok(())}#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{// 1. Create the Unix domain socket first// Make sure to clean up any existing socket filelet _ = std::fs::remove_file(UNIX_SOCKET_PATH);let unix_listener = UnixListener::bind(UNIX_SOCKET_PATH)?;println!("Waiting for backend to connect to {}...",UNIX_SOCKET_PATH);// 2. Create TCP listenerlet listener = TcpListener::bind(("0.0.0.0",PORT)).await?;println!("Proxy listening on TCP port {}...",PORT);// 3. Accept one connection from the backendlet(unix_stream, _) = unix_listener.accept().await?;println!("Backend connected!");// 4. Main connection acceptance looploop{match listener.accept().await{Ok((tcp_stream, _addr)) => {let fd = tcp_stream.as_raw_fd();ifletErr(e) = send_fd(&unix_stream, fd){eprintln!("Error sending fd: {e}");}drop(tcp_stream);}Err(e) => {eprintln!("Accept error: {e}");}}}}
There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the
never-staleIssues and PRs exempt from automated stale handling.
label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.
There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.
Activity
benjamingr commentedon Jun 9, 2024
@nodejs/libuv
juanarbol commentedon Oct 31, 2024
Not sure if Node.js is the place for "UNIX-only" features. Do you think that it could be feasible on different platforms like windows?
joepie91 commentedon Oct 31, 2024
I don't know if there's an equivalent functionality in Windows. I do not use Windows enough to know.
But I don't think this (particular form of this) feature being "UNIX-only" is necessarily a reason not to support it, either - my reason for opening an issue is because implementing it requires an integration with the existing
netmodule (and libuv) for which there currently seem to be no suitable integration points. If I thought this could credibly done outside of core, I would not have opened an issue, as I subscribe to the idea of a 'minimal core'.Indeed there are quite a few things in the core modules already which are in some way platform-specific, but which could not feasibly have been implemented externally (like named pipes and domain sockets themselves, which definitely are not cross-platform features and behave quite differently).
While it would technically be possible to implement as a native extension module, as far as I can tell it would pretty much require replicating the entire
netmodule and tracking its development and porting any changes. That seems like a somewhat absurd amount of work (and not a particularly reliable approach) to achieve what seems to amount to "changing the net code so it passes through an extra argument" :)Of course it's possible that I've overlooked something and that there is in fact a way to trivially do this externally. If so, please let me know! But if there is such a way, then I don't think anyone's found it, judging from the lack of solutions to this problem - with every past attempt being abandoned, except for one module that basically partially reimplements
netas described above and which has an unclear maintenance state.bnoordhuis commentedon Oct 31, 2024
Libuv and node support SCM_RIGHTS and so does node but it's wired up in a way that makes the SCM_RIGHTS file descriptors show up as regular accepted incoming connections.
If you grep around the JS code in lib/, you'll see
new Pipe(PipeConstants.IPC)calls - on Unices, that's a UNIX socket that can send and receive SCM_RIGHTS messages. To send file descriptors, you need to wrap them in anet.Socketfirst.immjs commentedon Dec 16, 2024
Regarding receiving ancillary messages, I've been looking into it since the past weekend (with no prior experience in low level programming so I might be missing a few things), and noticed that the FDs are accepted by libuv, but not passed through the
"data"event; more specifically, it seems to be lost between theread_cbgiven to libuv'suv_read_start, andStreamBase::OnUvReadcalled by said callbackI'm guessing
StreamBaseis not unique to sockets, let alone UNIX domain sockets, so would it be better to send a separate event (something like"accepted_fd") that contains the relevant new fd which could be opened usingnew net.Socket({ fd })?Similarly for sending, would it be possible to have a queue of FDs to be sent, which would maybe prevent having to interfere with
StreamBaseitself?bnoordhuis commentedon Dec 16, 2024
@immjs that's not how libuv models it
Libuv doesn't hand you SCM_RIGHTS file descriptors directly (because that doesn't translate well to Windows), instead they show up as regular incoming connections that you uv_accept().
That's why node wraps them in a StreamBase. From node's perspective they're indistinguishable from normal stream connections.
josephrocca commentedon Feb 11, 2025
I spent a couple of days flailing about with a
SCM_RIGHTS-based connection hand-off proxy for WebSocket backend servers, and this Github issue is about the only result in Google that talks about SCM_RIGHTS in Node.js, so in case others also struggle, here's a working example.The bug that cost me most of that time was that I didn't realize that it must(?) be the "sender" process that creates the socket file, and the receiver listens to it as a client, rather than the other way around. That's not true for my Rust version of the code, so I didn't think to try that.
Anyway, here's the working code (and I've attached the Rust proxy code below for reference):
Rust hand-off proxy ("sender"), for reference:
Click to show code
References:
github-actions commentedon Aug 11, 2025
There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the never-staleIssues and PRs exempt from automated stale handling.
label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment.
For more information on how the project manages feature requests, please consult the feature request management document.
github-actions commentedon Sep 11, 2025
There has been no activity on this feature request and it is being closed. If you feel closing this issue is not the right thing to do, please leave a comment.
For more information on how the project manages feature requests, please consult the feature request management document.
feat: say why UNIX_FD is unsupported, and scope what it would take (#344
feat(bun): a unix transport that carries file descriptors (#400)
feat(unixsock): a unix socket that passes descriptors, on the event l…