Skip to main content

arcbox_virtio_vsock/
host.rs

1//! Host-side vsock backend (Hypervisor.framework) — relays guest connections to Unix sockets.
2
3#![cfg(unix)]
4
5use std::collections::HashMap;
6use std::io::{Read, Write};
7use std::os::unix::net::UnixStream;
8
9use arcbox_virtio_core::error::{Result, VirtioError};
10
11use crate::addr::VsockAddr;
12use crate::backend::VsockBackend;
13
14/// Active forwarding channel between a guest vsock port and a host-side Unix
15/// socket.
16#[derive(Debug)]
17struct VsockChannel {
18    /// The connected Unix socket on the host side.
19    stream: UnixStream,
20}
21
22/// Host-side vsock backend for the HV (Hypervisor.framework) backend.
23///
24/// Accepts connections from the guest and forwards them to host-side Unix
25/// domain sockets. Each guest port is mapped to a Unix socket path on the host;
26/// when the guest opens a connection the backend connects to the corresponding
27/// socket and relays data bidirectionally.
28///
29/// This is the primary backend used for arcbox-agent RPC communication on
30/// macOS.
31pub struct HostVsockBackend {
32    /// Port to Unix socket path mappings.
33    port_map: HashMap<u32, String>,
34    /// Active forwarding channels keyed by (`src_port`, `dst_port`).
35    channels: HashMap<(u32, u32), VsockChannel>,
36}
37
38impl std::fmt::Debug for HostVsockBackend {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("HostVsockBackend")
41            .field("port_map", &self.port_map)
42            .field("channels_count", &self.channels.len())
43            .finish()
44    }
45}
46
47impl HostVsockBackend {
48    /// Creates a new host vsock backend with no port mappings.
49    #[must_use]
50    pub fn new() -> Self {
51        Self {
52            port_map: HashMap::new(),
53            channels: HashMap::new(),
54        }
55    }
56
57    /// Registers a mapping from guest vsock port to a host-side Unix socket
58    /// path.
59    ///
60    /// When the guest opens a connection to `port`, the backend will connect
61    /// to `socket_path` on the host and relay traffic.
62    pub fn add_port_mapping(&mut self, port: u32, socket_path: String) {
63        self.port_map.insert(port, socket_path);
64    }
65
66    /// Creates a backend with pre-configured port mappings.
67    #[must_use]
68    pub fn with_port_map(port_map: HashMap<u32, String>) -> Self {
69        Self {
70            port_map,
71            channels: HashMap::new(),
72        }
73    }
74}
75
76impl Default for HostVsockBackend {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl VsockBackend for HostVsockBackend {
83    fn on_connect(&mut self, addr: VsockAddr) -> Result<()> {
84        let socket_path = self.port_map.get(&addr.port).ok_or_else(|| {
85            VirtioError::InvalidOperation(format!("No port mapping for vsock port {}", addr.port))
86        })?;
87
88        let stream = UnixStream::connect(socket_path).map_err(|e| {
89            VirtioError::Io(format!(
90                "Failed to connect to Unix socket {}: {}",
91                socket_path, e
92            ))
93        })?;
94
95        stream
96            .set_nonblocking(true)
97            .map_err(|e| VirtioError::Io(format!("Failed to set nonblocking: {}", e)))?;
98
99        tracing::debug!(
100            "HostVsockBackend: connected port {} to {}",
101            addr.port,
102            socket_path
103        );
104
105        // Use (port, port) as channel key — the guest side always uses the same
106        // port for the connection.
107        self.channels
108            .insert((addr.port, addr.port), VsockChannel { stream });
109        Ok(())
110    }
111
112    fn on_send(&mut self, addr: VsockAddr, data: &[u8]) -> Result<usize> {
113        let channel = self
114            .channels
115            .get_mut(&(addr.port, addr.port))
116            .ok_or_else(|| {
117                VirtioError::InvalidOperation(format!("No channel for vsock port {}", addr.port))
118            })?;
119
120        channel
121            .stream
122            .write(data)
123            .map_err(|e| VirtioError::Io(format!("Failed to write to Unix socket: {}", e)))
124    }
125
126    fn on_recv(&mut self, addr: VsockAddr, buf: &mut [u8]) -> Result<usize> {
127        let channel = self
128            .channels
129            .get_mut(&(addr.port, addr.port))
130            .ok_or_else(|| {
131                VirtioError::InvalidOperation(format!("No channel for vsock port {}", addr.port))
132            })?;
133
134        match channel.stream.read(buf) {
135            Ok(n) => Ok(n),
136            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(0),
137            Err(e) => Err(VirtioError::Io(format!(
138                "Failed to read from Unix socket: {}",
139                e
140            ))),
141        }
142    }
143
144    fn on_close(&mut self, addr: VsockAddr) -> Result<()> {
145        self.channels.remove(&(addr.port, addr.port));
146        tracing::debug!("HostVsockBackend: closed port {}", addr.port);
147        Ok(())
148    }
149
150    fn has_pending_data(&self, addr: VsockAddr) -> bool {
151        // Existence of the channel entry stands in as a coarse signal —
152        // non-blocking peek is the alternative.
153        self.channels.contains_key(&(addr.port, addr.port))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_host_vsock_backend_new() {
163        let backend = HostVsockBackend::new();
164        assert!(backend.port_map.is_empty());
165        assert!(backend.channels.is_empty());
166    }
167
168    #[test]
169    fn test_host_vsock_backend_add_port_mapping() {
170        let mut backend = HostVsockBackend::new();
171        backend.add_port_mapping(1234, "/tmp/test.sock".to_string());
172        assert_eq!(backend.port_map.get(&1234).unwrap(), "/tmp/test.sock");
173    }
174
175    #[test]
176    fn test_host_vsock_backend_connect_no_mapping() {
177        let mut backend = HostVsockBackend::new();
178        let addr = VsockAddr::new(3, 9999);
179        let result = backend.on_connect(addr);
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_host_vsock_backend_with_unix_socket() {
185        use std::os::unix::net::UnixListener;
186
187        let tmpdir = tempfile::tempdir().unwrap();
188        let sock_path = tmpdir.path().join("test.sock");
189        let sock_path_str = sock_path.to_str().unwrap().to_string();
190
191        let _listener = UnixListener::bind(&sock_path).unwrap();
192
193        let mut backend = HostVsockBackend::new();
194        backend.add_port_mapping(5000, sock_path_str);
195
196        let addr = VsockAddr::new(3, 5000);
197        backend.on_connect(addr).unwrap();
198
199        let data = b"ping";
200        let sent = backend.on_send(addr, data).unwrap();
201        assert_eq!(sent, data.len());
202
203        backend.on_close(addr).unwrap();
204        assert!(!backend.has_pending_data(addr));
205    }
206}