1use std::net::{Ipv4Addr, Ipv6Addr};
4
5use serde::{Deserialize, Serialize};
6
7use crate::exec::ExecRlimit;
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub struct GuestBootstrap {
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub block_root: Option<BootstrapBlockRoot>,
22
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub dir_mounts: Vec<BootstrapDirMount>,
26
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub file_mounts: Vec<BootstrapFileMount>,
30
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub disk_mounts: Vec<BootstrapDiskMount>,
34
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub tmpfs_mounts: Vec<BootstrapTmpfsMount>,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub hostname: Option<String>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub host_alias: Option<String>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub network: Option<BootstrapNetwork>,
50
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub rlimits: Vec<ExecRlimit>,
54
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub user: Option<String>,
58
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub default_cwd: Option<String>,
62
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub default_env: Vec<BootstrapEnvVar>,
69
70 #[serde(default)]
72 pub security_profile: BootstrapSecurityProfile,
73
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub handoff_init: Option<BootstrapHandoffInit>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(tag = "kind", rename_all = "kebab-case")]
82pub enum BootstrapBlockRoot {
83 DiskImage {
85 device: String,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 fstype: Option<String>,
91 },
92
93 OciErofs {
95 lower: String,
97
98 upper: BootstrapBlockRootUpper,
100 },
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(tag = "kind", rename_all = "kebab-case")]
106pub enum BootstrapBlockRootUpper {
107 Device {
109 device: String,
111
112 fstype: String,
114 },
115
116 Tmpfs {
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 size_mib: Option<u32>,
121 },
122}
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
126pub struct BootstrapMountFlags {
127 #[serde(default)]
129 pub readonly: bool,
130
131 #[serde(default)]
133 pub noexec: bool,
134
135 #[serde(default)]
137 pub nosuid: bool,
138
139 #[serde(default)]
141 pub nodev: bool,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct BootstrapDirMount {
147 pub tag: String,
149
150 pub guest_path: String,
152
153 #[serde(default)]
155 pub flags: BootstrapMountFlags,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct BootstrapFileMount {
161 pub tag: String,
163
164 pub filename: String,
166
167 pub guest_path: String,
169
170 #[serde(default)]
172 pub flags: BootstrapMountFlags,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct BootstrapDiskMount {
178 pub id: String,
180
181 pub guest_path: String,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub fstype: Option<String>,
187
188 #[serde(default)]
190 pub flags: BootstrapMountFlags,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct BootstrapTmpfsMount {
196 pub path: String,
198
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub size_mib: Option<u32>,
202
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub mode: Option<u32>,
206
207 #[serde(default)]
209 pub flags: BootstrapMountFlags,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct BootstrapNetwork {
215 pub interface: String,
217
218 pub mac: [u8; 6],
220
221 pub mtu: u16,
223
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub ipv4: Option<BootstrapIpv4>,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub ipv6: Option<BootstrapIpv6>,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235pub struct BootstrapIpv4 {
236 pub address: Ipv4Addr,
238
239 pub prefix_len: u8,
241
242 pub gateway: Ipv4Addr,
244
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub dns: Option<Ipv4Addr>,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
252pub struct BootstrapIpv6 {
253 pub address: Ipv6Addr,
255
256 pub prefix_len: u8,
258
259 pub gateway: Ipv6Addr,
261
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub dns: Option<Ipv6Addr>,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub struct BootstrapEnvVar {
270 pub key: String,
272
273 pub value: String,
275}
276
277#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "snake_case")]
280pub enum BootstrapSecurityProfile {
281 #[default]
283 Default,
284
285 Restricted,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291pub struct BootstrapHandoffInit {
292 pub cmd: String,
294
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub args: Vec<String>,
298
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub cwd: Option<String>,
302
303 #[serde(default, skip_serializing_if = "Vec::is_empty")]
305 pub env: Vec<BootstrapEnvVar>,
306}
307
308#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::{
316 codec,
317 message::{Message, MessageType, PROTOCOL_VERSION},
318 };
319
320 #[test]
321 fn guest_bootstrap_round_trips_transport_sensitive_values() {
322 let bootstrap = GuestBootstrap {
323 block_root: Some(BootstrapBlockRoot::OciErofs {
324 lower: "/dev/vda".to_string(),
325 upper: BootstrapBlockRootUpper::Tmpfs {
326 size_mib: Some(512),
327 },
328 }),
329 dir_mounts: vec![BootstrapDirMount {
330 tag: "workspace".to_string(),
331 guest_path: "/workspace:with separators".to_string(),
332 flags: BootstrapMountFlags {
333 noexec: true,
334 ..BootstrapMountFlags::default()
335 },
336 }],
337 file_mounts: vec![BootstrapFileMount {
338 tag: "config".to_string(),
339 filename: "app.json".to_string(),
340 guest_path: "/etc/app.json".to_string(),
341 flags: BootstrapMountFlags {
342 readonly: true,
343 ..BootstrapMountFlags::default()
344 },
345 }],
346 disk_mounts: vec![BootstrapDiskMount {
347 id: "data".to_string(),
348 guest_path: "/data".to_string(),
349 fstype: Some("ext4".to_string()),
350 flags: BootstrapMountFlags::default(),
351 }],
352 tmpfs_mounts: vec![BootstrapTmpfsMount {
353 path: "/tmp".to_string(),
354 size_mib: Some(64),
355 mode: Some(0o1777),
356 flags: BootstrapMountFlags::default(),
357 }],
358 hostname: Some("quoted-env-test".to_string()),
359 host_alias: Some("host.microsandbox.internal".to_string()),
360 network: Some(BootstrapNetwork {
361 interface: "eth0".to_string(),
362 mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
363 mtu: 1500,
364 ipv4: Some(BootstrapIpv4 {
365 address: "172.16.0.2".parse().unwrap(),
366 prefix_len: 30,
367 gateway: "172.16.0.1".parse().unwrap(),
368 dns: Some("172.16.0.1".parse().unwrap()),
369 }),
370 ipv6: Some(BootstrapIpv6 {
371 address: "fd42:6d73:62::2".parse().unwrap(),
372 prefix_len: 64,
373 gateway: "fd42:6d73:62::1".parse().unwrap(),
374 dns: Some("fd42:6d73:62::1".parse().unwrap()),
375 }),
376 }),
377 rlimits: vec![ExecRlimit {
378 resource: "nofile".to_string(),
379 soft: 1024,
380 hard: 4096,
381 }],
382 user: Some("1000:1000".to_string()),
383 default_cwd: Some("/workspace with spaces".to_string()),
384 default_env: vec![
385 BootstrapEnvVar {
386 key: "APP_CONFIG".to_string(),
387 value: "{\"message\":\"hello\"}".to_string(),
388 },
389 BootstrapEnvVar {
390 key: "UNICODE".to_string(),
391 value: "snowman: \u{2603}\nnext\tcolumn".to_string(),
392 },
393 BootstrapEnvVar {
394 key: "EMPTY".to_string(),
395 value: String::new(),
396 },
397 ],
398 security_profile: BootstrapSecurityProfile::Restricted,
399 handoff_init: Some(BootstrapHandoffInit {
400 cmd: "/sbin/init".to_string(),
401 args: vec!["--unit=multi user.target".to_string()],
402 cwd: Some("/workspace with spaces".to_string()),
403 env: vec![BootstrapEnvVar {
404 key: "HANDOFF_JSON".to_string(),
405 value: "{\"enabled\":true}".to_string(),
406 }],
407 }),
408 };
409
410 let message = Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
411 assert_eq!(message.v, PROTOCOL_VERSION);
412 let mut frame = Vec::new();
413 codec::encode_to_buf(&message, &mut frame).unwrap();
414 let decoded = codec::decode_message_frame(&frame).unwrap();
415 assert_eq!(decoded.payload::<GuestBootstrap>().unwrap(), bootstrap);
416 }
417}