use std::net::{Ipv4Addr, Ipv6Addr};
use serde::{Deserialize, Serialize};
use crate::exec::ExecRlimit;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuestBootstrap {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_root: Option<BootstrapBlockRoot>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dir_mounts: Vec<BootstrapDirMount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_mounts: Vec<BootstrapFileMount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disk_mounts: Vec<BootstrapDiskMount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tmpfs_mounts: Vec<BootstrapTmpfsMount>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub host_alias: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network: Option<BootstrapNetwork>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rlimits: Vec<ExecRlimit>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_cwd: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_env: Vec<BootstrapEnvVar>,
#[serde(default)]
pub security_profile: BootstrapSecurityProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handoff_init: Option<BootstrapHandoffInit>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum BootstrapBlockRoot {
DiskImage {
device: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
fstype: Option<String>,
},
OciErofs {
lower: String,
upper: BootstrapBlockRootUpper,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum BootstrapBlockRootUpper {
Device {
device: String,
fstype: String,
},
Tmpfs {
#[serde(default, skip_serializing_if = "Option::is_none")]
size_mib: Option<u32>,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapMountFlags {
#[serde(default)]
pub readonly: bool,
#[serde(default)]
pub noexec: bool,
#[serde(default)]
pub nosuid: bool,
#[serde(default)]
pub nodev: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapDirMount {
pub tag: String,
pub guest_path: String,
#[serde(default)]
pub flags: BootstrapMountFlags,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapFileMount {
pub tag: String,
pub filename: String,
pub guest_path: String,
#[serde(default)]
pub flags: BootstrapMountFlags,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapDiskMount {
pub id: String,
pub guest_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fstype: Option<String>,
#[serde(default)]
pub flags: BootstrapMountFlags,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapTmpfsMount {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_mib: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<u32>,
#[serde(default)]
pub flags: BootstrapMountFlags,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapNetwork {
pub interface: String,
pub mac: [u8; 6],
pub mtu: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ipv4: Option<BootstrapIpv4>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ipv6: Option<BootstrapIpv6>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapIpv4 {
pub address: Ipv4Addr,
pub prefix_len: u8,
pub gateway: Ipv4Addr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Ipv4Addr>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapIpv6 {
pub address: Ipv6Addr,
pub prefix_len: u8,
pub gateway: Ipv6Addr,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Ipv6Addr>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapEnvVar {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BootstrapSecurityProfile {
#[default]
Default,
Restricted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapHandoffInit {
pub cmd: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<BootstrapEnvVar>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
codec,
message::{Message, MessageType, PROTOCOL_VERSION},
};
#[test]
fn guest_bootstrap_round_trips_transport_sensitive_values() {
let bootstrap = GuestBootstrap {
block_root: Some(BootstrapBlockRoot::OciErofs {
lower: "/dev/vda".to_string(),
upper: BootstrapBlockRootUpper::Tmpfs {
size_mib: Some(512),
},
}),
dir_mounts: vec![BootstrapDirMount {
tag: "workspace".to_string(),
guest_path: "/workspace:with separators".to_string(),
flags: BootstrapMountFlags {
noexec: true,
..BootstrapMountFlags::default()
},
}],
file_mounts: vec![BootstrapFileMount {
tag: "config".to_string(),
filename: "app.json".to_string(),
guest_path: "/etc/app.json".to_string(),
flags: BootstrapMountFlags {
readonly: true,
..BootstrapMountFlags::default()
},
}],
disk_mounts: vec![BootstrapDiskMount {
id: "data".to_string(),
guest_path: "/data".to_string(),
fstype: Some("ext4".to_string()),
flags: BootstrapMountFlags::default(),
}],
tmpfs_mounts: vec![BootstrapTmpfsMount {
path: "/tmp".to_string(),
size_mib: Some(64),
mode: Some(0o1777),
flags: BootstrapMountFlags::default(),
}],
hostname: Some("quoted-env-test".to_string()),
host_alias: Some("host.microsandbox.internal".to_string()),
network: Some(BootstrapNetwork {
interface: "eth0".to_string(),
mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
mtu: 1500,
ipv4: Some(BootstrapIpv4 {
address: "172.16.0.2".parse().unwrap(),
prefix_len: 30,
gateway: "172.16.0.1".parse().unwrap(),
dns: Some("172.16.0.1".parse().unwrap()),
}),
ipv6: Some(BootstrapIpv6 {
address: "fd42:6d73:62::2".parse().unwrap(),
prefix_len: 64,
gateway: "fd42:6d73:62::1".parse().unwrap(),
dns: Some("fd42:6d73:62::1".parse().unwrap()),
}),
}),
rlimits: vec![ExecRlimit {
resource: "nofile".to_string(),
soft: 1024,
hard: 4096,
}],
user: Some("1000:1000".to_string()),
default_cwd: Some("/workspace with spaces".to_string()),
default_env: vec![
BootstrapEnvVar {
key: "APP_CONFIG".to_string(),
value: "{\"message\":\"hello\"}".to_string(),
},
BootstrapEnvVar {
key: "UNICODE".to_string(),
value: "snowman: \u{2603}\nnext\tcolumn".to_string(),
},
BootstrapEnvVar {
key: "EMPTY".to_string(),
value: String::new(),
},
],
security_profile: BootstrapSecurityProfile::Restricted,
handoff_init: Some(BootstrapHandoffInit {
cmd: "/sbin/init".to_string(),
args: vec!["--unit=multi user.target".to_string()],
cwd: Some("/workspace with spaces".to_string()),
env: vec![BootstrapEnvVar {
key: "HANDOFF_JSON".to_string(),
value: "{\"enabled\":true}".to_string(),
}],
}),
};
let message = Message::with_payload(MessageType::Bootstrap, 0, &bootstrap).unwrap();
assert_eq!(message.v, PROTOCOL_VERSION);
let mut frame = Vec::new();
codec::encode_to_buf(&message, &mut frame).unwrap();
let decoded = codec::decode_message_frame(&frame).unwrap();
assert_eq!(decoded.payload::<GuestBootstrap>().unwrap(), bootstrap);
}
}