Skip to main content

shuru_proxy/
lib.rs

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