Skip to main content

microsandbox_network/
network.rs

1//! `SmoltcpNetwork` — orchestration type that ties [`NetworkConfig`] to the
2//! smoltcp engine.
3//!
4//! This is the networking analog to `PassthroughFs`/`MemFs` on the filesystem side — the single
5//! type the runtime creates from config, wires into the VM builder, and starts
6//! the networking stack.
7
8use std::net::{Ipv4Addr, Ipv6Addr};
9use std::sync::Arc;
10use std::thread::JoinHandle;
11
12use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
13use msb_krun::backends::net::NetBackend;
14
15use crate::backend::SmoltcpBackend;
16use crate::config::NetworkConfig;
17use crate::shared::{DEFAULT_QUEUE_CAPACITY, SharedState};
18use crate::stack::{self, GatewayIps, PollLoopConfig};
19use crate::tls::state::TlsState;
20
21//--------------------------------------------------------------------------------------------------
22// Constants
23//--------------------------------------------------------------------------------------------------
24
25/// Maximum sandbox slot value. Limited by MAC/IPv6 encoding (16 bits = 65535).
26/// The IPv4 pool (100.96.0.0/11 with /30 blocks) supports up to 524287 slots,
27/// but MAC and IPv6 derivation only encode the low 16 bits, so 65535 is the
28/// effective maximum.
29const MAX_SLOT: u64 = u16::MAX as u64;
30
31//--------------------------------------------------------------------------------------------------
32// Types
33//--------------------------------------------------------------------------------------------------
34
35/// The networking engine. Created from [`NetworkConfig`] by the runtime.
36///
37/// Owns the smoltcp poll thread and provides:
38/// - [`take_backend()`](Self::take_backend) — the `NetBackend` for `VmBuilder::net()`
39/// - [`guest_env_vars()`](Self::guest_env_vars) — `MSB_NET*` env vars for the guest
40/// - [`ca_cert_pem()`](Self::ca_cert_pem) — CA certificate for TLS interception
41pub struct SmoltcpNetwork {
42    config: NetworkConfig,
43    shared: Arc<SharedState>,
44    backend: Option<SmoltcpBackend>,
45    poll_handle: Option<JoinHandle<()>>,
46
47    // Resolved from config + slot.
48    guest_mac: [u8; 6],
49    gateway_mac: [u8; 6],
50    mtu: u16,
51    guest_ipv4: Ipv4Addr,
52    gateway_ipv4: Ipv4Addr,
53    guest_ipv6: Ipv6Addr,
54    gateway_ipv6: Ipv6Addr,
55
56    // TLS state (if enabled). Created in new(), used for ca_cert_pem().
57    tls_state: Option<Arc<TlsState>>,
58}
59
60/// Handle for installing host-side termination behavior into the network stack.
61#[derive(Clone)]
62pub struct TerminationHandle {
63    shared: Arc<SharedState>,
64}
65
66/// Read-only view of aggregate network byte counters.
67#[derive(Clone)]
68pub struct MetricsHandle {
69    shared: Arc<SharedState>,
70}
71
72//--------------------------------------------------------------------------------------------------
73// Methods
74//--------------------------------------------------------------------------------------------------
75
76impl SmoltcpNetwork {
77    /// Create from user config + sandbox slot (for IP/MAC derivation).
78    ///
79    /// # Panics
80    ///
81    /// Panics if `slot` exceeds the address pool capacity (65535 for MAC/IPv6,
82    /// 524287 for IPv4).
83    pub fn new(config: NetworkConfig, slot: u64) -> Self {
84        assert!(
85            slot <= MAX_SLOT,
86            "sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
87        );
88
89        let guest_mac = config
90            .interface
91            .mac
92            .unwrap_or_else(|| derive_guest_mac(slot));
93        let gateway_mac = derive_gateway_mac(slot);
94        let mtu = config.interface.mtu.unwrap_or(1500);
95        let guest_ipv4 = config
96            .interface
97            .ipv4_address
98            .unwrap_or_else(|| derive_guest_ipv4(slot));
99        let gateway_ipv4 = gateway_from_guest_ipv4(guest_ipv4);
100        let guest_ipv6 = config
101            .interface
102            .ipv6_address
103            .unwrap_or_else(|| derive_guest_ipv6(slot));
104        let gateway_ipv6 = gateway_from_guest_ipv6(guest_ipv6);
105
106        let queue_capacity = config
107            .max_connections
108            .unwrap_or(DEFAULT_QUEUE_CAPACITY)
109            .max(DEFAULT_QUEUE_CAPACITY);
110        let shared = Arc::new(SharedState::new(queue_capacity));
111        let backend = SmoltcpBackend::new(shared.clone());
112
113        let tls_state = if config.tls.enabled {
114            Some(Arc::new(TlsState::new(
115                config.tls.clone(),
116                config.secrets.clone(),
117            )))
118        } else {
119            None
120        };
121
122        Self {
123            config,
124            shared,
125            backend: Some(backend),
126            poll_handle: None,
127            guest_mac,
128            gateway_mac,
129            mtu,
130            guest_ipv4,
131            gateway_ipv4,
132            guest_ipv6,
133            gateway_ipv6,
134            tls_state,
135        }
136    }
137
138    /// Get the gateway IPs for virtio-net configuration and domain-based policy rules.
139    fn gateway_ips(&self) -> GatewayIps {
140        GatewayIps {
141            ipv4: self.gateway_ipv4,
142            ipv6: self.gateway_ipv6,
143        }
144    }
145
146    /// Start the smoltcp poll thread.
147    ///
148    /// Must be called before VM boot. Requires a tokio runtime handle for
149    /// spawning proxy tasks, DNS resolution, and published port listeners.
150    pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
151        let shared = self.shared.clone();
152        let poll_config = PollLoopConfig {
153            gateway_mac: self.gateway_mac,
154            guest_mac: self.guest_mac,
155            gateway: self.gateway_ips(),
156            guest_ipv4: self.guest_ipv4,
157            mtu: self.mtu as usize,
158        };
159        let network_policy = self.config.policy.clone();
160        let dns_config = self.config.dns.clone();
161        let tls_state = self.tls_state.clone();
162        let published_ports = self.config.ports.clone();
163        let max_connections = self.config.max_connections;
164
165        self.poll_handle = Some(
166            std::thread::Builder::new()
167                .name("smoltcp-poll".into())
168                .spawn(move || {
169                    stack::smoltcp_poll_loop(
170                        shared,
171                        poll_config,
172                        network_policy,
173                        dns_config,
174                        tls_state,
175                        published_ports,
176                        max_connections,
177                        tokio_handle,
178                    );
179                })
180                .expect("failed to spawn smoltcp poll thread"),
181        );
182    }
183
184    /// Take the `NetBackend` for `VmBuilder::net()`. One-shot.
185    pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
186        Box::new(self.backend.take().expect("backend already taken"))
187    }
188
189    /// Guest MAC address for `VmBuilder::net().mac()`.
190    pub fn guest_mac(&self) -> [u8; 6] {
191        self.guest_mac
192    }
193
194    /// Generate `MSB_NET*` environment variables for the guest.
195    ///
196    /// The guest init (`agentd`) reads these to configure the network
197    /// interface via ioctls + netlink.
198    pub fn guest_env_vars(&self) -> Vec<(String, String)> {
199        let mut vars = vec![
200            (
201                ENV_NET.into(),
202                format!(
203                    "iface=eth0,mac={},mtu={}",
204                    format_mac(self.guest_mac),
205                    self.mtu,
206                ),
207            ),
208            (
209                ENV_NET_IPV4.into(),
210                format!(
211                    "addr={}/30,gw={},dns={}",
212                    self.guest_ipv4, self.gateway_ipv4, self.gateway_ipv4,
213                ),
214            ),
215            (
216                ENV_NET_IPV6.into(),
217                format!(
218                    "addr={}/64,gw={},dns={}",
219                    self.guest_ipv6, self.gateway_ipv6, self.gateway_ipv6,
220                ),
221            ),
222            (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
223        ];
224
225        // Auto-expose secret placeholders as environment variables.
226        for secret in &self.config.secrets.secrets {
227            vars.push((secret.env_var.clone(), secret.placeholder.clone()));
228        }
229
230        vars
231    }
232
233    /// CA certificate PEM bytes if TLS interception is enabled.
234    ///
235    /// Write to the runtime mount before VM boot so the guest can trust it.
236    pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
237        self.tls_state.as_ref().map(|s| s.ca_cert_pem())
238    }
239
240    /// Host-trusted CA bundle to ship into the guest, if
241    /// [`NetworkConfig::trust_host_cas`] is enabled.
242    ///
243    /// Returned PEM may concatenate CAs that the Mozilla root bundle in
244    /// the guest already trusts; duplicates are harmless and saved the
245    /// cost of computing a delta. Returns `None` when the host store is
246    /// empty or the feature is disabled.
247    pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
248        if !self.config.trust_host_cas {
249            return None;
250        }
251        crate::tls::host_cas::collect_host_cas()
252    }
253
254    /// Create a handle for wiring runtime termination into the network stack.
255    pub fn termination_handle(&self) -> TerminationHandle {
256        TerminationHandle {
257            shared: self.shared.clone(),
258        }
259    }
260
261    /// Create a handle for reading aggregate network byte counters.
262    pub fn metrics_handle(&self) -> MetricsHandle {
263        MetricsHandle {
264            shared: self.shared.clone(),
265        }
266    }
267}
268
269impl TerminationHandle {
270    /// Install the termination hook.
271    pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
272        self.shared.set_termination_hook(hook);
273    }
274}
275
276impl MetricsHandle {
277    /// Total guest -> runtime bytes observed at the virtio-net boundary.
278    pub fn tx_bytes(&self) -> u64 {
279        self.shared.tx_bytes()
280    }
281
282    /// Total runtime -> guest bytes observed at the virtio-net boundary.
283    pub fn rx_bytes(&self) -> u64 {
284        self.shared.rx_bytes()
285    }
286}
287
288//--------------------------------------------------------------------------------------------------
289// Functions
290//--------------------------------------------------------------------------------------------------
291
292/// Derive a guest MAC address from the sandbox slot.
293///
294/// Format: `02:ms:bx:SS:SS:02` where SS:SS encodes the slot.
295fn derive_guest_mac(slot: u64) -> [u8; 6] {
296    let s = slot.to_be_bytes();
297    [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
298}
299
300/// Derive a gateway MAC address from the sandbox slot.
301///
302/// Format: `02:ms:bx:SS:SS:01`.
303fn derive_gateway_mac(slot: u64) -> [u8; 6] {
304    let s = slot.to_be_bytes();
305    [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
306}
307
308/// Derive a guest IPv4 address from the sandbox slot.
309///
310/// Pool: `100.96.0.0/11`. Each slot gets a `/30` block (4 IPs).
311/// Guest is at offset +2 in the block.
312fn derive_guest_ipv4(slot: u64) -> Ipv4Addr {
313    let base: u32 = u32::from(Ipv4Addr::new(100, 96, 0, 0));
314    let offset = (slot as u32) * 4 + 2; // +2 = guest within /30
315    Ipv4Addr::from(base + offset)
316}
317
318/// Gateway IPv4 from guest IPv4: guest - 1 (offset +1 in the /30 block).
319fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
320    Ipv4Addr::from(u32::from(guest) - 1)
321}
322
323/// Derive a guest IPv6 address from the sandbox slot.
324///
325/// Pool: `fd42:6d73:62::/48`. Each slot gets a `/64` prefix.
326/// Guest is `::2` in its prefix.
327fn derive_guest_ipv6(slot: u64) -> Ipv6Addr {
328    Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, slot as u16, 0, 0, 0, 2)
329}
330
331/// Gateway IPv6 from guest IPv6: `::1` in the same prefix.
332fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
333    let segs = guest.segments();
334    Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
335}
336
337/// Format a MAC address as `xx:xx:xx:xx:xx:xx`.
338fn format_mac(mac: [u8; 6]) -> String {
339    format!(
340        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
341        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
342    )
343}
344
345//--------------------------------------------------------------------------------------------------
346// Tests
347//--------------------------------------------------------------------------------------------------
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn derive_addresses_slot_0() {
355        assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
356        assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
357        assert_eq!(derive_guest_ipv4(0), Ipv4Addr::new(100, 96, 0, 2));
358        assert_eq!(
359            gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 2)),
360            Ipv4Addr::new(100, 96, 0, 1)
361        );
362    }
363
364    #[test]
365    fn derive_addresses_slot_1() {
366        assert_eq!(derive_guest_ipv4(1), Ipv4Addr::new(100, 96, 0, 6));
367        assert_eq!(
368            gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 6)),
369            Ipv4Addr::new(100, 96, 0, 5)
370        );
371    }
372
373    #[test]
374    fn derive_ipv6_slot_0() {
375        assert_eq!(
376            derive_guest_ipv6(0),
377            "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
378        );
379        assert_eq!(
380            gateway_from_guest_ipv6(derive_guest_ipv6(0)),
381            "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
382        );
383    }
384
385    #[test]
386    fn format_mac_address() {
387        assert_eq!(
388            format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
389            "02:6d:73:00:00:01"
390        );
391    }
392
393    #[test]
394    fn guest_env_vars_format() {
395        let config = NetworkConfig::default();
396        let net = SmoltcpNetwork::new(config, 0);
397        let vars = net.guest_env_vars();
398
399        assert_eq!(vars.len(), 4);
400        assert_eq!(vars[0].0, ENV_NET);
401        assert!(vars[0].1.contains("iface=eth0"));
402        assert_eq!(vars[1].0, ENV_NET_IPV4);
403        assert!(vars[1].1.contains("/30"));
404        assert_eq!(vars[2].0, ENV_NET_IPV6);
405        assert!(vars[2].1.contains("/64"));
406        assert_eq!(vars[3].0, ENV_HOST_ALIAS);
407        assert_eq!(vars[3].1, crate::HOST_ALIAS);
408    }
409}