Skip to main content

microsandbox_vsock/
stream.rs

1use std::io;
2use std::os::fd::{AsRawFd, OwnedFd, RawFd};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use msb_krun::backends::vsock::{
7    VsockConnectRequest, VsockConnectState, VsockNotifier, VsockPortBackend, VsockShutdown,
8    VsockStreamBackend,
9};
10use nix::errno::Errno;
11use nix::sys::socket::{
12    MsgFlags, Shutdown, SockType, UnixAddr, connect, getsockopt, recv, send, shutdown, sockopt,
13};
14
15use crate::common::{
16    DEFAULT_MAX_ACTIVE_PEERS, PeerLease, PeerLimit, nonblocking_unix_socket, validate_socket_path,
17};
18
19//--------------------------------------------------------------------------------------------------
20// Types
21//--------------------------------------------------------------------------------------------------
22
23/// Factory that connects guest streams to one existing host Unix socket.
24pub struct UnixStreamPortBackend {
25    path: PathBuf,
26    peers: PeerLimit,
27}
28
29struct UnixStreamBackend {
30    fd: OwnedFd,
31    connected: AtomicBool,
32    defer_connect_check: AtomicBool,
33    _lease: PeerLease,
34}
35
36//--------------------------------------------------------------------------------------------------
37// Methods
38//--------------------------------------------------------------------------------------------------
39
40impl UnixStreamPortBackend {
41    /// Create a route to an existing host `SOCK_STREAM` Unix socket.
42    pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
43        Self::with_max_active_peers(path, DEFAULT_MAX_ACTIVE_PEERS)
44    }
45
46    /// Create a route with an explicit cap on active guest connections.
47    pub fn with_max_active_peers(path: impl AsRef<Path>, max: usize) -> io::Result<Self> {
48        let path = path.as_ref().to_path_buf();
49        validate_socket_path(&path)?;
50        if max == 0 {
51            return Err(io::Error::new(
52                io::ErrorKind::InvalidInput,
53                "vsock stream peer limit must be non-zero",
54            ));
55        }
56        Ok(Self {
57            path,
58            peers: PeerLimit::new(max),
59        })
60    }
61}
62
63//--------------------------------------------------------------------------------------------------
64// Trait Implementations
65//--------------------------------------------------------------------------------------------------
66
67impl VsockPortBackend for UnixStreamPortBackend {
68    fn connect(
69        &self,
70        _request: VsockConnectRequest,
71        _notifier: VsockNotifier,
72    ) -> io::Result<Box<dyn VsockStreamBackend>> {
73        let lease = self.peers.acquire()?;
74        let fd = nonblocking_unix_socket(SockType::Stream)?;
75        let address = UnixAddr::new(&self.path)
76            .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
77
78        let (connected, pending) = match connect(fd.as_raw_fd(), &address) {
79            Ok(()) => (true, false),
80            // Linux reports EAGAIN for an in-progress nonblocking AF_UNIX
81            // connect, while BSD hosts normally use EINPROGRESS.
82            Err(Errno::EINPROGRESS | Errno::EAGAIN) => (false, true),
83            Err(err) => return Err(io::Error::from(err)),
84        };
85
86        Ok(Box::new(UnixStreamBackend {
87            fd,
88            connected: AtomicBool::new(connected),
89            // libkrun queries once immediately after `connect`. Defer SO_ERROR
90            // until the next writable event so zero cannot be mistaken for a
91            // completed nonblocking connection.
92            defer_connect_check: AtomicBool::new(pending),
93            _lease: lease,
94        }))
95    }
96}
97
98impl VsockStreamBackend for UnixStreamBackend {
99    fn connect_state(&self) -> io::Result<VsockConnectState> {
100        if self.connected.load(Ordering::Acquire) {
101            return Ok(VsockConnectState::Connected);
102        }
103        if self.defer_connect_check.swap(false, Ordering::AcqRel) {
104            return Ok(VsockConnectState::Connecting);
105        }
106
107        let error = getsockopt(&self.fd, sockopt::SocketError).map_err(io::Error::from)?;
108        if error != 0 {
109            return Err(io::Error::from_raw_os_error(error));
110        }
111        self.connected.store(true, Ordering::Release);
112        Ok(VsockConnectState::Connected)
113    }
114
115    fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
116        recv(self.fd.as_raw_fd(), buf, MsgFlags::MSG_DONTWAIT).map_err(io::Error::from)
117    }
118
119    fn write(&self, buf: &[u8]) -> io::Result<usize> {
120        #[cfg(target_os = "linux")]
121        let flags = MsgFlags::MSG_NOSIGNAL;
122        #[cfg(not(target_os = "linux"))]
123        let flags = MsgFlags::empty();
124        send(self.fd.as_raw_fd(), buf, flags).map_err(io::Error::from)
125    }
126
127    fn shutdown(&self, how: VsockShutdown) -> io::Result<()> {
128        let how = match how {
129            VsockShutdown::Read => Shutdown::Read,
130            VsockShutdown::Write => Shutdown::Write,
131            VsockShutdown::Both => Shutdown::Both,
132        };
133        shutdown(self.fd.as_raw_fd(), how).map_err(io::Error::from)
134    }
135
136    fn pollable(&self) -> Option<RawFd> {
137        Some(self.fd.as_raw_fd())
138    }
139}
140
141//--------------------------------------------------------------------------------------------------
142// Tests
143//--------------------------------------------------------------------------------------------------
144
145#[cfg(test)]
146mod tests {
147    use std::io::{Read, Write};
148    use std::os::unix::net::UnixListener;
149
150    use msb_krun::backends::vsock::{VsockConnectRequest, VsockPortBackend};
151
152    use super::*;
153
154    #[test]
155    fn stream_backend_connects_and_moves_bytes_in_both_directions() {
156        let dir = tempfile::tempdir().unwrap();
157        let path = dir.path().join("service.sock");
158        let listener = UnixListener::bind(&path).unwrap();
159        let service = UnixStreamPortBackend::new(&path).unwrap();
160        let endpoint = service
161            .connect(
162                VsockConnectRequest {
163                    guest_cid: 3,
164                    guest_port: 4000,
165                    host_port: 5000,
166                },
167                VsockNotifier::new().unwrap(),
168            )
169            .unwrap();
170        let (mut host, _) = listener.accept().unwrap();
171
172        for _ in 0..100 {
173            if endpoint.connect_state().unwrap() == VsockConnectState::Connected {
174                break;
175            }
176            std::thread::yield_now();
177        }
178        assert_eq!(
179            endpoint.connect_state().unwrap(),
180            VsockConnectState::Connected
181        );
182
183        endpoint.write(b"guest").unwrap();
184        let mut request = [0; 5];
185        host.read_exact(&mut request).unwrap();
186        assert_eq!(&request, b"guest");
187
188        host.write_all(b"host").unwrap();
189        let mut response = [0; 4];
190        for _ in 0..100 {
191            match endpoint.read(&mut response) {
192                Ok(4) => break,
193                Err(err) if err.kind() == io::ErrorKind::WouldBlock => std::thread::yield_now(),
194                result => panic!("unexpected stream read result: {result:?}"),
195            }
196        }
197        assert_eq!(&response, b"host");
198    }
199}