1use std::collections::BTreeMap;
4use std::fmt;
5use std::net::{Ipv4Addr, Ipv6Addr};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
10use serde::{Deserialize, Serialize};
11use zeroize::Zeroizing;
12
13use crate::modify::SecretSource;
14
15pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
21
22pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
24
25pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
35#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
36pub enum DiskImageFormat {
37 Qcow2,
39 Raw,
41 Vmdk,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
48pub enum RootfsSource {
49 Bind {
51 #[cfg_attr(feature = "ts", ts(type = "string"))]
53 path: PathBuf,
54 #[serde(default)]
61 follow_root_symlinks: bool,
62 },
63
64 Oci(OciRootfsSource),
66
67 DiskImage {
69 #[cfg_attr(feature = "ts", ts(type = "string"))]
71 path: PathBuf,
72 format: DiskImageFormat,
74 fstype: Option<String>,
76 },
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
82#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
83pub struct OciRootfsSource {
84 pub reference: String,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub root_disk: Option<RootDisk>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
99#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
100#[serde(tag = "kind", rename_all = "kebab-case")]
101pub enum RootDisk {
102 Managed {
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 size_mib: Option<u32>,
108 },
109
110 Tmpfs {
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 size_mib: Option<u32>,
116 },
117
118 DiskImage {
121 #[cfg_attr(feature = "ts", ts(type = "string"))]
123 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
124 path: PathBuf,
125 format: DiskImageFormat,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 fstype: Option<String>,
130 },
131}
132
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
136#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
137pub enum PullPolicy {
138 #[default]
140 IfMissing,
141
142 Always,
144
145 Never,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
158#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
159#[serde(rename_all = "lowercase")]
160pub enum StatVirtualization {
161 Strict,
163 Relaxed,
165 Off,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
174#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
175#[serde(rename_all = "lowercase")]
176pub enum HostPermissions {
177 Private,
179 Mirror,
181}
182
183#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
185#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
186#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
187#[serde(rename_all = "lowercase")]
188pub enum SecurityProfile {
189 #[default]
193 Default,
194
195 Restricted,
199}
200
201#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
203#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
204#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
205#[serde(default)]
206pub struct MountOptions {
207 pub readonly: bool,
211
212 pub noexec: bool,
216
217 pub nosuid: bool,
219
220 pub nodev: bool,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
228pub enum VolumeKind {
229 Directory,
231
232 Disk,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
239#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
240pub struct VolumeSpec {
241 pub name: String,
243
244 pub kind: VolumeKind,
246
247 pub quota_mib: Option<u32>,
249
250 pub capacity_mib: Option<u32>,
252
253 pub labels: Vec<(String, String)>,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
260#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
261pub enum NamedVolumeMode {
262 Existing,
264
265 Create,
267
268 EnsureExists,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
275#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
276pub struct NamedVolumeCreate {
277 pub mode: NamedVolumeMode,
279
280 pub name: String,
282
283 pub kind: VolumeKind,
285
286 pub quota_mib: Option<u32>,
288
289 pub capacity_mib: Option<u32>,
291
292 pub labels: Vec<(String, String)>,
294}
295
296#[derive(Clone)]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
300#[cfg_attr(feature = "ts", ts(tag = "type"))]
301pub enum VolumeMount {
302 Bind {
304 #[cfg_attr(feature = "ts", ts(type = "string"))]
306 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
307 host: PathBuf,
308 guest: String,
310 options: MountOptions,
312 stat_virtualization: StatVirtualization,
314 host_permissions: HostPermissions,
316 follow_root_symlinks: bool,
323 quota_mib: Option<u32>,
329 },
330
331 Named {
333 name: String,
335 guest: String,
337 create: Option<NamedVolumeCreate>,
341 options: MountOptions,
343 stat_virtualization: StatVirtualization,
345 host_permissions: HostPermissions,
347 follow_root_symlinks: bool,
352 },
353
354 Tmpfs {
356 guest: String,
358 size_mib: Option<u32>,
360 options: MountOptions,
362 },
363
364 DiskImage {
366 #[cfg_attr(feature = "ts", ts(type = "string"))]
368 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
369 host: PathBuf,
370 guest: String,
372 format: DiskImageFormat,
374 fstype: Option<String>,
376 options: MountOptions,
378 },
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
384#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
385pub enum Patch {
386 Text {
388 path: String,
390 content: String,
392 mode: Option<u32>,
394 replace: bool,
396 },
397
398 File {
400 path: String,
402 content: Vec<u8>,
404 mode: Option<u32>,
406 replace: bool,
408 },
409
410 CopyFile {
412 #[cfg_attr(feature = "ts", ts(type = "string"))]
414 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
415 src: PathBuf,
416 dst: String,
418 mode: Option<u32>,
420 replace: bool,
422 },
423
424 CopyDir {
426 #[cfg_attr(feature = "ts", ts(type = "string"))]
428 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
429 src: PathBuf,
430 dst: String,
432 replace: bool,
434 },
435
436 Symlink {
438 target: String,
440 link: String,
442 replace: bool,
444 },
445
446 Mkdir {
448 path: String,
450 mode: Option<u32>,
452 },
453
454 Remove {
456 path: String,
458 },
459
460 Append {
462 path: String,
464 content: String,
466 },
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
477#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
478#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
479#[serde(default)]
480pub struct NetworkSpec {
481 pub enabled: bool,
483
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub interface: Option<InterfaceOverrides>,
487
488 pub ports: Vec<PublishedPortSpec>,
490
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub policy: Option<NetworkPolicy>,
494
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub dns: Option<DnsConfig>,
498
499 #[serde(skip_serializing_if = "Option::is_none")]
501 pub tls: Option<TlsConfig>,
502
503 #[serde(skip_serializing_if = "Option::is_none")]
505 pub secrets: Option<SecretsConfig>,
506
507 pub max_connections: Option<usize>,
509
510 pub trust_host_cas: bool,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
516#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
517#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
518pub struct PublishedPortSpec {
519 pub host_port: u16,
521
522 pub guest_port: u16,
524
525 #[serde(default)]
527 pub protocol: PortProtocol,
528
529 pub host_bind: String,
531}
532
533#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
535#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
536#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
537pub enum PortProtocol {
538 #[default]
540 #[serde(rename = "tcp")]
541 Tcp,
542
543 #[serde(rename = "udp")]
545 Udp,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
554#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
555#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
556pub struct HandoffInit {
557 pub cmd: String,
561
562 #[serde(default)]
564 pub args: Vec<String>,
565
566 #[serde(default)]
568 pub env: Vec<(String, String)>,
569}
570
571#[derive(Debug, Default, Clone, Serialize, Deserialize)]
577#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
578#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
579pub struct SandboxPolicy {
580 #[serde(default)]
589 pub ephemeral: bool,
590
591 pub max_duration_secs: Option<u64>,
593
594 pub idle_timeout_secs: Option<u64>,
596}
597
598#[derive(Debug, Clone, Serialize, Deserialize)]
609#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
610pub struct SnapshotSpec {
611 pub name: String,
613
614 #[serde(default)]
617 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
618 pub dest_dir: Option<PathBuf>,
619
620 pub source_sandbox: String,
622
623 pub labels: Vec<(String, String)>,
625
626 pub force: bool,
628
629 pub record_integrity: bool,
631
632 #[serde(default)]
638 pub resumable: bool,
639}
640
641#[derive(Debug, Default, Clone, Serialize, Deserialize)]
649#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
650#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
651#[serde(default)]
652pub struct SandboxSpec {
653 pub name: String,
655
656 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
658 pub image: RootfsSource,
659
660 pub resources: SandboxResources,
662
663 pub runtime: SandboxRuntimeOptions,
665
666 pub env: Vec<EnvVar>,
668
669 pub labels: BTreeMap<String, String>,
671
672 pub rlimits: Vec<Rlimit>,
674
675 pub mounts: Vec<VolumeMount>,
677
678 pub patches: Vec<Patch>,
680
681 pub network: NetworkSpec,
683
684 pub init: Option<HandoffInit>,
686
687 pub pull_policy: PullPolicy,
689
690 pub security_profile: SecurityProfile,
692
693 pub lifecycle: SandboxPolicy,
695}
696
697#[derive(Debug, Clone, Copy, Serialize)]
699#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
700#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
701pub struct SandboxResources {
702 pub cpus: u8,
704
705 pub memory_mib: u32,
707
708 pub max_cpus: u8,
710
711 pub max_memory_mib: u32,
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
717#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
718#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
719#[serde(default)]
720pub struct SandboxRuntimeOptions {
721 pub workdir: Option<String>,
723
724 pub shell: Option<String>,
726
727 pub scripts: BTreeMap<String, String>,
729
730 pub entrypoint: Option<Vec<String>>,
732
733 pub cmd: Option<Vec<String>>,
735
736 pub hostname: Option<String>,
738
739 pub user: Option<String>,
741
742 pub log_level: Option<SandboxLogLevel>,
744
745 pub metrics_sample_interval_ms: Option<u64>,
747
748 pub disable_metrics_sample: bool,
750}
751
752#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
754#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
755#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
756pub struct EnvVar {
757 pub key: String,
759
760 pub value: String,
762}
763
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
766#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
767#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
768#[serde(rename_all = "lowercase")]
769pub enum SandboxLogLevel {
770 Error,
772
773 Warn,
775
776 Info,
778
779 Debug,
781
782 Trace,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
792#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
793#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
794pub enum RlimitResource {
795 Cpu,
797 Fsize,
799 Data,
801 Stack,
803 Core,
805 Rss,
807 Nproc,
809 Nofile,
811 Memlock,
813 As,
815 Locks,
817 Sigpending,
819 Msgqueue,
821 Nice,
823 Rtprio,
825 Rttime,
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
831#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
832#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
833pub struct Rlimit {
834 pub resource: RlimitResource,
836
837 pub soft: u64,
839
840 pub hard: u64,
842}
843
844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
850#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
851#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
852#[serde(rename_all = "lowercase")]
853pub enum LogSource {
854 Stdout,
856
857 Stderr,
859
860 Output,
862
863 System,
865}
866
867impl DiskImageFormat {
872 pub fn as_str(&self) -> &'static str {
874 match self {
875 Self::Qcow2 => "qcow2",
876 Self::Raw => "raw",
877 Self::Vmdk => "vmdk",
878 }
879 }
880
881 pub fn from_extension(ext: &str) -> Option<Self> {
885 match ext {
886 "qcow2" => Some(Self::Qcow2),
887 "raw" => Some(Self::Raw),
888 "vmdk" => Some(Self::Vmdk),
889 _ => None,
890 }
891 }
892}
893
894impl OciRootfsSource {
895 pub fn new(reference: impl Into<String>) -> Self {
897 Self {
898 reference: reference.into(),
899 root_disk: None,
900 }
901 }
902}
903
904impl RootDisk {
905 pub fn managed(size_mib: u32) -> Self {
907 Self::Managed {
908 size_mib: Some(size_mib),
909 }
910 }
911
912 pub fn tmpfs(size_mib: u32) -> Self {
914 Self::Tmpfs {
915 size_mib: Some(size_mib),
916 }
917 }
918
919 pub fn size_mib(&self) -> Option<u32> {
921 match self {
922 Self::Managed { size_mib } | Self::Tmpfs { size_mib } => *size_mib,
923 Self::DiskImage { .. } => None,
924 }
925 }
926
927 pub fn kind_str(&self) -> &'static str {
929 match self {
930 Self::Managed { .. } => "managed",
931 Self::Tmpfs { .. } => "tmpfs",
932 Self::DiskImage { .. } => "disk-image",
933 }
934 }
935
936 pub fn is_managed(&self) -> bool {
938 matches!(self, Self::Managed { .. })
939 }
940}
941
942impl RootfsSource {
943 pub fn oci(reference: impl Into<String>) -> Self {
945 Self::Oci(OciRootfsSource::new(reference))
946 }
947
948 pub fn oci_reference(&self) -> Option<&str> {
950 match self {
951 Self::Oci(oci) => Some(&oci.reference),
952 _ => None,
953 }
954 }
955
956 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
958 match self {
959 Self::Oci(oci) => oci.root_disk.as_ref(),
960 _ => None,
961 }
962 }
963
964 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
967 match self {
968 Self::Oci(oci) => match &oci.root_disk {
969 Some(RootDisk::Managed { size_mib }) => *size_mib,
970 Some(_) => None,
971 None => None,
972 },
973 _ => None,
974 }
975 }
976}
977
978impl EnvVar {
979 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
981 Self {
982 key: key.into(),
983 value: value.into(),
984 }
985 }
986
987 pub fn as_pair(&self) -> (&str, &str) {
989 (&self.key, &self.value)
990 }
991}
992
993impl VolumeKind {
994 pub fn as_str(self) -> &'static str {
996 match self {
997 Self::Directory => "dir",
998 Self::Disk => "disk",
999 }
1000 }
1001
1002 pub fn from_db_value(value: &str) -> Self {
1004 match value {
1005 "disk" => Self::Disk,
1006 _ => Self::Directory,
1007 }
1008 }
1009}
1010
1011impl VolumeSpec {
1012 pub fn new(name: impl Into<String>) -> Self {
1014 Self {
1015 name: name.into(),
1016 kind: VolumeKind::Directory,
1017 quota_mib: None,
1018 capacity_mib: None,
1019 labels: Vec::new(),
1020 }
1021 }
1022}
1023
1024impl NamedVolumeCreate {
1025 pub fn mode(&self) -> NamedVolumeMode {
1027 self.mode
1028 }
1029
1030 pub fn name(&self) -> &str {
1032 &self.name
1033 }
1034
1035 pub fn kind(&self) -> VolumeKind {
1037 self.kind
1038 }
1039
1040 pub fn quota_mib(&self) -> Option<u32> {
1042 self.quota_mib
1043 }
1044
1045 pub fn capacity_mib(&self) -> Option<u32> {
1047 self.capacity_mib
1048 }
1049
1050 pub fn labels(&self) -> &[(String, String)] {
1052 &self.labels
1053 }
1054}
1055
1056impl VolumeMount {
1057 pub fn guest(&self) -> &str {
1059 match self {
1060 Self::Bind { guest, .. }
1061 | Self::Named { guest, .. }
1062 | Self::Tmpfs { guest, .. }
1063 | Self::DiskImage { guest, .. } => guest,
1064 }
1065 }
1066
1067 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1069 match self {
1070 Self::Named { create, .. } => create.as_ref(),
1071 _ => None,
1072 }
1073 }
1074}
1075
1076impl RlimitResource {
1077 pub fn as_str(&self) -> &'static str {
1079 match self {
1080 Self::Cpu => "cpu",
1081 Self::Fsize => "fsize",
1082 Self::Data => "data",
1083 Self::Stack => "stack",
1084 Self::Core => "core",
1085 Self::Rss => "rss",
1086 Self::Nproc => "nproc",
1087 Self::Nofile => "nofile",
1088 Self::Memlock => "memlock",
1089 Self::As => "as",
1090 Self::Locks => "locks",
1091 Self::Sigpending => "sigpending",
1092 Self::Msgqueue => "msgqueue",
1093 Self::Nice => "nice",
1094 Self::Rtprio => "rtprio",
1095 Self::Rttime => "rttime",
1096 }
1097 }
1098}
1099
1100impl LogSource {
1101 pub fn effective(requested: &[Self]) -> Vec<Self> {
1103 if requested.is_empty() {
1104 vec![Self::Stdout, Self::Stderr, Self::Output]
1105 } else {
1106 let mut sources = requested.to_vec();
1107 sources.sort_by_key(|src| match src {
1108 Self::Stdout => 0,
1109 Self::Stderr => 1,
1110 Self::Output => 2,
1111 Self::System => 3,
1112 });
1113 sources.dedup();
1114 sources
1115 }
1116 }
1117}
1118
1119impl SandboxLogLevel {
1120 pub const fn as_str(self) -> &'static str {
1122 match self {
1123 Self::Error => "error",
1124 Self::Warn => "warn",
1125 Self::Info => "info",
1126 Self::Debug => "debug",
1127 Self::Trace => "trace",
1128 }
1129 }
1130}
1131
1132impl std::fmt::Display for DiskImageFormat {
1137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1138 f.write_str(self.as_str())
1139 }
1140}
1141
1142impl FromStr for DiskImageFormat {
1143 type Err = String;
1144
1145 fn from_str(s: &str) -> Result<Self, Self::Err> {
1146 match s {
1147 "qcow2" => Ok(Self::Qcow2),
1148 "raw" => Ok(Self::Raw),
1149 "vmdk" => Ok(Self::Vmdk),
1150 _ => Err(format!("unknown disk image format: {s}")),
1151 }
1152 }
1153}
1154
1155impl Default for RootfsSource {
1156 fn default() -> Self {
1157 Self::oci(String::new())
1158 }
1159}
1160
1161impl Default for SandboxResources {
1162 fn default() -> Self {
1163 Self {
1164 cpus: DEFAULT_SANDBOX_CPUS,
1165 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1166 max_cpus: DEFAULT_SANDBOX_CPUS,
1167 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1168 }
1169 }
1170}
1171
1172impl<'de> Deserialize<'de> for SandboxResources {
1173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1174 where
1175 D: serde::Deserializer<'de>,
1176 {
1177 #[derive(Deserialize)]
1178 struct RawResources {
1179 #[serde(default = "default_sandbox_cpus")]
1180 cpus: u8,
1181 #[serde(default = "default_sandbox_memory_mib")]
1182 memory_mib: u32,
1183 max_cpus: Option<u8>,
1184 max_memory_mib: Option<u32>,
1185 }
1186
1187 let raw = RawResources::deserialize(deserializer)?;
1188 Ok(Self {
1189 cpus: raw.cpus,
1190 memory_mib: raw.memory_mib,
1191 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1195 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1196 })
1197 }
1198}
1199
1200impl Default for SandboxRuntimeOptions {
1201 fn default() -> Self {
1202 Self {
1203 workdir: None,
1204 shell: None,
1205 scripts: BTreeMap::new(),
1206 entrypoint: None,
1207 cmd: None,
1208 hostname: None,
1209 user: None,
1210 log_level: None,
1211 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1212 disable_metrics_sample: false,
1213 }
1214 }
1215}
1216
1217impl Default for NetworkSpec {
1218 fn default() -> Self {
1219 Self {
1220 enabled: true,
1221 interface: None,
1222 ports: Vec::new(),
1223 policy: None,
1224 dns: None,
1225 tls: None,
1226 secrets: None,
1227 max_connections: None,
1228 trust_host_cas: false,
1229 }
1230 }
1231}
1232
1233impl Default for PublishedPortSpec {
1234 fn default() -> Self {
1235 Self {
1236 host_port: 0,
1237 guest_port: 0,
1238 protocol: PortProtocol::Tcp,
1239 host_bind: "127.0.0.1".into(),
1240 }
1241 }
1242}
1243
1244impl From<(String, String)> for EnvVar {
1245 fn from((key, value): (String, String)) -> Self {
1246 Self { key, value }
1247 }
1248}
1249
1250impl From<EnvVar> for (String, String) {
1251 fn from(var: EnvVar) -> Self {
1252 (var.key, var.value)
1253 }
1254}
1255
1256impl FromStr for SandboxLogLevel {
1257 type Err = String;
1258
1259 fn from_str(s: &str) -> Result<Self, Self::Err> {
1260 match s {
1261 "error" => Ok(Self::Error),
1262 "warn" => Ok(Self::Warn),
1263 "info" => Ok(Self::Info),
1264 "debug" => Ok(Self::Debug),
1265 "trace" => Ok(Self::Trace),
1266 _ => Err(format!("unknown sandbox log level: {s}")),
1267 }
1268 }
1269}
1270
1271impl Serialize for VolumeMount {
1272 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1273 use serde::ser::SerializeMap;
1274
1275 match self {
1276 Self::Bind {
1277 host,
1278 guest,
1279 options,
1280 stat_virtualization,
1281 host_permissions,
1282 follow_root_symlinks,
1283 quota_mib,
1284 } => {
1285 let mut map = serializer.serialize_map(Some(8))?;
1286 map.serialize_entry("type", "Bind")?;
1287 map.serialize_entry("host", host)?;
1288 map.serialize_entry("guest", guest)?;
1289 map.serialize_entry("options", options)?;
1290 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1291 map.serialize_entry("host_permissions", host_permissions)?;
1292 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1293 map.serialize_entry("quota_mib", quota_mib)?;
1294 map.end()
1295 }
1296 Self::Named {
1297 name,
1298 guest,
1299 create: _,
1300 options,
1301 stat_virtualization,
1302 host_permissions,
1303 follow_root_symlinks,
1304 } => {
1305 let mut map = serializer.serialize_map(Some(7))?;
1306 map.serialize_entry("type", "Named")?;
1307 map.serialize_entry("name", name)?;
1308 map.serialize_entry("guest", guest)?;
1309 map.serialize_entry("options", options)?;
1310 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1311 map.serialize_entry("host_permissions", host_permissions)?;
1312 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1313 map.end()
1314 }
1315 Self::Tmpfs {
1316 guest,
1317 size_mib,
1318 options,
1319 } => {
1320 let mut map = serializer.serialize_map(Some(4))?;
1321 map.serialize_entry("type", "Tmpfs")?;
1322 map.serialize_entry("guest", guest)?;
1323 map.serialize_entry("size_mib", size_mib)?;
1324 map.serialize_entry("options", options)?;
1325 map.end()
1326 }
1327 Self::DiskImage {
1328 host,
1329 guest,
1330 format,
1331 fstype,
1332 options,
1333 } => {
1334 let mut map = serializer.serialize_map(Some(6))?;
1335 map.serialize_entry("type", "DiskImage")?;
1336 map.serialize_entry("host", host)?;
1337 map.serialize_entry("guest", guest)?;
1338 map.serialize_entry("format", format)?;
1339 map.serialize_entry("fstype", fstype)?;
1340 map.serialize_entry("options", options)?;
1341 map.end()
1342 }
1343 }
1344 }
1345}
1346
1347impl<'de> Deserialize<'de> for VolumeMount {
1348 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1349 fn default_strict() -> StatVirtualization {
1350 StatVirtualization::Strict
1351 }
1352
1353 fn default_private() -> HostPermissions {
1354 HostPermissions::Private
1355 }
1356
1357 #[derive(Deserialize)]
1358 #[serde(tag = "type")]
1359 enum VolumeMountHelper {
1360 Bind {
1361 host: PathBuf,
1362 guest: String,
1363 #[serde(default)]
1364 options: Option<MountOptions>,
1365 #[serde(default)]
1366 readonly: bool,
1367 #[serde(default = "default_strict")]
1368 stat_virtualization: StatVirtualization,
1369 #[serde(default = "default_private")]
1370 host_permissions: HostPermissions,
1371 #[serde(default)]
1372 follow_root_symlinks: bool,
1373 #[serde(default)]
1374 quota_mib: Option<u32>,
1375 },
1376 Named {
1377 name: String,
1378 guest: String,
1379 #[serde(default)]
1380 options: Option<MountOptions>,
1381 #[serde(default)]
1382 readonly: bool,
1383 #[serde(default = "default_strict")]
1384 stat_virtualization: StatVirtualization,
1385 #[serde(default = "default_private")]
1386 host_permissions: HostPermissions,
1387 #[serde(default)]
1388 follow_root_symlinks: bool,
1389 },
1390 Tmpfs {
1391 guest: String,
1392 #[serde(default)]
1393 size_mib: Option<u32>,
1394 #[serde(default)]
1395 options: Option<MountOptions>,
1396 #[serde(default)]
1397 readonly: bool,
1398 },
1399 DiskImage {
1400 host: PathBuf,
1401 guest: String,
1402 format: DiskImageFormat,
1403 #[serde(default)]
1404 fstype: Option<String>,
1405 #[serde(default)]
1406 options: Option<MountOptions>,
1407 #[serde(default)]
1408 readonly: bool,
1409 },
1410 }
1411
1412 let helper = VolumeMountHelper::deserialize(deserializer)?;
1413 Ok(match helper {
1414 VolumeMountHelper::Bind {
1415 host,
1416 guest,
1417 options,
1418 readonly,
1419 stat_virtualization,
1420 host_permissions,
1421 follow_root_symlinks,
1422 quota_mib,
1423 } => Self::Bind {
1424 host,
1425 guest,
1426 options: decode_mount_options(options, readonly),
1427 stat_virtualization,
1428 host_permissions,
1429 follow_root_symlinks,
1430 quota_mib,
1431 },
1432 VolumeMountHelper::Named {
1433 name,
1434 guest,
1435 options,
1436 readonly,
1437 stat_virtualization,
1438 host_permissions,
1439 follow_root_symlinks,
1440 } => Self::Named {
1441 name,
1442 guest,
1443 create: None,
1444 options: decode_mount_options(options, readonly),
1445 stat_virtualization,
1446 host_permissions,
1447 follow_root_symlinks,
1448 },
1449 VolumeMountHelper::Tmpfs {
1450 guest,
1451 size_mib,
1452 options,
1453 readonly,
1454 } => Self::Tmpfs {
1455 guest,
1456 size_mib,
1457 options: decode_mount_options(options, readonly),
1458 },
1459 VolumeMountHelper::DiskImage {
1460 host,
1461 guest,
1462 format,
1463 fstype,
1464 options,
1465 readonly,
1466 } => Self::DiskImage {
1467 host,
1468 guest,
1469 format,
1470 fstype,
1471 options: decode_mount_options(options, readonly),
1472 },
1473 })
1474 }
1475}
1476
1477impl fmt::Debug for VolumeMount {
1478 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1479 match self {
1480 Self::Bind {
1481 host,
1482 guest,
1483 options,
1484 stat_virtualization,
1485 host_permissions,
1486 follow_root_symlinks,
1487 quota_mib,
1488 } => f
1489 .debug_struct("Bind")
1490 .field("host", host)
1491 .field("guest", guest)
1492 .field("options", options)
1493 .field("stat_virtualization", stat_virtualization)
1494 .field("host_permissions", host_permissions)
1495 .field("follow_root_symlinks", follow_root_symlinks)
1496 .field("quota_mib", quota_mib)
1497 .finish(),
1498 Self::Named {
1499 name,
1500 guest,
1501 create,
1502 options,
1503 stat_virtualization,
1504 host_permissions,
1505 follow_root_symlinks,
1506 } => f
1507 .debug_struct("Named")
1508 .field("name", name)
1509 .field("guest", guest)
1510 .field("create", create)
1511 .field("options", options)
1512 .field("stat_virtualization", stat_virtualization)
1513 .field("host_permissions", host_permissions)
1514 .field("follow_root_symlinks", follow_root_symlinks)
1515 .finish(),
1516 Self::Tmpfs {
1517 guest,
1518 size_mib,
1519 options,
1520 } => f
1521 .debug_struct("Tmpfs")
1522 .field("guest", guest)
1523 .field("size_mib", size_mib)
1524 .field("options", options)
1525 .finish(),
1526 Self::DiskImage {
1527 host,
1528 guest,
1529 format,
1530 fstype,
1531 options,
1532 } => f
1533 .debug_struct("DiskImage")
1534 .field("host", host)
1535 .field("guest", guest)
1536 .field("format", format)
1537 .field("fstype", fstype)
1538 .field("options", options)
1539 .finish(),
1540 }
1541 }
1542}
1543
1544impl TryFrom<&str> for RlimitResource {
1546 type Error = String;
1547
1548 fn try_from(s: &str) -> Result<Self, Self::Error> {
1549 match s.to_ascii_lowercase().as_str() {
1550 "cpu" => Ok(Self::Cpu),
1551 "fsize" => Ok(Self::Fsize),
1552 "data" => Ok(Self::Data),
1553 "stack" => Ok(Self::Stack),
1554 "core" => Ok(Self::Core),
1555 "rss" => Ok(Self::Rss),
1556 "nproc" => Ok(Self::Nproc),
1557 "nofile" => Ok(Self::Nofile),
1558 "memlock" => Ok(Self::Memlock),
1559 "as" => Ok(Self::As),
1560 "locks" => Ok(Self::Locks),
1561 "sigpending" => Ok(Self::Sigpending),
1562 "msgqueue" => Ok(Self::Msgqueue),
1563 "nice" => Ok(Self::Nice),
1564 "rtprio" => Ok(Self::Rtprio),
1565 "rttime" => Ok(Self::Rttime),
1566 _ => Err(format!("unknown rlimit resource: {s}")),
1567 }
1568 }
1569}
1570
1571fn default_sandbox_cpus() -> u8 {
1576 DEFAULT_SANDBOX_CPUS
1577}
1578
1579fn default_sandbox_memory_mib() -> u32 {
1580 DEFAULT_SANDBOX_MEMORY_MIB
1581}
1582
1583fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
1584 options.unwrap_or(MountOptions {
1585 readonly,
1586 ..MountOptions::default()
1587 })
1588}
1589
1590pub(crate) fn default_strict() -> StatVirtualization {
1592 StatVirtualization::Strict
1593}
1594
1595pub(crate) fn default_private() -> HostPermissions {
1597 HostPermissions::Private
1598}
1599
1600pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
1602
1603#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1610#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1611#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1612pub struct SecretsConfig {
1613 #[serde(default)]
1615 pub secrets: Vec<SecretEntry>,
1616
1617 #[serde(default)]
1619 pub on_violation: ViolationAction,
1620}
1621
1622#[derive(Clone, Serialize, Deserialize)]
1627#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1628#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1629pub struct SecretEntry {
1630 pub env_var: String,
1636
1637 #[serde(default = "empty_secret_value")]
1646 #[cfg_attr(feature = "ts", ts(type = "string"))]
1647 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1648 pub value: Zeroizing<String>,
1649
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1654 pub source: Option<SecretSource>,
1655
1656 pub placeholder: String,
1661
1662 #[serde(default)]
1664 pub allowed_hosts: Vec<HostPattern>,
1665
1666 #[serde(default)]
1668 pub injection: SecretInjection,
1669
1670 #[serde(default, skip_serializing_if = "Option::is_none")]
1672 pub on_violation: Option<ViolationAction>,
1673
1674 #[serde(default = "default_true")]
1679 pub require_tls_identity: bool,
1680}
1681
1682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1684#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1685#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1686#[serde(rename_all = "kebab-case")]
1687pub enum HostPattern {
1688 #[serde(alias = "Exact")]
1690 Exact(String),
1691 #[serde(alias = "Wildcard")]
1693 Wildcard(String),
1694 #[serde(alias = "Any")]
1696 Any,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize)]
1701#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1702#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1703pub struct SecretInjection {
1704 #[serde(default = "default_true")]
1706 pub headers: bool,
1707
1708 #[serde(default = "default_true")]
1710 pub basic_auth: bool,
1711
1712 #[serde(default)]
1714 pub query_params: bool,
1715
1716 #[serde(default)]
1724 pub body: bool,
1725}
1726
1727#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1729#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1730#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1731#[serde(rename_all = "kebab-case")]
1732pub enum ViolationAction {
1733 #[serde(alias = "Block")]
1735 Block,
1736 #[default]
1738 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
1739 BlockAndLog,
1740 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
1742 BlockAndTerminate,
1743 #[serde(alias = "Passthrough")]
1745 Passthrough(Vec<HostPattern>),
1746}
1747
1748#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1750pub enum SecretConfigError {
1751 #[error("secret #{secret_index}: env_var must not be empty")]
1753 EmptyEnvVar {
1754 secret_index: usize,
1756 },
1757
1758 #[error("secret #{secret_index}: env_var must not contain `=`")]
1760 EnvVarContainsEquals {
1761 secret_index: usize,
1763 },
1764
1765 #[error("secret #{secret_index}: env_var must not contain NUL")]
1767 EnvVarContainsNul {
1768 secret_index: usize,
1770 },
1771
1772 #[error("secret #{secret_index}: at least one allowed host is required")]
1774 MissingAllowedHosts {
1775 secret_index: usize,
1777 },
1778
1779 #[error("secret #{secret_index}: placeholder must not be empty")]
1781 EmptyPlaceholder {
1782 secret_index: usize,
1784 },
1785
1786 #[error(
1788 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
1789 )]
1790 PlaceholderTooLong {
1791 secret_index: usize,
1793 actual_bytes: usize,
1795 max_bytes: usize,
1797 },
1798
1799 #[error("secret #{secret_index}: placeholder must not contain NUL")]
1801 PlaceholderContainsNul {
1802 secret_index: usize,
1804 },
1805
1806 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
1808 PlaceholderContainsLineBreak {
1809 secret_index: usize,
1811 },
1812}
1813
1814impl SecretsConfig {
1815 pub fn validate(&self) -> Result<(), SecretConfigError> {
1817 for (index, secret) in self.secrets.iter().enumerate() {
1818 secret.validate(index)?;
1819 }
1820 Ok(())
1821 }
1822}
1823
1824impl SecretEntry {
1825 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
1827 validate_env_var(&self.env_var, secret_index)?;
1828
1829 if self.allowed_hosts.is_empty() {
1830 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
1831 }
1832
1833 validate_placeholder(&self.placeholder, secret_index)
1834 }
1835}
1836
1837impl fmt::Debug for SecretEntry {
1839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1840 f.debug_struct("SecretEntry")
1841 .field("env_var", &self.env_var)
1842 .field("value", &"[REDACTED]")
1843 .field("source", &self.source)
1844 .field("placeholder", &self.placeholder)
1845 .field("allowed_hosts", &self.allowed_hosts)
1846 .field("injection", &self.injection)
1847 .field("on_violation", &self.on_violation)
1848 .field("require_tls_identity", &self.require_tls_identity)
1849 .finish()
1850 }
1851}
1852
1853impl HostPattern {
1854 pub fn parse(host: &str) -> Self {
1857 if host == "*" {
1858 HostPattern::Any
1859 } else if host.starts_with("*.") {
1860 HostPattern::Wildcard(host.to_string())
1861 } else {
1862 HostPattern::Exact(host.to_string())
1863 }
1864 }
1865
1866 pub fn matches(&self, hostname: &str) -> bool {
1871 match self {
1872 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
1873 HostPattern::Wildcard(pattern) => {
1874 if let Some(suffix) = pattern.strip_prefix("*.") {
1875 hostname.eq_ignore_ascii_case(suffix)
1876 || (hostname.len() > suffix.len() + 1
1877 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
1878 && hostname[hostname.len() - suffix.len()..]
1879 .eq_ignore_ascii_case(suffix))
1880 } else {
1881 hostname.eq_ignore_ascii_case(pattern)
1882 }
1883 }
1884 HostPattern::Any => true,
1885 }
1886 }
1887}
1888
1889impl Default for SecretInjection {
1890 fn default() -> Self {
1891 Self {
1892 headers: true,
1893 basic_auth: true,
1894 query_params: false,
1895 body: false,
1896 }
1897 }
1898}
1899
1900fn default_true() -> bool {
1901 true
1902}
1903
1904fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
1905 if env_var.is_empty() {
1906 return Err(SecretConfigError::EmptyEnvVar { secret_index });
1907 }
1908 if env_var.contains('=') {
1909 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
1910 }
1911 if env_var.contains('\0') {
1912 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
1913 }
1914 Ok(())
1915}
1916
1917fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
1918 if placeholder.is_empty() {
1919 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
1920 }
1921
1922 let actual_bytes = placeholder.len();
1923 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
1924 return Err(SecretConfigError::PlaceholderTooLong {
1925 secret_index,
1926 actual_bytes,
1927 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
1928 });
1929 }
1930
1931 if placeholder.contains('\0') {
1932 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
1933 }
1934 if placeholder.contains('\r') || placeholder.contains('\n') {
1935 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
1936 }
1937
1938 Ok(())
1939}
1940
1941#[derive(Debug, Clone, Serialize, Deserialize)]
1951#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1952#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1953pub struct TlsConfig {
1954 #[serde(default)]
1956 pub enabled: bool,
1957
1958 #[serde(default = "default_intercepted_ports")]
1960 pub intercepted_ports: Vec<u16>,
1961
1962 #[serde(default)]
1964 pub bypass: Vec<String>,
1965
1966 #[serde(default = "default_true")]
1968 pub verify_upstream: bool,
1969
1970 #[serde(default = "default_true")]
1973 pub block_quic_on_intercept: bool,
1974
1975 #[serde(default)]
1977 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
1978 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
1979 pub upstream_ca_cert: Vec<PathBuf>,
1980
1981 #[serde(default, alias = "scoped_upstream_ca_certs")]
1983 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
1984
1985 #[serde(default)]
1987 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
1988
1989 #[serde(default, alias = "ca")]
1992 pub intercept_ca: InterceptCaConfig,
1993
1994 #[serde(default)]
1996 pub cache: CertCacheConfig,
1997}
1998
1999#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2001#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2002#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2003pub struct InterceptCaConfig {
2004 #[serde(default)]
2007 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2008 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2009 pub cert_path: Option<PathBuf>,
2010
2011 #[serde(default)]
2014 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2015 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2016 pub key_path: Option<PathBuf>,
2017}
2018
2019#[derive(Debug, Clone, Serialize, Deserialize)]
2021#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2022#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2023pub struct CertCacheConfig {
2024 #[serde(default = "default_cache_capacity")]
2026 pub capacity: usize,
2027
2028 #[serde(default = "default_cert_validity_hours")]
2030 pub validity_hours: u64,
2031}
2032
2033#[derive(Debug, Clone, Serialize, Deserialize)]
2035#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2036#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2037pub struct ScopedUpstreamCaCert {
2038 pub pattern: String,
2040
2041 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2043 #[cfg_attr(feature = "ts", ts(type = "string"))]
2044 pub path: PathBuf,
2045}
2046
2047#[derive(Debug, Clone, Serialize, Deserialize)]
2049#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2050#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2051pub struct ScopedVerifyUpstream {
2052 pub pattern: String,
2054
2055 pub verify: bool,
2057}
2058
2059impl Default for TlsConfig {
2060 fn default() -> Self {
2061 Self {
2062 enabled: false,
2063 intercepted_ports: default_intercepted_ports(),
2064 bypass: Vec::new(),
2065 verify_upstream: true,
2066 block_quic_on_intercept: true,
2067 upstream_ca_cert: Vec::new(),
2068 scoped_upstream_ca_cert: Vec::new(),
2069 scoped_verify_upstream: Vec::new(),
2070 intercept_ca: InterceptCaConfig::default(),
2071 cache: CertCacheConfig::default(),
2072 }
2073 }
2074}
2075
2076impl Default for CertCacheConfig {
2077 fn default() -> Self {
2078 Self {
2079 capacity: default_cache_capacity(),
2080 validity_hours: default_cert_validity_hours(),
2081 }
2082 }
2083}
2084
2085fn default_intercepted_ports() -> Vec<u16> {
2086 vec![443]
2087}
2088
2089fn default_cache_capacity() -> usize {
2090 1000
2091}
2092
2093fn default_cert_validity_hours() -> u64 {
2094 24
2095}
2096
2097#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2103#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2104#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2105#[serde(rename_all = "snake_case")]
2106pub enum Action {
2107 Allow,
2109 Deny,
2111}
2112
2113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2115#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2116#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2117#[serde(rename_all = "snake_case")]
2118pub enum Direction {
2119 Egress,
2121 Ingress,
2123 Any,
2125}
2126
2127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2129#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2130#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2131#[serde(rename_all = "snake_case")]
2132pub enum Protocol {
2133 Tcp,
2135 Udp,
2137 Icmpv4,
2139 Icmpv6,
2141}
2142
2143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2145#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2146#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2147#[serde(rename_all = "snake_case")]
2148pub enum DestinationGroup {
2149 Public,
2151 Loopback,
2153 Private,
2155 LinkLocal,
2157 Metadata,
2159 Multicast,
2161 Host,
2163}
2164
2165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2172#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2173#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2174#[serde(rename_all = "snake_case")]
2175pub enum Destination {
2176 Any,
2178 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2180 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2181 Domain(String),
2183 DomainSuffix(String),
2185 Group(DestinationGroup),
2187}
2188
2189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2191#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2192#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2193pub struct PortRange {
2194 pub start: u16,
2196 pub end: u16,
2198}
2199
2200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2203#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2204#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2205pub struct Rule {
2206 pub direction: Direction,
2208 pub destination: Destination,
2210 #[serde(default)]
2212 pub protocols: Vec<Protocol>,
2213 #[serde(default)]
2215 pub ports: Vec<PortRange>,
2216 pub action: Action,
2218}
2219
2220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2223#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2224#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2225pub struct NetworkPolicy {
2226 #[serde(default = "action_deny")]
2228 pub default_egress: Action,
2229 #[serde(default = "action_deny")]
2231 pub default_ingress: Action,
2232 #[serde(default)]
2234 pub rules: Vec<Rule>,
2235}
2236
2237fn action_deny() -> Action {
2240 Action::Deny
2241}
2242
2243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2249#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2250#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2251#[serde(default)]
2252pub struct DnsConfig {
2253 pub rebind_protection: bool,
2255 pub nameservers: Vec<String>,
2258 pub query_timeout_ms: u64,
2260}
2261
2262impl Default for DnsConfig {
2263 fn default() -> Self {
2264 Self {
2265 rebind_protection: true,
2266 nameservers: Vec::new(),
2267 query_timeout_ms: 5000,
2268 }
2269 }
2270}
2271
2272#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2276#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2277#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2278#[serde(default)]
2279pub struct InterfaceOverrides {
2280 #[serde(skip_serializing_if = "Option::is_none")]
2282 pub mac: Option<[u8; 6]>,
2283 #[serde(skip_serializing_if = "Option::is_none")]
2285 pub mtu: Option<u16>,
2286 #[serde(skip_serializing_if = "Option::is_none")]
2288 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2289 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2290 pub ipv4_address: Option<Ipv4Addr>,
2291 #[serde(skip_serializing_if = "Option::is_none")]
2293 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2294 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2295 pub ipv4_pool: Option<Ipv4Network>,
2296 #[serde(skip_serializing_if = "Option::is_none")]
2298 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2299 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2300 pub ipv6_address: Option<Ipv6Addr>,
2301 #[serde(skip_serializing_if = "Option::is_none")]
2303 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2304 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2305 pub ipv6_pool: Option<Ipv6Network>,
2306}
2307
2308fn empty_secret_value() -> Zeroizing<String> {
2309 Zeroizing::new(String::new())
2310}
2311
2312#[cfg(test)]
2317mod tests {
2318 use super::*;
2319
2320 #[test]
2321 fn disk_image_format_from_extension() {
2322 assert_eq!(
2323 DiskImageFormat::from_extension("qcow2"),
2324 Some(DiskImageFormat::Qcow2)
2325 );
2326 assert_eq!(
2327 DiskImageFormat::from_extension("raw"),
2328 Some(DiskImageFormat::Raw)
2329 );
2330 assert_eq!(
2331 DiskImageFormat::from_extension("vmdk"),
2332 Some(DiskImageFormat::Vmdk)
2333 );
2334 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
2335 assert_eq!(DiskImageFormat::from_extension(""), None);
2336 }
2337
2338 #[test]
2339 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
2340 let resources: SandboxResources =
2341 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
2342
2343 assert_eq!(resources.cpus, 4);
2344 assert_eq!(resources.max_cpus, 4);
2345 assert_eq!(resources.memory_mib, 2048);
2346 assert_eq!(resources.max_memory_mib, 2048);
2347 }
2348
2349 #[test]
2350 fn disk_image_format_display_roundtrip() {
2351 for format in [
2352 DiskImageFormat::Qcow2,
2353 DiskImageFormat::Raw,
2354 DiskImageFormat::Vmdk,
2355 ] {
2356 let rendered = format.to_string();
2357 let parsed: DiskImageFormat = rendered.parse().unwrap();
2358 assert_eq!(parsed, format);
2359 }
2360 }
2361
2362 #[test]
2363 fn disk_image_format_from_str_unknown() {
2364 assert!("ext4".parse::<DiskImageFormat>().is_err());
2365 }
2366
2367 #[test]
2368 fn log_source_effective_uses_default_user_program_sources() {
2369 assert_eq!(
2370 LogSource::effective(&[]),
2371 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
2372 );
2373 }
2374
2375 #[test]
2376 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
2377 assert_eq!(
2378 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
2379 vec![LogSource::Stdout, LogSource::System]
2380 );
2381 }
2382
2383 #[test]
2384 fn rlimit_resource_parses_case_insensitively() {
2385 assert_eq!(
2386 RlimitResource::try_from("NOFILE").unwrap(),
2387 RlimitResource::Nofile
2388 );
2389 assert!(RlimitResource::try_from("bogus").is_err());
2390 }
2391
2392 #[test]
2393 fn sandbox_policy_serde_roundtrip() {
2394 let policy = SandboxPolicy {
2395 ephemeral: true,
2396 max_duration_secs: Some(3600),
2397 idle_timeout_secs: Some(120),
2398 };
2399
2400 let json = serde_json::to_string(&policy).unwrap();
2401 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
2402
2403 assert!(decoded.ephemeral);
2404 assert_eq!(decoded.max_duration_secs, Some(3600));
2405 assert_eq!(decoded.idle_timeout_secs, Some(120));
2406 }
2407
2408 #[test]
2409 fn sandbox_policy_defaults_to_persistent() {
2410 assert!(!SandboxPolicy::default().ephemeral);
2411 }
2412
2413 #[test]
2414 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
2415 let decoded: SandboxPolicy =
2418 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
2419 assert!(!decoded.ephemeral);
2420 assert_eq!(decoded.max_duration_secs, Some(60));
2421 }
2422
2423 #[test]
2424 fn sandbox_spec_default_uses_static_resource_defaults() {
2425 let spec = SandboxSpec::default();
2426
2427 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
2428 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
2429 assert_eq!(
2430 spec.runtime.metrics_sample_interval_ms,
2431 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
2432 );
2433 }
2434
2435 #[test]
2436 fn sandbox_log_level_roundtrips_lowercase_values() {
2437 for (input, expected) in [
2438 ("error", SandboxLogLevel::Error),
2439 ("warn", SandboxLogLevel::Warn),
2440 ("info", SandboxLogLevel::Info),
2441 ("debug", SandboxLogLevel::Debug),
2442 ("trace", SandboxLogLevel::Trace),
2443 ] {
2444 let parsed: SandboxLogLevel = input.parse().unwrap();
2445 assert_eq!(parsed, expected);
2446 assert_eq!(parsed.as_str(), input);
2447 }
2448 }
2449}