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