Skip to main content

vm_proxy/
lib.rs

1pub mod config;
2mod device;
3mod dns;
4mod proxy;
5mod secrets;
6mod stack;
7mod stream;
8mod substitute;
9mod tls;
10
11pub use config::ProxyConfig;
12
13use std::collections::HashMap;
14use std::net::Ipv4Addr;
15use std::num::NonZeroUsize;
16use std::os::unix::io::RawFd;
17use std::sync::{Arc, RwLock};
18
19use lru::LruCache;
20use proxy::ProxyEngine;
21use stack::NetworkStack;
22use tls::CertificateAuthority;
23use tokio::sync::mpsc;
24use tracing::info;
25
26/// Cache of IPs that the proxy is allowed to connect to.
27/// Populated by DNS resolution of allowed domains. When the domain allowlist
28/// is active, TCP connections to IPs not in this cache are rejected, closing
29/// the bypass where a guest connects directly to a hardcoded IP.
30///
31/// Bounded via LRU so long-lived sandboxes do not accumulate stale IPs
32/// indefinitely. Recency is bumped on lookup, so IPs in active use stay warm.
33pub type AllowedIps = Arc<RwLock<LruCache<Ipv4Addr, ()>>>;
34
35/// Cap on pinned IPs. A single CDN-fronted domain can resolve to dozens of
36/// IPs, and configs can list many domains, so keep this generous.
37const ALLOWED_IPS_CAPACITY: usize = 1024;
38
39/// Handle to a running proxy. Shuts down on drop.
40pub struct ProxyHandle {
41    _stack_thread: std::thread::JoinHandle<()>,
42    _runtime_thread: std::thread::JoinHandle<()>,
43    /// Placeholder tokens generated for secrets. Key = env var name, Value = placeholder.
44    pub placeholders: HashMap<String, String>,
45    /// CA certificate in PEM format (for injecting into guest trust store).
46    pub ca_cert_pem: Vec<u8>,
47}
48
49/// Generate a unique placeholder token for a secret.
50fn generate_placeholder() -> String {
51    use std::sync::atomic::{AtomicU64, Ordering};
52    use std::time::{SystemTime, UNIX_EPOCH};
53    static COUNTER: AtomicU64 = AtomicU64::new(0);
54    let ts = SystemTime::now()
55        .duration_since(UNIX_EPOCH)
56        .unwrap()
57        .as_nanos() as u64;
58    let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
59    format!("hanzo_tok_{:016x}{:04x}", ts, seq)
60}
61
62/// Create a Unix datagram socketpair for VZFileHandleNetworkDeviceAttachment.
63/// Returns (vm_fd, host_fd). The vm_fd goes to VZ, host_fd goes to the proxy.
64pub fn create_socketpair() -> anyhow::Result<(RawFd, RawFd)> {
65    let mut fds = [0i32; 2];
66    let ret = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr()) };
67    if ret != 0 {
68        return Err(anyhow::anyhow!(
69            "socketpair failed: {}",
70            std::io::Error::last_os_error()
71        ));
72    }
73
74    let host_fd = fds[1];
75
76    // Apple recommends SO_RCVBUF >= 2x SO_SNDBUF for VZFileHandleNetworkDeviceAttachment
77    unsafe {
78        let sndbuf: libc::c_int = 1024 * 1024;
79        let rcvbuf: libc::c_int = 4 * 1024 * 1024;
80        libc::setsockopt(
81            host_fd,
82            libc::SOL_SOCKET,
83            libc::SO_SNDBUF,
84            &sndbuf as *const _ as _,
85            std::mem::size_of::<libc::c_int>() as _,
86        );
87        libc::setsockopt(
88            host_fd,
89            libc::SOL_SOCKET,
90            libc::SO_RCVBUF,
91            &rcvbuf as *const _ as _,
92            std::mem::size_of::<libc::c_int>() as _,
93        );
94    }
95
96    Ok((fds[0], fds[1]))
97}
98
99/// Start the proxy engine. Returns a handle that keeps it running.
100///
101/// - `host_fd`: the host end of the socketpair (raw L2 Ethernet frames)
102/// - `config`: proxy configuration (secrets, network rules)
103pub fn start(host_fd: RawFd, config: ProxyConfig) -> anyhow::Result<ProxyHandle> {
104    // Install rustls crypto provider (process-wide, idempotent)
105    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
106
107    let ca = CertificateAuthority::new()?;
108    let ca_cert_pem = ca.ca_cert_pem();
109
110    // Generate placeholder tokens for each secret
111    let mut placeholders = HashMap::new();
112    for name in config.secrets.keys() {
113        placeholders.insert(name.clone(), generate_placeholder());
114    }
115
116    let allowed_ips: AllowedIps = Arc::new(RwLock::new(LruCache::new(
117        NonZeroUsize::new(ALLOWED_IPS_CAPACITY).expect("non-zero capacity"),
118    )));
119
120    let (event_tx, event_rx) = mpsc::unbounded_channel();
121    let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
122
123    let stack_thread = std::thread::Builder::new()
124        .name("vm-netstack".into())
125        .spawn(move || {
126            let mut stack = NetworkStack::new(host_fd, event_tx, cmd_rx);
127            stack.run();
128        })?;
129
130    let proxy_config = config;
131    let proxy_placeholders = placeholders.clone();
132    let proxy_allowed_ips = allowed_ips.clone();
133    let runtime_thread = std::thread::Builder::new()
134        .name("vm-proxy".into())
135        .spawn(move || {
136            let rt = tokio::runtime::Builder::new_multi_thread()
137                .worker_threads(2)
138                .enable_all()
139                .build()
140                .expect("failed to create tokio runtime for proxy");
141
142            rt.block_on(async move {
143                let mut engine = ProxyEngine::new(
144                    proxy_config,
145                    event_rx,
146                    cmd_tx,
147                    ca,
148                    proxy_placeholders,
149                    proxy_allowed_ips,
150                );
151                engine.run().await;
152            });
153        })?;
154
155    info!("proxy started");
156
157    Ok(ProxyHandle {
158        _stack_thread: stack_thread,
159        _runtime_thread: runtime_thread,
160        placeholders,
161        ca_cert_pem,
162    })
163}