1use std::net::{Ipv4Addr, Ipv6Addr};
4
5use microsandbox_types::ExecLimits;
6use serde::{Deserialize, Serialize};
7
8use crate::exec::ExecRlimit;
9
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
19pub struct GuestBootstrap {
20 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
22 pub init_failure_ack: bool,
23
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub block_root: Option<BootstrapBlockRoot>,
27
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
30 pub dir_mounts: Vec<BootstrapDirMount>,
31
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub file_mounts: Vec<BootstrapFileMount>,
35
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub disk_mounts: Vec<BootstrapDiskMount>,
39
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub tmpfs_mounts: Vec<BootstrapTmpfsMount>,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub hostname: Option<String>,
47
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub host_alias: Option<String>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub network: Option<BootstrapNetwork>,
55
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub rlimits: Vec<ExecRlimit>,
59
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub user: Option<String>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub default_cwd: Option<String>,
67
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub default_env: Vec<BootstrapEnvVar>,
74
75 #[serde(default)]
77 pub security_profile: BootstrapSecurityProfile,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub handoff_init: Option<BootstrapHandoffInit>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub exec_limits: Option<ExecLimits>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(tag = "kind", rename_all = "kebab-case")]
91pub enum BootstrapBlockRoot {
92 DiskImage {
94 device: String,
96
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 fstype: Option<String>,
100 },
101
102 OciErofs {
104 lower: String,
106
107 upper: BootstrapBlockRootUpper,
109 },
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(tag = "kind", rename_all = "kebab-case")]
115pub enum BootstrapBlockRootUpper {
116 Device {
118 device: String,
120
121 fstype: String,
123 },
124
125 Tmpfs {
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 size_mib: Option<u32>,
130 },
131}
132
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135pub struct BootstrapMountFlags {
136 #[serde(default)]
138 pub readonly: bool,
139
140 #[serde(default)]
142 pub noexec: bool,
143
144 #[serde(default)]
146 pub nosuid: bool,
147
148 #[serde(default)]
150 pub nodev: bool,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct BootstrapDirMount {
156 pub tag: String,
158
159 pub guest_path: String,
161
162 #[serde(default)]
164 pub flags: BootstrapMountFlags,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct BootstrapFileMount {
170 pub tag: String,
172
173 pub filename: String,
175
176 pub guest_path: String,
178
179 #[serde(default)]
181 pub flags: BootstrapMountFlags,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct BootstrapDiskMount {
187 pub id: String,
189
190 pub guest_path: String,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub fstype: Option<String>,
196
197 #[serde(default)]
199 pub flags: BootstrapMountFlags,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct BootstrapTmpfsMount {
205 pub path: String,
207
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub size_mib: Option<u32>,
211
212 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub mode: Option<u32>,
215
216 #[serde(default)]
218 pub flags: BootstrapMountFlags,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub struct BootstrapNetwork {
224 pub interface: String,
226
227 pub mac: [u8; 6],
229
230 pub mtu: u16,
232
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub ipv4: Option<BootstrapIpv4>,
236
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub ipv6: Option<BootstrapIpv6>,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244pub struct BootstrapIpv4 {
245 pub address: Ipv4Addr,
247
248 pub prefix_len: u8,
250
251 pub gateway: Ipv4Addr,
253
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub dns: Option<Ipv4Addr>,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
261pub struct BootstrapIpv6 {
262 pub address: Ipv6Addr,
264
265 pub prefix_len: u8,
267
268 pub gateway: Ipv6Addr,
270
271 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub dns: Option<Ipv6Addr>,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct BootstrapEnvVar {
279 pub key: String,
281
282 pub value: String,
284}
285
286#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289pub enum BootstrapSecurityProfile {
290 #[default]
292 Default,
293
294 Restricted,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct BootstrapHandoffInit {
301 pub cmd: String,
303
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
306 pub args: Vec<String>,
307
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub cwd: Option<String>,
311
312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
314 pub env: Vec<BootstrapEnvVar>,
315}
316
317#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::{
325 codec,
326 message::{Message, MessageType, PROTOCOL_VERSION},
327 };
328
329 #[test]
330 fn guest_bootstrap_round_trips_transport_sensitive_values() {
331 let bootstrap = GuestBootstrap {
332 exec_limits: None,
333 init_failure_ack: true,
334 block_root: Some(BootstrapBlockRoot::OciErofs {
335 lower: "/dev/vda".to_string(),
336 upper: BootstrapBlockRootUpper::Tmpfs {
337 size_mib: Some(512),
338 },
339 }),
340 dir_mounts: vec![BootstrapDirMount {
341 tag: "workspace".to_string(),
342 guest_path: "/workspace:with separators".to_string(),
343 flags: BootstrapMountFlags {
344 noexec: true,
345 ..BootstrapMountFlags::default()
346 },
347 }],
348 file_mounts: vec![BootstrapFileMount {
349 tag: "config".to_string(),
350 filename: "app.json".to_string(),
351 guest_path: "/etc/app.json".to_string(),
352 flags: BootstrapMountFlags {
353 readonly: true,
354 ..BootstrapMountFlags::default()
355 },
356 }],
357 disk_mounts: vec![BootstrapDiskMount {
358 id: "data".to_string(),
359 guest_path: "/data".to_string(),
360 fstype: Some("ext4".to_string()),
361 flags: BootstrapMountFlags::default(),
362 }],
363 tmpfs_mounts: vec![BootstrapTmpfsMount {
364 path: "/tmp".to_string(),
365 size_mib: Some(64),
366 mode: Some(0o1777),
367 flags: BootstrapMountFlags::default(),
368 }],
369 hostname: Some("quoted-env-test".to_string()),
370 host_alias: Some("host.microsandbox.internal".to_string()),
371 network: Some(BootstrapNetwork {
372 interface: "eth0".to_string(),
373 mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
374 mtu: 1500,
375 ipv4: Some(BootstrapIpv4 {
376 address: "172.16.0.2".parse().unwrap(),
377 prefix_len: 30,
378 gateway: "172.16.0.1".parse().unwrap(),
379 dns: Some("172.16.0.1".parse().unwrap()),
380 }),
381 ipv6: Some(BootstrapIpv6 {
382 address: "fd42:6d73:62::2".parse().unwrap(),
383 prefix_len: 64,
384 gateway: "fd42:6d73:62::1".parse().unwrap(),
385 dns: Some("fd42:6d73:62::1".parse().unwrap()),
386 }),
387 }),
388 rlimits: vec![ExecRlimit {
389 resource: "nofile".to_string(),
390 soft: 1024,
391 hard: 4096,
392 }],
393 user: Some("1000:1000".to_string()),
394 default_cwd: Some("/workspace with spaces".to_string()),
395 default_env: vec![
396 BootstrapEnvVar {
397 key: "APP_CONFIG".to_string(),
398 value: "{\"message\":\"hello\"}".to_string(),
399 },
400 BootstrapEnvVar {
401 key: "UNICODE".to_string(),
402 value: "snowman: \u{2603}\nnext\tcolumn".to_string(),
403 },
404 BootstrapEnvVar {
405 key: "EMPTY".to_string(),
406 value: String::new(),
407 },
408 ],
409 security_profile: BootstrapSecurityProfile::Restricted,
410 handoff_init: Some(BootstrapHandoffInit {
411 cmd: "/sbin/init".to_string(),
412 args: vec!["--unit=multi user.target".to_string()],
413 cwd: Some("/workspace with spaces".to_string()),
414 env: vec![BootstrapEnvVar {
415 key: "HANDOFF_JSON".to_string(),
416 value: "{\"enabled\":true}".to_string(),
417 }],
418 }),
419 };
420
421 let message = Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
422 assert_eq!(message.v, PROTOCOL_VERSION);
423 let mut frame = Vec::new();
424 codec::encode_to_buf(&message, &mut frame).unwrap();
425 let decoded = codec::decode_message_frame(&frame).unwrap();
426 assert_eq!(decoded.payload::<GuestBootstrap>().unwrap(), bootstrap);
427 }
428}