use std::io::{Read, Write};
use std::os::fd::OwnedFd;
use std::os::unix::net::UnixStream;
use std::path::Path;
use super::{read_frame, write_frame, PeerIdentity, RpcTransport, MAX_FRAME_LEN};
use crate::rpc::{RpcError, RpcResult};
pub(crate) const MAX_FDS_PER_FRAME: usize = 64;
pub struct UnixTransport {
stream: UnixStream,
peer: PeerIdentity,
desc: String,
fd_recv_buf: std::sync::Mutex<Vec<u8>>,
}
impl UnixTransport {
pub fn from_stream(stream: UnixStream) -> RpcResult<Self> {
let peer = resolve_peer(&stream);
let desc = match stream.peer_addr() {
Ok(a) => format!("unix:{a:?}"),
Err(_) => "unix:socketpair".to_string(),
};
Ok(UnixTransport {
stream,
peer,
desc,
fd_recv_buf: std::sync::Mutex::new(Vec::new()),
})
}
pub fn pair() -> RpcResult<(Self, Self)> {
use rustix::net::{AddressFamily, SocketFlags, SocketType};
let (a, b) = rustix::net::socketpair(
AddressFamily::UNIX,
SocketType::STREAM,
SocketFlags::empty(),
None,
)
.map_err(std::io::Error::from)?;
for fd in [&a, &b] {
rustix::io::fcntl_setfd(fd, rustix::io::FdFlags::CLOEXEC)
.map_err(std::io::Error::from)?;
}
Ok((
Self::from_stream(UnixStream::from(a))?,
Self::from_stream(UnixStream::from(b))?,
))
}
pub fn from_owned_fd(fd: OwnedFd) -> RpcResult<Self> {
Self::from_stream(UnixStream::from(fd))
}
pub fn connect(path: impl AsRef<Path>) -> RpcResult<Self> {
let stream = UnixStream::connect(path)?;
Self::from_stream(stream)
}
}
fn resolve_peer(stream: &UnixStream) -> PeerIdentity {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
match rustix::net::sockopt::socket_peercred(stream) {
Ok(ucred) => PeerIdentity::Local {
uid: ucred.uid.as_raw(),
pid: ucred.pid.as_raw_nonzero().get(),
},
Err(_) => PeerIdentity::Anonymous,
}
}
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "android")))]
{
resolve_peer_bsd(stream)
}
#[cfg(not(unix))]
{
let _ = stream;
PeerIdentity::Anonymous
}
}
#[cfg(all(unix, not(target_os = "linux"), not(target_os = "android")))]
fn resolve_peer_bsd(stream: &UnixStream) -> PeerIdentity {
use std::os::fd::AsRawFd;
let fd = stream.as_raw_fd();
let mut euid: libc::uid_t = 0;
let mut egid: libc::gid_t = 0;
let rc = unsafe { libc::getpeereid(fd, &mut euid, &mut egid) };
if rc != 0 {
let errno = std::io::Error::last_os_error().raw_os_error();
return match errno {
Some(libc::ENOTCONN) | Some(libc::EINVAL) => super::mem::self_identity(),
_ => {
log::warn!(
"RPC unix peer-cred unavailable (getpeereid errno={errno:?}); \
reporting Anonymous — no peer ACL is possible"
);
PeerIdentity::Anonymous
}
};
}
PeerIdentity::Local {
uid: euid as u32,
pid: peer_pid(fd),
}
}
#[cfg(target_os = "macos")]
fn peer_pid(fd: std::os::fd::RawFd) -> i32 {
let mut pid: libc::pid_t = -1;
let mut len = std::mem::size_of::<libc::pid_t>() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
fd,
libc::SOL_LOCAL,
libc::LOCAL_PEERPID,
&mut pid as *mut libc::pid_t as *mut libc::c_void,
&mut len,
)
};
if rc == 0 {
pid
} else {
-1
}
}
#[cfg(all(
unix,
not(target_os = "linux"),
not(target_os = "macos"),
not(target_os = "android")
))]
fn peer_pid(_fd: std::os::fd::RawFd) -> i32 {
-1
}
impl RpcTransport for UnixTransport {
fn send_frame(&self, buf: &[u8]) -> RpcResult<()> {
let mut w = &self.stream;
write_frame(&mut w, buf)
}
fn recv_frame(&self) -> RpcResult<Vec<u8>> {
let mut r = &self.stream;
read_frame(&mut r)
}
fn send_raw(&self, buf: &[u8]) -> RpcResult<()> {
let mut w = &self.stream;
w.write_all(buf).map_err(RpcError::from)?;
w.flush().map_err(RpcError::from)?;
Ok(())
}
fn recv_raw(&self, buf: &mut [u8]) -> RpcResult<usize> {
let mut r = &self.stream;
match r.read(buf) {
Ok(n) => Ok(n),
Err(e) if super::is_timeout(&e) => Err(RpcError::Timeout),
Err(e) => Err(RpcError::from(e)),
}
}
fn send_raw_with_fds(&self, buf: &[u8], fds: &[std::os::fd::BorrowedFd<'_>]) -> RpcResult<()> {
use rustix::net::{SendAncillaryBuffer, SendAncillaryMessage, SendFlags};
use std::io::IoSlice;
use std::mem::MaybeUninit;
if fds.is_empty() {
return self.send_raw(buf);
}
if fds.len() > MAX_FDS_PER_FRAME {
return Err(RpcError::Protocol("too many fds in one RPC frame"));
}
if buf.len() > MAX_FRAME_LEN {
return Err(RpcError::FrameTooLarge {
declared: buf.len(),
max: MAX_FRAME_LEN,
});
}
let mut space = vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(fds.len()))];
let mut sent = 0;
while sent < buf.len() {
let mut anc = SendAncillaryBuffer::new(&mut space);
if sent == 0 {
let ok = anc.push(SendAncillaryMessage::ScmRights(fds));
debug_assert!(ok, "cmsg_space sized for exactly these fds");
}
let n = match rustix::net::sendmsg(
&self.stream,
&[IoSlice::new(&buf[sent..])],
&mut anc,
SendFlags::empty(),
) {
Ok(n) => n,
Err(rustix::io::Errno::INTR) => continue,
Err(e) => return Err(std::io::Error::from(e).into()),
};
if n == 0 {
return Err(RpcError::PeerClosed);
}
sent += n;
}
Ok(())
}
fn recv_raw_with_fds(&self, buf: &mut [u8]) -> RpcResult<(usize, Vec<std::os::fd::OwnedFd>)> {
use rustix::net::{RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags};
use std::io::IoSliceMut;
use std::mem::MaybeUninit;
let mut fds: Vec<std::os::fd::OwnedFd> = Vec::new();
let mut space =
vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_FDS_PER_FRAME))];
let mut anc = RecvAncillaryBuffer::new(&mut space);
let r = loop {
match rustix::net::recvmsg(
&self.stream,
&mut [IoSliceMut::new(buf)],
&mut anc,
RecvFlags::empty(),
) {
Ok(r) => break r,
Err(rustix::io::Errno::INTR) => continue,
Err(e) => {
let io_err = std::io::Error::from(e);
if super::is_timeout(&io_err) {
return Err(RpcError::Timeout);
}
return Err(io_err.into());
}
}
};
if r.flags.contains(ReturnFlags::CTRUNC) {
return Err(RpcError::Protocol(
"SCM_RIGHTS control message truncated (too many fds in one message)",
));
}
for msg in anc.drain() {
if let RecvAncillaryMessage::ScmRights(iter) = msg {
for fd in iter {
rustix::io::fcntl_setfd(&fd, rustix::io::FdFlags::CLOEXEC)
.map_err(std::io::Error::from)?;
fds.push(fd);
if fds.len() > MAX_FDS_PER_FRAME {
return Err(RpcError::Protocol("too many fds in one RPC frame"));
}
}
}
}
Ok((r.bytes, fds))
}
fn peer_identity(&self) -> PeerIdentity {
self.peer.clone()
}
fn describe(&self) -> &str {
&self.desc
}
fn set_read_timeout(&self, timeout: Option<std::time::Duration>) -> RpcResult<()> {
self.stream.set_read_timeout(timeout)?;
Ok(())
}
fn set_write_timeout(&self, timeout: Option<std::time::Duration>) -> RpcResult<()> {
self.stream.set_write_timeout(timeout)?;
Ok(())
}
fn send_frame_with_fds(
&self,
buf: &[u8],
fds: &[std::os::fd::BorrowedFd<'_>],
) -> RpcResult<()> {
use rustix::net::{SendAncillaryBuffer, SendAncillaryMessage, SendFlags};
use std::io::IoSlice;
use std::mem::MaybeUninit;
if fds.is_empty() {
return self.send_frame(buf);
}
if fds.len() > MAX_FDS_PER_FRAME {
return Err(RpcError::Protocol("too many fds in one RPC frame"));
}
if buf.len() > MAX_FRAME_LEN {
return Err(RpcError::FrameTooLarge {
declared: buf.len(),
max: MAX_FRAME_LEN,
});
}
let mut framed = Vec::with_capacity(4 + buf.len());
framed.extend_from_slice(&(buf.len() as u32).to_le_bytes());
framed.extend_from_slice(buf);
let mut space = vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(fds.len()))];
let mut sent = 0;
while sent < framed.len() {
let mut anc = SendAncillaryBuffer::new(&mut space);
if sent == 0 {
let ok = anc.push(SendAncillaryMessage::ScmRights(fds));
debug_assert!(ok, "cmsg_space sized for exactly these fds");
}
let n = match rustix::net::sendmsg(
&self.stream,
&[IoSlice::new(&framed[sent..])],
&mut anc,
SendFlags::empty(),
) {
Ok(n) => n,
Err(rustix::io::Errno::INTR) => continue,
Err(e) => return Err(std::io::Error::from(e).into()),
};
if n == 0 {
return Err(RpcError::PeerClosed);
}
sent += n;
}
Ok(())
}
fn recv_frame_with_fds(&self) -> RpcResult<(Vec<u8>, Vec<std::os::fd::OwnedFd>)> {
use rustix::net::{RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags};
use std::io::IoSliceMut;
use std::mem::MaybeUninit;
let mut leftover = self.fd_recv_buf.lock().expect("fd recv buf poisoned");
let mut fds: Vec<std::os::fd::OwnedFd> = Vec::new();
loop {
if leftover.len() >= 4 {
let len = u32::from_le_bytes(leftover[0..4].try_into().unwrap()) as usize;
if len > MAX_FRAME_LEN {
return Err(RpcError::FrameTooLarge {
declared: len,
max: MAX_FRAME_LEN,
});
}
if leftover.len() >= 4 + len {
let frame = leftover[4..4 + len].to_vec();
leftover.drain(0..4 + len);
return Ok((frame, fds));
}
}
let mut tmp = [0u8; 8192];
let mut space =
vec![MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_FDS_PER_FRAME))];
let mut anc = RecvAncillaryBuffer::new(&mut space);
let r = loop {
match rustix::net::recvmsg(
&self.stream,
&mut [IoSliceMut::new(&mut tmp)],
&mut anc,
RecvFlags::empty(),
) {
Ok(r) => break r,
Err(rustix::io::Errno::INTR) => continue,
Err(e) => {
let io_err = std::io::Error::from(e);
if super::is_timeout(&io_err) {
return Err(if leftover.is_empty() && fds.is_empty() {
RpcError::Timeout
} else {
RpcError::Truncated
});
}
return Err(io_err.into());
}
}
};
if r.flags.contains(ReturnFlags::CTRUNC) {
return Err(RpcError::Protocol(
"SCM_RIGHTS control message truncated (too many fds in one message)",
));
}
for msg in anc.drain() {
if let RecvAncillaryMessage::ScmRights(iter) = msg {
for fd in iter {
rustix::io::fcntl_setfd(&fd, rustix::io::FdFlags::CLOEXEC)
.map_err(std::io::Error::from)?;
fds.push(fd);
if fds.len() > MAX_FDS_PER_FRAME {
return Err(RpcError::Protocol("too many fds in one RPC frame"));
}
}
}
}
if r.bytes == 0 {
return Err(if leftover.is_empty() && fds.is_empty() {
RpcError::PeerClosed
} else {
RpcError::Truncated
});
}
leftover.extend_from_slice(&tmp[..r.bytes]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::RpcError;
use std::sync::Arc;
#[test]
fn unix_roundtrip_all_sizes() {
let (a, b) = UnixTransport::pair().expect("socketpair");
let a = Arc::new(a);
for size in [0usize, 1, 64 * 1024, 1 << 20, (1 << 20) + 1] {
let payload: Vec<u8> = (0..size).map(|i| (i % 251) as u8).collect();
let sender = {
let a = a.clone();
let p = payload.clone();
std::thread::spawn(move || a.send_frame(&p).unwrap())
};
assert_eq!(b.recv_frame().expect("recv"), payload, "size {size}");
sender.join().unwrap();
}
}
#[test]
fn unix_peer_identity_is_this_process_for_socketpair() {
let (a, _b) = UnixTransport::pair().expect("socketpair");
let id = a.peer_identity();
assert_eq!(
id,
PeerIdentity::Local {
uid: rustix::process::getuid().as_raw(),
pid: std::process::id() as i32,
},
"socketpair peer must be this process (got {id})"
);
assert!(a.describe().starts_with("unix:"));
}
#[test]
fn unix_peer_closed_on_drop() {
let (a, b) = UnixTransport::pair().expect("socketpair");
drop(b);
assert!(matches!(a.recv_frame(), Err(RpcError::PeerClosed)));
}
#[test]
fn unix_from_owned_fd_roundtrip() {
use rustix::net::{AddressFamily, SocketFlags, SocketType};
let (a, b) = rustix::net::socketpair(
AddressFamily::UNIX,
SocketType::STREAM,
SocketFlags::empty(),
None,
)
.expect("socketpair");
let client = UnixTransport::from_owned_fd(a).expect("adopt a");
let server = UnixTransport::from_owned_fd(b).expect("adopt b");
let payload = b"hello-accessor".to_vec();
let client = Arc::new(client);
let sender = {
let c = client.clone();
let p = payload.clone();
std::thread::spawn(move || c.send_frame(&p).unwrap())
};
assert_eq!(server.recv_frame().expect("recv"), payload);
sender.join().unwrap();
}
#[test]
fn unix_partial_header_then_close_is_truncated() {
let (a, b) = UnixTransport::pair().expect("socketpair");
{
use std::io::Write;
let mut s = &a.stream;
s.write_all(&[1u8, 0]).unwrap(); }
drop(a);
let r = b.recv_frame();
assert!(
matches!(r, Err(RpcError::Truncated)),
"expected Truncated (2-of-4 header consumed before EOF), got {r:?}"
);
}
}