From 652e5347d968a1cea35d20463772712ce6bd93a1 Mon Sep 17 00:00:00 2001 From: Tom McLaughlin Date: Sun, 2 Aug 2026 23:25:52 -0700 Subject: [PATCH 1/2] Add fd:// transport for an already-connected socket --- src/main.rs | 17 +++++++---- src/transport.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 664efc1..6374fd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ //! `p9fuse` binary: a general-purpose 9p2000.L mounter built on the `p9fuse` library crate. Two //! subcommands mount a remote 9p server -- `mount9p` via the kernel v9fs client (needs //! CAP_SYS_ADMIN) and `mount9p-fuse` via an unprivileged userspace FUSE bridge. The server is -//! selected by `--connect`'s URL scheme: `tcp://host:port`, `unix:///path`, or `ws://` / `wss://`. +//! selected by `--connect`'s URL scheme: `tcp://host:port`, `unix:///path`, `ws://` / `wss://`, +//! or `fd://N` for a socket that was already connected and handed to us. use clap::{Parser, Subcommand}; -use p9fuse::transport::{NineTransport, TcpTransport, UnixTransport, WebSocketTransport}; +use p9fuse::transport::{FdTransport, NineTransport, TcpTransport, UnixTransport, WebSocketTransport}; use p9fuse::{fuse9p, mount9p}; use std::path::{Path, PathBuf}; @@ -24,7 +25,8 @@ enum Cmd { /// kernel modules). Prefer `mount9p-fuse` where privilege isn't available. #[command(name = "mount9p")] Mount9p { - /// Server URL: tcp://host:port, unix:///path, or ws://.../wss://... (`--connect-ws` alias). + /// Server URL: tcp://host:port, unix:///path, ws://.../wss://... (`--connect-ws` alias), + /// or fd://N for an already-connected socket on that file descriptor. #[arg(long, alias = "connect-ws")] connect: String, /// Extra websocket handshake header(s), "Name: Value" (e.g. an auth token). Repeatable. @@ -49,7 +51,8 @@ enum Cmd { /// `chmod` works. #[command(name = "mount9p-fuse")] Mount9pFuse { - /// Server URL: tcp://host:port, unix:///path, or ws://.../wss://... (`--connect-ws` alias). + /// Server URL: tcp://host:port, unix:///path, ws://.../wss://... (`--connect-ws` alias), + /// or fd://N for an already-connected socket on that file descriptor. #[arg(long, alias = "connect-ws")] connect: String, /// Extra websocket handshake header(s), "Name: Value" (e.g. an auth token). Repeatable. @@ -231,9 +234,13 @@ async fn build_transport( Ok(Box::new( WebSocketTransport::connect(connect, headers).await?, )) + } else if let Some(fd) = connect.strip_prefix("fd://") { + // No retry: the fd is either a connected socket or it isn't -- there's nothing to wait for. + let fd: std::os::unix::io::RawFd = fd.parse()?; + Ok(Box::new(unsafe { FdTransport::from_raw_fd(fd)? })) } else { Err(format!( - "unsupported --connect {connect:?}: use tcp://host:port, unix:///path, or ws://.../wss://..." + "unsupported --connect {connect:?}: use tcp://host:port, unix:///path, ws://.../wss://..., or fd://N" ) .into()) } diff --git a/src/transport.rs b/src/transport.rs index 66f9e75..24d0f7d 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -66,6 +66,79 @@ impl NineTransport for TcpTransport { } } +/// 9p over a socket someone else already connected and passed to us on a file descriptor. +/// +/// For the case where this process cannot do the connecting: the peer dials *in*, and whoever +/// accepted the connection hands the socket over (as stdin, say) rather than relaying its bytes. +/// That keeps every byte on a direct kernel path between the 9p server and this process. +/// +/// Takes ownership of `fd`, and works for either an AF_INET/AF_INET6 or an AF_UNIX socket -- +/// whoever accepted it decides which, so ask the kernel rather than assume. +pub struct FdTransport(FdInner); + +enum FdInner { + Tcp(TcpStream), + Unix(UnixStream), +} + +impl FdTransport { + /// # Safety + /// `fd` must be a connected socket that nothing else owns or reads/writes concurrently. + pub unsafe fn from_raw_fd(fd: std::os::unix::io::RawFd) -> io::Result { + use std::os::unix::io::FromRawFd; + + // tokio requires a nonblocking fd; an inherited one usually isn't. + let flags = libc::fcntl(fd, libc::F_GETFL); + if flags < 0 { + return Err(io::Error::last_os_error()); + } + if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 { + return Err(io::Error::last_os_error()); + } + + let mut domain: libc::c_int = 0; + let mut len = std::mem::size_of::() as libc::socklen_t; + let rc = libc::getsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_DOMAIN, + &mut domain as *mut _ as *mut libc::c_void, + &mut len, + ); + if rc < 0 { + return Err(io::Error::last_os_error()); + } + + match domain { + libc::AF_UNIX => Ok(Self(FdInner::Unix(UnixStream::from_std( + std::os::unix::net::UnixStream::from_raw_fd(fd), + )?))), + libc::AF_INET | libc::AF_INET6 => Ok(Self(FdInner::Tcp(TcpStream::from_std( + std::net::TcpStream::from_raw_fd(fd), + )?))), + other => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("fd {fd} is a socket of unsupported domain {other}"), + )), + } + } +} + +impl NineTransport for FdTransport { + fn split(self: Box) -> (ByteSink, ByteStream) { + match self.0 { + FdInner::Tcp(s) => { + let (rd, wr) = s.into_split(); + framed(rd, wr) + } + FdInner::Unix(s) => { + let (rd, wr) = s.into_split(); + framed(rd, wr) + } + } + } +} + /// 9p over a Unix-domain socket -- for a server on the same host (rootless diod, a local export). pub struct UnixTransport(pub UnixStream); From 1a1a932a887dc06466f00e722a9f27278bc66841 Mon Sep 17 00:00:00 2001 From: thomasjm Date: Wed, 5 Aug 2026 17:55:36 -0700 Subject: [PATCH 2/2] Fix cargo fmt --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 6374fd8..e09e522 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,9 @@ //! or `fd://N` for a socket that was already connected and handed to us. use clap::{Parser, Subcommand}; -use p9fuse::transport::{FdTransport, NineTransport, TcpTransport, UnixTransport, WebSocketTransport}; +use p9fuse::transport::{ + FdTransport, NineTransport, TcpTransport, UnixTransport, WebSocketTransport, +}; use p9fuse::{fuse9p, mount9p}; use std::path::{Path, PathBuf};