镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Sending/receiving ancillary data (FDs) over Unix domain sockets #53391

Closed as not planned

Description

@joepie91

What is the problem this feature will solve?

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.

Activity

  1. benjamingr commented on Jun 9, 2024

    @benjamingr
    Member

    @nodejs/libuv

  2. juanarbol commented on Oct 31, 2024

    @juanarbol
    Member

    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?

  3. joepie91 commented on Oct 31, 2024

    @joepie91
    ContributorAuthor

    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.

  4. bnoordhuis commented on Oct 31, 2024

    @bnoordhuis
    Member

    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.

  5. immjs commented on Dec 16, 2024

    @immjs

    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?

  6. bnoordhuis commented on Dec 16, 2024

    @bnoordhuis
    Member

    @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.

  7. josephrocca commented on Feb 11, 2025

    @josephrocca

    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):

    import net from 'node:net';
    const socketPath = '/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.js
    const client = net.createConnection(socketPath, () => {
      const fd = client._handle.fd;
      console.log(`Worker is connected to proxy's Unix Domain Socket, fd:`, fd);
      const pipe = new Pipe(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.
        let handle = 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.
          let socket = new net.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,
    };
    
    static UNIX_SOCKET_PATH: &str = "/tmp/worker_0.sock";
    static PORT: u16 = 9000;
    
    /// Sends a raw file descriptor across a Unix stream using SCM_RIGHTS.
    fn send_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]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
      // 1. Create the Unix domain socket first
      // Make sure to clean up any existing socket file
      let _ = 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 listener
      let listener = TcpListener::bind(("0.0.0.0", PORT)).await?;
      println!("Proxy listening on TCP port {}...", PORT);
    
      // 3. Accept one connection from the backend
      let (unix_stream, _) = unix_listener.accept().await?;
      println!("Backend connected!");
    
      // 4. Main connection acceptance loop
      loop {
          match listener.accept().await {
              Ok((tcp_stream, _addr)) => {
                  let fd = tcp_stream.as_raw_fd();
                  if let Err(e) = send_fd(&unix_stream, fd) {
                      eprintln!("Error sending fd: {e}");
                  }
                  drop(tcp_stream);
              }
              Err(e) => {
                  eprintln!("Accept error: {e}");
              }
          }
      }
    }

    References:

  8. github-actions commented on Aug 11, 2025

    @github-actions
    Contributor

    There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the never-stale Issues 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.

  9. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Aug 11, 2025
  10. github-actions commented on Sep 11, 2025

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues requesting new Node.js features.staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions