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 = "std::ops::Not::not")]
21 pub init_failure_ack: bool,
22
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub block_root: Option<BootstrapBlockRoot>,
26
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub dir_mounts: Vec<BootstrapDirMount>,
30
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub file_mounts: Vec<BootstrapFileMount>,
34
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub disk_mounts: Vec<BootstrapDiskMount>,
38
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub tmpfs_mounts: Vec<BootstrapTmpfsMount>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub hostname: Option<String>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub host_alias: Option<String>,
50
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub network: Option<BootstrapNetwork>,
54
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub rlimits: Vec<ExecRlimit>,
58
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub user: Option<String>,
62
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub default_cwd: Option<String>,
66
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub default_env: Vec<BootstrapEnvVar>,
73
74 #[serde(default)]
76 pub security_profile: BootstrapSecurityProfile,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub handoff_init: Option<BootstrapHandoffInit>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(tag = "kind", rename_all = "kebab-case")]
86pub enum BootstrapBlockRoot {
87 DiskImage {
89 device: String,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 fstype: Option<String>,
95 },
96
97 OciErofs {
99 lower: String,
101
102 upper: BootstrapBlockRootUpper,
104 },
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(tag = "kind", rename_all = "kebab-case")]
110pub enum BootstrapBlockRootUpper {
111 Device {
113 device: String,
115
116 fstype: String,
118 },
119
120 Tmpfs {
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 size_mib: Option<u32>,
125 },
126}
127
128#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
130pub struct BootstrapMountFlags {
131 #[serde(default)]
133 pub readonly: bool,
134
135 #[serde(default)]
137 pub noexec: bool,
138
139 #[serde(default)]
141 pub nosuid: bool,
142
143 #[serde(default)]
145 pub nodev: bool,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct BootstrapDirMount {
151 pub tag: String,
153
154 pub guest_path: String,
156
157 #[serde(default)]
159 pub flags: BootstrapMountFlags,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct BootstrapFileMount {
165 pub tag: String,
167
168 pub filename: String,
170
171 pub guest_path: String,
173
174 #[serde(default)]
176 pub flags: BootstrapMountFlags,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct BootstrapDiskMount {
182 pub id: String,
184
185 pub guest_path: String,
187
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub fstype: Option<String>,
191
192 #[serde(default)]
194 pub flags: BootstrapMountFlags,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct BootstrapTmpfsMount {
200 pub path: String,
202
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub size_mib: Option<u32>,
206
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub mode: Option<u32>,
210
211 #[serde(default)]
213 pub flags: BootstrapMountFlags,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct BootstrapNetwork {
219 pub interface: String,
221
222 pub mac: [u8; 6],
224
225 pub mtu: u16,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub ipv4: Option<BootstrapIpv4>,
231
232 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub ipv6: Option<BootstrapIpv6>,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
239pub struct BootstrapIpv4 {
240 pub address: Ipv4Addr,
242
243 pub prefix_len: u8,
245
246 pub gateway: Ipv4Addr,
248
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub dns: Option<Ipv4Addr>,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256pub struct BootstrapIpv6 {
257 pub address: Ipv6Addr,
259
260 pub prefix_len: u8,
262
263 pub gateway: Ipv6Addr,
265
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub dns: Option<Ipv6Addr>,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct BootstrapEnvVar {
274 pub key: String,
276
277 pub value: String,
279}
280
281#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "snake_case")]
284pub enum BootstrapSecurityProfile {
285 #[default]
287 Default,
288
289 Restricted,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct BootstrapHandoffInit {
296 pub cmd: String,
298
299 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub args: Vec<String>,
302
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub cwd: Option<String>,
306
307 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 pub env: Vec<BootstrapEnvVar>,
310}
311
312#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{
320 codec,
321 message::{Message, MessageType, PROTOCOL_VERSION},
322 };
323
324 #[test]
325 fn guest_bootstrap_round_trips_transport_sensitive_values() {
326 let bootstrap = GuestBootstrap {
327 init_failure_ack: true,
328 block_root: Some(BootstrapBlockRoot::OciErofs {
329 lower: "/dev/vda".to_string(),
330 upper: BootstrapBlockRootUpper::Tmpfs {
331 size_mib: Some(512),
332 },
333 }),
334 dir_mounts: vec![BootstrapDirMount {
335 tag: "workspace".to_string(),
336 guest_path: "/workspace:with separators".to_string(),
337 flags: BootstrapMountFlags {
338 noexec: true,
339 ..BootstrapMountFlags::default()
340 },
341 }],
342 file_mounts: vec![BootstrapFileMount {
343 tag: "config".to_string(),
344 filename: "app.json".to_string(),
345 guest_path: "/etc/app.json".to_string(),
346 flags: BootstrapMountFlags {
347 readonly: true,
348 ..BootstrapMountFlags::default()
349 },
350 }],
351 disk_mounts: vec![BootstrapDiskMount {
352 id: "data".to_string(),
353 guest_path: "/data".to_string(),
354 fstype: Some("ext4".to_string()),
355 flags: BootstrapMountFlags::default(),
356 }],
357 tmpfs_mounts: vec![BootstrapTmpfsMount {
358 path: "/tmp".to_string(),
359 size_mib: Some(64),
360 mode: Some(0o1777),
361 flags: BootstrapMountFlags::default(),
362 }],
363 hostname: Some("quoted-env-test".to_string()),
364 host_alias: Some("host.microsandbox.internal".to_string()),
365 network: Some(BootstrapNetwork {
366 interface: "eth0".to_string(),
367 mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
368 mtu: 1500,
369 ipv4: Some(BootstrapIpv4 {
370 address: "172.16.0.2".parse().unwrap(),
371 prefix_len: 30,
372 gateway: "172.16.0.1".parse().unwrap(),
373 dns: Some("172.16.0.1".parse().unwrap()),
374 }),
375 ipv6: Some(BootstrapIpv6 {
376 address: "fd42:6d73:62::2".parse().unwrap(),
377 prefix_len: 64,
378 gateway: "fd42:6d73:62::1".parse().unwrap(),
379 dns: Some("fd42:6d73:62::1".parse().unwrap()),
380 }),
381 }),
382 rlimits: vec![ExecRlimit {
383 resource: "nofile".to_string(),
384 soft: 1024,
385 hard: 4096,
386 }],
387 user: Some("1000:1000".to_string()),
388 default_cwd: Some("/workspace with spaces".to_string()),
389 default_env: vec![
390 BootstrapEnvVar {
391 key: "APP_CONFIG".to_string(),
392 value: "{\"message\":\"hello\"}".to_string(),
393 },
394 BootstrapEnvVar {
395 key: "UNICODE".to_string(),
396 value: "snowman: \u{2603}\nnext\tcolumn".to_string(),
397 },
398 BootstrapEnvVar {
399 key: "EMPTY".to_string(),
400 value: String::new(),
401 },
402 ],
403 security_profile: BootstrapSecurityProfile::Restricted,
404 handoff_init: Some(BootstrapHandoffInit {
405 cmd: "/sbin/init".to_string(),
406 args: vec!["--unit=multi user.target".to_string()],
407 cwd: Some("/workspace with spaces".to_string()),
408 env: vec![BootstrapEnvVar {
409 key: "HANDOFF_JSON".to_string(),
410 value: "{\"enabled\":true}".to_string(),
411 }],
412 }),
413 };
414
415 let message = Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
416 assert_eq!(message.v, PROTOCOL_VERSION);
417 let mut frame = Vec::new();
418 codec::encode_to_buf(&message, &mut frame).unwrap();
419 let decoded = codec::decode_message_frame(&frame).unwrap();
420 assert_eq!(decoded.payload::<GuestBootstrap>().unwrap(), bootstrap);
421 }
422}