Skip to main content

microsandbox_vsock/
dgram.rs

1use std::io;
2use std::os::fd::{AsRawFd, OwnedFd, RawFd};
3use std::os::unix::net::UnixDatagram as StdUnixDatagram;
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use msb_krun::backends::vsock::{
8    VsockDatagramBackend, VsockDatagramPeer, VsockDatagramPortBackend, VsockDatagramRead,
9    VsockNotifier,
10};
11use nix::sys::socket::{MsgFlags, recv, send};
12use tempfile::TempDir;
13
14use crate::common::{DEFAULT_MAX_ACTIVE_PEERS, PeerLease, PeerLimit, validate_socket_path};
15
16//--------------------------------------------------------------------------------------------------
17// Types
18//--------------------------------------------------------------------------------------------------
19
20/// Factory that connects guest datagram peers to one host Unix datagram service.
21pub struct UnixDatagramPortBackend {
22    path: PathBuf,
23    peer_dir: TempDir,
24    next_peer: AtomicU64,
25    peers: PeerLimit,
26}
27
28struct UnixDatagramBackend {
29    fd: OwnedFd,
30    bound_path: PathBuf,
31    _lease: PeerLease,
32}
33
34//--------------------------------------------------------------------------------------------------
35// Methods
36//--------------------------------------------------------------------------------------------------
37
38impl UnixDatagramPortBackend {
39    /// Create a route to an existing host `SOCK_DGRAM` Unix socket.
40    pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
41        Self::with_max_active_peers(path, DEFAULT_MAX_ACTIVE_PEERS)
42    }
43
44    /// Create a route with an explicit cap on active guest source peers.
45    pub fn with_max_active_peers(path: impl AsRef<Path>, max: usize) -> io::Result<Self> {
46        let path = path.as_ref().to_path_buf();
47        validate_socket_path(&path)?;
48        if max == 0 {
49            return Err(io::Error::new(
50                io::ErrorKind::InvalidInput,
51                "vsock datagram peer limit must be non-zero",
52            ));
53        }
54
55        // A short private directory keeps reply addresses within macOS's
56        // smaller sockaddr_un path limit. TempDir handles normal cleanup.
57        // Use the short, stable Unix temporary root rather than macOS's long
58        // per-user TMPDIR. `sockaddr_un` is only 104 bytes there and nix may
59        // otherwise return a truncated peer address that cannot be replied to.
60        let peer_dir = tempfile::Builder::new()
61            .prefix("msb-vsock-")
62            .tempdir_in("/tmp")?;
63        Ok(Self {
64            path,
65            peer_dir,
66            next_peer: AtomicU64::new(1),
67            peers: PeerLimit::new(max),
68        })
69    }
70
71    fn next_peer_path(&self) -> PathBuf {
72        let id = self.next_peer.fetch_add(1, Ordering::Relaxed);
73        self.peer_dir.path().join(format!("{id:x}.sock"))
74    }
75}
76
77//--------------------------------------------------------------------------------------------------
78// Trait Implementations
79//--------------------------------------------------------------------------------------------------
80
81impl VsockDatagramPortBackend for UnixDatagramPortBackend {
82    fn open_peer(
83        &self,
84        _peer: VsockDatagramPeer,
85        _notifier: VsockNotifier,
86    ) -> io::Result<Box<dyn VsockDatagramBackend>> {
87        let lease = self.peers.acquire()?;
88        let bound_path = self.next_peer_path();
89        // std includes the terminating NUL in sockaddr_un on BSD hosts. nix's
90        // shorter bind length makes macOS report a one-byte-truncated reply
91        // pathname to the receiving service.
92        let socket = StdUnixDatagram::bind(&bound_path)?;
93        if let Err(err) = socket.set_nonblocking(true) {
94            let _ = std::fs::remove_file(&bound_path);
95            return Err(err);
96        }
97        if let Err(err) = socket.connect(&self.path) {
98            let _ = std::fs::remove_file(&bound_path);
99            return Err(err);
100        }
101        let fd = socket.into();
102
103        Ok(Box::new(UnixDatagramBackend {
104            fd,
105            bound_path,
106            _lease: lease,
107        }))
108    }
109}
110
111impl VsockDatagramBackend for UnixDatagramBackend {
112    fn send(&self, payload: &[u8]) -> io::Result<()> {
113        let written =
114            send(self.fd.as_raw_fd(), payload, MsgFlags::empty()).map_err(io::Error::from)?;
115        if written != payload.len() {
116            return Err(io::Error::new(
117                io::ErrorKind::WriteZero,
118                "Unix datagram send did not consume the complete message",
119            ));
120        }
121        Ok(())
122    }
123
124    fn receive(&self, buf: &mut [u8]) -> io::Result<VsockDatagramRead> {
125        let received = recv(
126            self.fd.as_raw_fd(),
127            buf,
128            MsgFlags::MSG_DONTWAIT | MsgFlags::MSG_TRUNC,
129        )
130        .map_err(io::Error::from)?;
131        Ok(VsockDatagramRead {
132            len: received.min(buf.len()),
133            truncated: received > buf.len(),
134        })
135    }
136
137    fn pollable(&self) -> Option<RawFd> {
138        Some(self.fd.as_raw_fd())
139    }
140}
141
142impl Drop for UnixDatagramBackend {
143    fn drop(&mut self) {
144        if let Err(err) = std::fs::remove_file(&self.bound_path)
145            && err.kind() != io::ErrorKind::NotFound
146        {
147            tracing::debug!(
148                path = %self.bound_path.display(),
149                %err,
150                "failed to remove vsock datagram peer socket"
151            );
152        }
153    }
154}
155
156//--------------------------------------------------------------------------------------------------
157// Tests
158//--------------------------------------------------------------------------------------------------
159
160#[cfg(test)]
161mod tests {
162    use std::os::unix::net::UnixDatagram;
163
164    use super::*;
165
166    #[test]
167    fn datagram_backend_preserves_messages_and_reply_address() {
168        let dir = tempfile::tempdir().unwrap();
169        let path = dir.path().join("service.sock");
170        let host = UnixDatagram::bind(&path).unwrap();
171        host.set_read_timeout(Some(std::time::Duration::from_secs(1)))
172            .unwrap();
173
174        let service = UnixDatagramPortBackend::new(&path).unwrap();
175        let endpoint = service
176            .open_peer(
177                VsockDatagramPeer {
178                    guest_cid: 3,
179                    guest_port: 4000,
180                    host_port: 5000,
181                },
182                VsockNotifier::new().unwrap(),
183            )
184            .unwrap();
185
186        endpoint.send(b"guest-event").unwrap();
187        let mut request = [0; 32];
188        let (len, peer) = host.recv_from(&mut request).unwrap();
189        assert_eq!(&request[..len], b"guest-event");
190        let reply_path = peer.as_pathname().unwrap();
191        assert!(
192            reply_path.exists(),
193            "reply path missing: {}",
194            reply_path.display()
195        );
196        host.send_to(b"host-event", reply_path).unwrap();
197
198        let mut response = [0; 32];
199        let mut read = None;
200        for _ in 0..100 {
201            match endpoint.receive(&mut response) {
202                Ok(received) => {
203                    read = Some(received);
204                    break;
205                }
206                Err(err) if err.kind() == io::ErrorKind::WouldBlock => std::thread::yield_now(),
207                result => panic!("unexpected datagram receive result: {result:?}"),
208            }
209        }
210        let read = read.expect("reply datagram should become readable");
211        assert_eq!(&response[..read.len], b"host-event");
212        assert!(!read.truncated);
213    }
214}