arcbox_vm_proto/boot.rs
1//! Boot protocol types shared between the host-side sandbox orchestrator
2//! and the guest-side `vm-agent`.
3//!
4//! These types define the contract passed through kernel boot parameters.
5//! Both sides import from this module so the encoding is defined once.
6
7use std::fmt;
8use std::net::Ipv4Addr;
9use std::str::FromStr;
10
11/// Parsed representation of the Linux kernel `ip=` boot parameter.
12///
13/// Format: `ip=<client>::<gateway>:<netmask>::eth0:off`
14///
15/// Constructed by the sandbox manager
16/// (`arcbox_computer_runtime::ComputerManager`) when building boot args, and
17/// parsed by `vm-agent` to derive the DNS nameserver from the gateway.
18///
19/// [`fmt::Display`] and [`FromStr`] round-trip through the same format so the
20/// encoding is defined exactly once.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct KernelIpParam {
23 /// Guest IP address (the `<client>` field).
24 pub client: Ipv4Addr,
25 /// Gateway IP (also serves as the guest DNS nameserver).
26 pub gateway: Ipv4Addr,
27 /// Subnet mask derived from the network prefix length.
28 pub netmask: Ipv4Addr,
29}
30
31impl fmt::Display for KernelIpParam {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(
34 f,
35 "ip={}::{}:{}::eth0:off",
36 self.client, self.gateway, self.netmask,
37 )
38 }
39}
40
41impl FromStr for KernelIpParam {
42 type Err = String;
43
44 /// Parse a kernel `ip=` token (e.g. `ip=172.20.0.2::172.20.0.1:255.255.0.0::eth0:off`).
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 let value = s.strip_prefix("ip=").ok_or("missing ip= prefix")?;
47 let mut fields = value.split(':');
48
49 let client = fields
50 .next()
51 .filter(|s| !s.is_empty())
52 .ok_or("missing client field")?
53 .parse::<Ipv4Addr>()
54 .map_err(|e| format!("invalid client IP: {e}"))?;
55
56 // Skip <server> (always empty in our usage).
57 fields.next();
58
59 let gateway = fields
60 .next()
61 .filter(|s| !s.is_empty())
62 .ok_or("missing gateway field")?
63 .parse::<Ipv4Addr>()
64 .map_err(|e| format!("invalid gateway IP: {e}"))?;
65
66 let netmask = fields
67 .next()
68 .filter(|s| !s.is_empty())
69 .ok_or("missing netmask field")?
70 .parse::<Ipv4Addr>()
71 .map_err(|e| format!("invalid netmask: {e}"))?;
72
73 Ok(Self {
74 client,
75 gateway,
76 netmask,
77 })
78 }
79}
80
81/// Network reconfiguration command sent to `vm-agent` over the exec vsock
82/// channel (`MSG_NET_RECONFIG`) after a fresh-network snapshot restore.
83///
84/// A restored VM still carries the network the kernel configured from the
85/// origin's `ip=` boot parameter; when the restore allocated a new TAP/IP
86/// (`network_override`), the host sends this command so the guest
87/// re-addresses `eth0`, replaces the default route, and repoints DNS at the
88/// new gateway. Serialized as JSON, mirroring `StartCommand`.
89#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
90pub struct NetReconfigCommand {
91 /// New guest IP address for `eth0`.
92 pub ip: Ipv4Addr,
93 /// Subnet mask (e.g. 255.255.0.0).
94 pub netmask: Ipv4Addr,
95 /// Gateway IP (also serves as the guest DNS nameserver).
96 pub gateway: Ipv4Addr,
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn roundtrip() {
105 let param = KernelIpParam {
106 client: Ipv4Addr::new(172, 20, 0, 2),
107 gateway: Ipv4Addr::new(172, 20, 0, 1),
108 netmask: Ipv4Addr::new(255, 255, 0, 0),
109 };
110 let s = param.to_string();
111 assert_eq!(s, "ip=172.20.0.2::172.20.0.1:255.255.0.0::eth0:off");
112 assert_eq!(s.parse::<KernelIpParam>().unwrap(), param);
113 }
114
115 #[test]
116 fn rejects_invalid() {
117 assert!("no-prefix".parse::<KernelIpParam>().is_err());
118 assert!("ip=".parse::<KernelIpParam>().is_err());
119 assert!(
120 "ip=bogus::1.2.3.4:255.0.0.0::eth0:off"
121 .parse::<KernelIpParam>()
122 .is_err()
123 );
124 }
125}