Skip to main content

microsandbox_network/
ready.rs

1//! Startup handshake types for the supervisor ↔ msbnet readiness protocol.
2//!
3//! `msbnet` writes a single JSON line to stdout on successful bootstrap.
4//! The supervisor parses this line to obtain the resolved network parameters
5//! before spawning the VM.
6
7use serde::{Deserialize, Serialize};
8
9//--------------------------------------------------------------------------------------------------
10// Types
11//--------------------------------------------------------------------------------------------------
12
13/// JSON payload written to stdout by `msbnet` when ready.
14///
15/// Contains the resolved network parameters that the supervisor encodes
16/// as `MSB_NET*` environment variables for the VM.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct MsbnetReady {
19    /// PID of the msbnet process.
20    pub pid: u32,
21
22    /// Backend identifier (e.g. `"linux_tap"`, `"macos_vmnet"`).
23    pub backend: String,
24
25    /// Host-side interface name (e.g. `"msbtap42"`).
26    pub ifname: String,
27
28    /// Guest-side interface name (e.g. `"eth0"`).
29    pub guest_iface: String,
30
31    /// Guest MAC address (e.g. `"02:5a:7b:13:01:02"`).
32    pub mac: String,
33
34    /// MTU for the guest interface.
35    pub mtu: u16,
36
37    /// Resolved IPv4 network parameters.
38    pub ipv4: Option<MsbnetReadyIpv4>,
39
40    /// Resolved IPv6 network parameters.
41    pub ipv6: Option<MsbnetReadyIpv6>,
42
43    /// TLS interception readiness info. `None` when TLS is disabled.
44    #[serde(default)]
45    pub tls: Option<MsbnetReadyTls>,
46}
47
48/// TLS interception readiness info reported by `msbnet`.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct MsbnetReadyTls {
51    /// Whether TLS interception is active.
52    pub enabled: bool,
53
54    /// Local port the TLS proxy is listening on.
55    pub proxy_port: u16,
56
57    /// PEM-encoded CA certificate for guest trust store injection.
58    pub ca_pem: String,
59
60    /// Ports being intercepted.
61    pub intercepted_ports: Vec<u16>,
62}
63
64/// Resolved IPv4 parameters reported by `msbnet`.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct MsbnetReadyIpv4 {
67    /// Guest IPv4 address (e.g. `"100.96.1.2"`).
68    pub address: String,
69
70    /// Prefix length (e.g. `30`).
71    pub prefix_len: u8,
72
73    /// Gateway IPv4 address (e.g. `"100.96.1.1"`).
74    pub gateway: String,
75
76    /// DNS server addresses exposed to the guest.
77    pub dns: Vec<String>,
78}
79
80/// Resolved IPv6 parameters reported by `msbnet`.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct MsbnetReadyIpv6 {
83    /// Guest IPv6 address (e.g. `"fd42:6d73:62:2a::2"`).
84    pub address: String,
85
86    /// Prefix length (e.g. `64`).
87    pub prefix_len: u8,
88
89    /// Gateway IPv6 address (e.g. `"fd42:6d73:62:2a::1"`).
90    pub gateway: String,
91
92    /// DNS server addresses exposed to the guest.
93    pub dns: Vec<String>,
94}
95
96//--------------------------------------------------------------------------------------------------
97// Methods
98//--------------------------------------------------------------------------------------------------
99
100impl MsbnetReady {
101    /// Encode the resolved network parameters as `MSB_NET*` env var key-value pairs.
102    ///
103    /// Returns a list of `(key, value)` tuples suitable for injection into the
104    /// VM exec config.
105    pub fn to_env_vars(&self) -> Vec<(&'static str, String)> {
106        use std::fmt::Write;
107
108        let mut vars = Vec::with_capacity(3);
109
110        // MSB_NET=iface=eth0,mac=02:5a:7b:13:01:02,mtu=1500
111        let net = format!(
112            "iface={},mac={},mtu={}",
113            self.guest_iface, self.mac, self.mtu
114        );
115        vars.push((microsandbox_protocol::ENV_NET, net));
116
117        // MSB_NET_IPV4=addr=100.96.1.2/30,gw=100.96.1.1,dns=100.96.1.1
118        if let Some(ipv4) = &self.ipv4 {
119            let mut val = format!(
120                "addr={}/{},gw={}",
121                ipv4.address, ipv4.prefix_len, ipv4.gateway
122            );
123            // Only the first DNS server is emitted — the guest parser
124            // supports a single dns= entry per address family.
125            if let Some(dns) = ipv4.dns.first() {
126                let _ = write!(val, ",dns={dns}");
127            }
128            vars.push((microsandbox_protocol::ENV_NET_IPV4, val));
129        }
130
131        // MSB_NET_IPV6=addr=fd42:6d73:62:2a::2/64,gw=fd42:6d73:62:2a::1,dns=fd42:6d73:62:2a::1
132        if let Some(ipv6) = &self.ipv6 {
133            let mut val = format!(
134                "addr={}/{},gw={}",
135                ipv6.address, ipv6.prefix_len, ipv6.gateway
136            );
137            if let Some(dns) = ipv6.dns.first() {
138                let _ = write!(val, ",dns={dns}");
139            }
140            vars.push((microsandbox_protocol::ENV_NET_IPV6, val));
141        }
142
143        vars
144    }
145}
146
147//--------------------------------------------------------------------------------------------------
148// Tests
149//--------------------------------------------------------------------------------------------------
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_msbnet_ready_serde_roundtrip() {
157        let ready = MsbnetReady {
158            pid: 12345,
159            backend: "linux_tap".to_string(),
160            ifname: "msbtap42".to_string(),
161            guest_iface: "eth0".to_string(),
162            mac: "02:5a:7b:13:01:02".to_string(),
163            mtu: 1500,
164            ipv4: Some(MsbnetReadyIpv4 {
165                address: "100.96.1.2".to_string(),
166                prefix_len: 30,
167                gateway: "100.96.1.1".to_string(),
168                dns: vec!["100.96.1.1".to_string()],
169            }),
170            ipv6: Some(MsbnetReadyIpv6 {
171                address: "fd42:6d73:62:2a::2".to_string(),
172                prefix_len: 64,
173                gateway: "fd42:6d73:62:2a::1".to_string(),
174                dns: vec!["fd42:6d73:62:2a::1".to_string()],
175            }),
176            tls: None,
177        };
178
179        let json = serde_json::to_string(&ready).unwrap();
180        let decoded: MsbnetReady = serde_json::from_str(&json).unwrap();
181
182        assert_eq!(decoded.pid, 12345);
183        assert_eq!(decoded.backend, "linux_tap");
184        assert_eq!(decoded.ifname, "msbtap42");
185        assert_eq!(decoded.guest_iface, "eth0");
186        assert_eq!(decoded.mtu, 1500);
187        assert!(decoded.ipv4.is_some());
188        assert!(decoded.ipv6.is_some());
189    }
190
191    #[test]
192    fn test_msbnet_ready_to_env_vars_dual_stack() {
193        let ready = MsbnetReady {
194            pid: 1,
195            backend: "linux_tap".to_string(),
196            ifname: "msbtap42".to_string(),
197            guest_iface: "eth0".to_string(),
198            mac: "02:5a:7b:13:01:02".to_string(),
199            mtu: 1500,
200            ipv4: Some(MsbnetReadyIpv4 {
201                address: "100.96.1.2".to_string(),
202                prefix_len: 30,
203                gateway: "100.96.1.1".to_string(),
204                dns: vec!["100.96.1.1".to_string()],
205            }),
206            ipv6: Some(MsbnetReadyIpv6 {
207                address: "fd42:6d73:62:2a::2".to_string(),
208                prefix_len: 64,
209                gateway: "fd42:6d73:62:2a::1".to_string(),
210                dns: vec!["fd42:6d73:62:2a::1".to_string()],
211            }),
212            tls: None,
213        };
214
215        let vars = ready.to_env_vars();
216        assert_eq!(vars.len(), 3);
217        assert_eq!(vars[0].0, "MSB_NET");
218        assert_eq!(vars[0].1, "iface=eth0,mac=02:5a:7b:13:01:02,mtu=1500");
219        assert_eq!(vars[1].0, "MSB_NET_IPV4");
220        assert_eq!(vars[1].1, "addr=100.96.1.2/30,gw=100.96.1.1,dns=100.96.1.1");
221        assert_eq!(vars[2].0, "MSB_NET_IPV6");
222        assert_eq!(
223            vars[2].1,
224            "addr=fd42:6d73:62:2a::2/64,gw=fd42:6d73:62:2a::1,dns=fd42:6d73:62:2a::1"
225        );
226    }
227
228    #[test]
229    fn test_msbnet_ready_to_env_vars_ipv4_only() {
230        let ready = MsbnetReady {
231            pid: 1,
232            backend: "linux_tap".to_string(),
233            ifname: "msbtap0".to_string(),
234            guest_iface: "eth0".to_string(),
235            mac: "02:00:00:00:00:01".to_string(),
236            mtu: 1500,
237            ipv4: Some(MsbnetReadyIpv4 {
238                address: "100.96.0.2".to_string(),
239                prefix_len: 30,
240                gateway: "100.96.0.1".to_string(),
241                dns: vec![],
242            }),
243            ipv6: None,
244            tls: None,
245        };
246
247        let vars = ready.to_env_vars();
248        assert_eq!(vars.len(), 2);
249        assert_eq!(vars[1].0, "MSB_NET_IPV4");
250        assert_eq!(vars[1].1, "addr=100.96.0.2/30,gw=100.96.0.1");
251    }
252}