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 microsandbox_types_macros::ConfigPatch;
11use serde::{Deserialize, Serialize};
12use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath};
13use zeroize::Zeroizing;
14
15use crate::modify::SecretSource;
16use crate::{TypesError, TypesResult};
17
18pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
24
25pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
27
28pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
38#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
39pub enum DiskImageFormat {
40 Qcow2,
42 Raw,
44 Vmdk,
46}
47
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
51#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
52#[serde(rename_all = "kebab-case")]
53pub enum FlatClone {
54 #[default]
56 Auto,
57
58 Copy,
60
61 Reflink,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
68pub enum RootfsSource {
69 Bind {
71 #[cfg_attr(feature = "ts", ts(type = "string"))]
73 path: PathBuf,
74 #[serde(default)]
81 follow_root_symlinks: bool,
82 },
83
84 Oci(OciRootfsSource),
86
87 DiskImage {
89 #[cfg_attr(feature = "ts", ts(type = "string"))]
91 path: PathBuf,
92 format: DiskImageFormat,
94 fstype: Option<String>,
96 },
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
102#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
103pub struct OciRootfsSource {
104 pub reference: String,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub root_disk: Option<RootDisk>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
119#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
120#[serde(tag = "kind", rename_all = "kebab-case")]
121pub enum RootDisk {
122 Managed {
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 size_mib: Option<u32>,
128 },
129
130 Tmpfs {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 size_mib: Option<u32>,
136 },
137
138 DiskImage {
141 #[cfg_attr(feature = "ts", ts(type = "string"))]
143 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
144 path: PathBuf,
145 format: DiskImageFormat,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 fstype: Option<String>,
150 },
151
152 Flat {
157 #[serde(default, skip_serializing_if = "Option::is_none")]
160 size_mib: Option<u32>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 fstype: Option<String>,
164 #[serde(default, skip_serializing_if = "FlatClone::is_auto")]
166 clone: FlatClone,
167 },
168}
169
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
172#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
173#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
174pub enum PullPolicy {
175 #[default]
177 IfMissing,
178
179 Always,
181
182 Never,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
195#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
196#[serde(rename_all = "lowercase")]
197pub enum StatVirtualization {
198 Strict,
200 Relaxed,
202 Off,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
211#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
212#[serde(rename_all = "lowercase")]
213pub enum HostPermissions {
214 Private,
216 Mirror,
218}
219
220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
222#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
223#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
224#[serde(rename_all = "lowercase")]
225pub enum SecurityProfile {
226 #[default]
230 Default,
231
232 Restricted,
236}
237
238#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
244#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
245#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
246#[serde(rename_all = "snake_case")]
247pub enum DeploymentProfile {
248 #[default]
250 SingleTenant,
251
252 MultiTenant,
254}
255
256#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
258#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
259#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
260#[serde(default)]
261pub struct MountOptions {
262 pub readonly: bool,
266
267 pub noexec: bool,
271
272 pub nosuid: bool,
274
275 pub nodev: bool,
277
278 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub override_uid: Option<u32>,
287
288 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub override_gid: Option<u32>,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
299pub enum VolumeKind {
300 Directory,
302
303 Disk,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
310#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
311pub struct VolumeSpec {
312 pub name: String,
314
315 pub kind: VolumeKind,
317
318 pub quota_mib: Option<u32>,
320
321 pub capacity_mib: Option<u32>,
323
324 pub labels: Vec<(String, String)>,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
331#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
332pub enum NamedVolumeMode {
333 Existing,
335
336 Create,
338
339 EnsureExists,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
346#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
347pub struct NamedVolumeCreate {
348 pub mode: NamedVolumeMode,
350
351 pub name: String,
353
354 pub kind: VolumeKind,
356
357 pub quota_mib: Option<u32>,
359
360 pub capacity_mib: Option<u32>,
362
363 pub labels: Vec<(String, String)>,
365}
366
367#[derive(Clone)]
369#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
370#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
371#[cfg_attr(feature = "ts", ts(tag = "type"))]
372pub enum VolumeMount {
373 Bind {
375 #[cfg_attr(feature = "ts", ts(type = "string"))]
377 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
378 host: PathBuf,
379 guest: String,
381 options: MountOptions,
383 stat_virtualization: StatVirtualization,
385 host_permissions: HostPermissions,
387 follow_root_symlinks: bool,
394 quota_mib: Option<u32>,
400 },
401
402 Named {
404 name: String,
406 guest: String,
408 create: Option<NamedVolumeCreate>,
412 options: MountOptions,
414 stat_virtualization: StatVirtualization,
416 host_permissions: HostPermissions,
418 follow_root_symlinks: bool,
423 },
424
425 Tmpfs {
427 guest: String,
429 size_mib: Option<u32>,
431 options: MountOptions,
433 },
434
435 DiskImage {
437 #[cfg_attr(feature = "ts", ts(type = "string"))]
439 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
440 host: PathBuf,
441 guest: String,
443 format: DiskImageFormat,
445 fstype: Option<String>,
447 options: MountOptions,
449 },
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
454#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
455#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
456pub enum Patch {
457 Text {
459 path: String,
461 content: String,
463 mode: Option<u32>,
465 replace: bool,
467 },
468
469 File {
471 path: String,
473 content: Vec<u8>,
475 mode: Option<u32>,
477 replace: bool,
479 },
480
481 CopyFile {
483 #[cfg_attr(feature = "ts", ts(type = "string"))]
485 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
486 src: PathBuf,
487 dst: String,
489 mode: Option<u32>,
491 replace: bool,
493 },
494
495 CopyDir {
497 #[cfg_attr(feature = "ts", ts(type = "string"))]
499 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
500 src: PathBuf,
501 dst: String,
503 replace: bool,
505 },
506
507 Symlink {
509 target: String,
511 link: String,
513 replace: bool,
515 },
516
517 Mkdir {
519 path: String,
521 mode: Option<u32>,
523 },
524
525 Remove {
527 path: String,
529 },
530
531 Append {
533 path: String,
535 content: String,
537 },
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
548#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
549#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
550#[serde(default)]
551pub struct NetworkSpec {
552 pub enabled: bool,
554
555 #[serde(skip_serializing_if = "Option::is_none")]
557 #[config_patch(nested)]
558 pub interface: Option<InterfaceOverrides>,
559
560 pub ports: Vec<PublishedPortSpec>,
562
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub policy: Option<NetworkPolicy>,
566
567 #[serde(skip_serializing_if = "Option::is_none")]
569 #[config_patch(nested)]
570 pub dns: Option<DnsConfig>,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
574 #[config_patch(nested)]
575 pub tls: Option<TlsConfig>,
576
577 #[serde(skip_serializing_if = "Option::is_none")]
579 #[config_patch(nested)]
580 pub secrets: Option<SecretsConfig>,
581
582 pub max_connections: Option<usize>,
584
585 #[serde(skip_serializing_if = "Option::is_none")]
587 #[config_patch(nested)]
588 pub rate_limiter: Option<NetworkRateLimiterConfig>,
589
590 pub trust_host_cas: bool,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
597#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
598pub struct PublishedPortSpec {
599 pub host_port: u16,
601
602 pub guest_port: u16,
604
605 #[serde(default)]
607 pub protocol: PortProtocol,
608
609 pub host_bind: String,
611}
612
613#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
615#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
616#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
617pub enum PortProtocol {
618 #[default]
620 #[serde(rename = "tcp")]
621 Tcp,
622
623 #[serde(rename = "udp")]
625 Udp,
626}
627
628#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
634#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
635#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
636#[serde(default)]
637pub struct VsockSpec {
638 pub routes: Vec<VsockRouteSpec>,
640}
641
642impl VsockSpec {
643 pub fn is_empty(&self) -> bool {
645 self.routes.is_empty()
646 }
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
651#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
652#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
653pub struct VsockRouteSpec {
654 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
656 pub host_socket: PathBuf,
657
658 pub port: u32,
660
661 #[serde(default)]
663 pub socket_type: VsockSocketType,
664}
665
666#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
668#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
669#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
670#[serde(rename_all = "snake_case")]
671pub enum VsockSocketType {
672 #[default]
674 Stream,
675
676 Dgram,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
686#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
687#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
688pub struct HandoffInit {
689 pub cmd: String,
693
694 #[serde(default)]
696 pub args: Vec<String>,
697
698 #[serde(default)]
700 pub env: Vec<(String, String)>,
701}
702
703#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
709#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
710#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
711pub struct SandboxPolicy {
712 #[serde(default)]
721 pub ephemeral: bool,
722
723 pub max_duration_secs: Option<u64>,
725
726 pub idle_timeout_secs: Option<u64>,
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize)]
741#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
742pub struct SnapshotSpec {
743 pub name: String,
745
746 #[serde(default)]
749 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
750 pub dest_dir: Option<PathBuf>,
751
752 pub source_sandbox: String,
754
755 pub labels: Vec<(String, String)>,
757
758 pub force: bool,
760
761 pub record_integrity: bool,
763
764 #[serde(default)]
770 pub resumable: bool,
771}
772
773#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
781#[config_patch(name = SandboxConfigPatch)]
782#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
783#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
784#[serde(default)]
785pub struct SandboxSpec {
786 pub name: String,
788
789 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
791 pub image: RootfsSource,
792
793 #[config_patch(nested)]
795 pub resources: SandboxResources,
796
797 #[config_patch(nested)]
799 pub runtime: SandboxRuntimeOptions,
800
801 #[config_patch(merge_with = merge_env_vars)]
803 pub env: Vec<EnvVar>,
804
805 #[config_patch(merge)]
807 pub labels: BTreeMap<String, String>,
808
809 pub rlimits: Vec<Rlimit>,
811
812 pub mounts: Vec<VolumeMount>,
814
815 pub patches: Vec<Patch>,
817
818 #[config_patch(nested)]
820 pub network: NetworkSpec,
821
822 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
824 #[config_patch(nested)]
825 pub vsock: VsockSpec,
826
827 pub init: Option<HandoffInit>,
829
830 pub pull_policy: PullPolicy,
832
833 pub security_profile: SecurityProfile,
835
836 pub deployment_profile: DeploymentProfile,
842
843 #[config_patch(nested)]
845 pub lifecycle: SandboxPolicy,
846}
847
848#[derive(Debug, Clone, Serialize, ConfigPatch)]
850#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
851#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
852pub struct SandboxResources {
853 pub cpus: u8,
855
856 pub memory_mib: u32,
858
859 pub max_cpus: u8,
861
862 pub max_memory_mib: u32,
864
865 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
867 pub cpu_placement: CpuPlacement,
868
869 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub placement_profile: Option<String>,
872
873 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
875 pub thp: TransparentHugePagePolicy,
876}
877
878#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
880#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
881#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
882#[serde(rename_all = "lowercase")]
883pub enum CpuPlacement {
884 #[default]
886 Inherit,
887
888 Auto,
890
891 Spread,
893
894 Compact,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
900#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
901#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
902#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
903pub enum NumaPlacement {
904 PreferSingle,
906 StrictSingle,
908 Inherit,
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
914#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
915#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
916#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
917pub enum MemoryPlacement {
918 FollowCpu,
920 Inherit,
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
926#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
927#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
928#[serde(deny_unknown_fields)]
929pub struct PlacementProfile {
930 pub numa: NumaPlacement,
932 pub memory: MemoryPlacement,
934}
935
936#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
938#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
939#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
940#[serde(rename_all = "lowercase")]
941pub enum TransparentHugePagePolicy {
942 Always,
944
945 #[default]
947 Madvise,
948
949 Never,
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
955#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
956#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
957#[serde(default)]
958pub struct SandboxRuntimeOptions {
959 pub workdir: Option<String>,
961
962 pub shell: Option<String>,
964
965 #[config_patch(merge)]
967 pub scripts: BTreeMap<String, String>,
968
969 pub entrypoint: Option<Vec<String>>,
971
972 pub cmd: Option<Vec<String>>,
974
975 pub hostname: Option<String>,
977
978 pub user: Option<String>,
980
981 pub log_level: Option<SandboxLogLevel>,
983
984 pub metrics_sample_interval_ms: Option<u64>,
986
987 pub disable_metrics_sample: bool,
989}
990
991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
993#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
994#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
995pub struct EnvVar {
996 pub key: String,
998
999 pub value: String,
1001}
1002
1003#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1005#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1006#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1007#[serde(rename_all = "lowercase")]
1008pub enum SandboxLogLevel {
1009 Error,
1011
1012 Warn,
1014
1015 Info,
1017
1018 Debug,
1020
1021 Trace,
1023}
1024
1025#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1031#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1032#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1033pub enum RlimitResource {
1034 Cpu,
1036 Fsize,
1038 Data,
1040 Stack,
1042 Core,
1044 Rss,
1046 Nproc,
1048 Nofile,
1050 Memlock,
1052 As,
1054 Locks,
1056 Sigpending,
1058 Msgqueue,
1060 Nice,
1062 Rtprio,
1064 Rttime,
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1070#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1071#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1072pub struct Rlimit {
1073 pub resource: RlimitResource,
1075
1076 pub soft: u64,
1078
1079 pub hard: u64,
1081}
1082
1083#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1089#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1090#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1091#[serde(rename_all = "lowercase")]
1092pub enum LogSource {
1093 Stdout,
1095
1096 Stderr,
1098
1099 Output,
1101
1102 System,
1104}
1105
1106impl DiskImageFormat {
1111 pub fn as_str(&self) -> &'static str {
1113 match self {
1114 Self::Qcow2 => "qcow2",
1115 Self::Raw => "raw",
1116 Self::Vmdk => "vmdk",
1117 }
1118 }
1119
1120 pub fn from_extension(ext: &str) -> Option<Self> {
1124 match ext {
1125 "qcow2" => Some(Self::Qcow2),
1126 "raw" => Some(Self::Raw),
1127 "vmdk" => Some(Self::Vmdk),
1128 _ => None,
1129 }
1130 }
1131}
1132
1133impl OciRootfsSource {
1134 pub fn new(reference: impl Into<String>) -> Self {
1136 Self {
1137 reference: reference.into(),
1138 root_disk: None,
1139 }
1140 }
1141}
1142
1143impl TransparentHugePagePolicy {
1144 pub fn is_madvise(&self) -> bool {
1146 matches!(self, Self::Madvise)
1147 }
1148
1149 pub fn as_str(self) -> &'static str {
1151 match self {
1152 Self::Always => "always",
1153 Self::Madvise => "madvise",
1154 Self::Never => "never",
1155 }
1156 }
1157}
1158
1159impl RootDisk {
1160 pub fn managed(size_mib: u32) -> Self {
1162 Self::Managed {
1163 size_mib: Some(size_mib),
1164 }
1165 }
1166
1167 pub fn tmpfs(size_mib: u32) -> Self {
1169 Self::Tmpfs {
1170 size_mib: Some(size_mib),
1171 }
1172 }
1173
1174 pub fn flat(size_mib: u32) -> Self {
1176 Self::Flat {
1177 size_mib: Some(size_mib),
1178 fstype: None,
1179 clone: FlatClone::Auto,
1180 }
1181 }
1182
1183 pub fn size_mib(&self) -> Option<u32> {
1185 match self {
1186 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1187 *size_mib
1188 }
1189 Self::DiskImage { .. } => None,
1190 }
1191 }
1192
1193 pub fn kind_str(&self) -> &'static str {
1195 match self {
1196 Self::Managed { .. } => "managed",
1197 Self::Tmpfs { .. } => "tmpfs",
1198 Self::DiskImage { .. } => "disk-image",
1199 Self::Flat { .. } => "flat",
1200 }
1201 }
1202
1203 pub fn is_managed(&self) -> bool {
1205 matches!(self, Self::Managed { .. })
1206 }
1207}
1208
1209impl FlatClone {
1210 pub const fn as_str(self) -> &'static str {
1212 match self {
1213 Self::Auto => "auto",
1214 Self::Copy => "copy",
1215 Self::Reflink => "reflink",
1216 }
1217 }
1218
1219 pub const fn is_auto(&self) -> bool {
1221 matches!(self, Self::Auto)
1222 }
1223}
1224
1225impl RootfsSource {
1226 pub fn oci(reference: impl Into<String>) -> Self {
1228 Self::Oci(OciRootfsSource::new(reference))
1229 }
1230
1231 pub fn oci_reference(&self) -> Option<&str> {
1233 match self {
1234 Self::Oci(oci) => Some(&oci.reference),
1235 _ => None,
1236 }
1237 }
1238
1239 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1241 match self {
1242 Self::Oci(oci) => oci.root_disk.as_ref(),
1243 _ => None,
1244 }
1245 }
1246
1247 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1250 match self {
1251 Self::Oci(oci) => match &oci.root_disk {
1252 Some(RootDisk::Managed { size_mib }) => *size_mib,
1253 Some(_) => None,
1254 None => None,
1255 },
1256 _ => None,
1257 }
1258 }
1259}
1260
1261impl EnvVar {
1262 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1264 Self {
1265 key: key.into(),
1266 value: value.into(),
1267 }
1268 }
1269
1270 pub fn as_pair(&self) -> (&str, &str) {
1272 (&self.key, &self.value)
1273 }
1274}
1275
1276impl VolumeKind {
1277 pub fn as_str(self) -> &'static str {
1279 match self {
1280 Self::Directory => "dir",
1281 Self::Disk => "disk",
1282 }
1283 }
1284
1285 pub fn from_db_value(value: &str) -> Self {
1287 match value {
1288 "disk" => Self::Disk,
1289 _ => Self::Directory,
1290 }
1291 }
1292}
1293
1294impl VolumeSpec {
1295 pub fn new(name: impl Into<String>) -> Self {
1297 Self {
1298 name: name.into(),
1299 kind: VolumeKind::Directory,
1300 quota_mib: None,
1301 capacity_mib: None,
1302 labels: Vec::new(),
1303 }
1304 }
1305}
1306
1307impl NamedVolumeCreate {
1308 pub fn mode(&self) -> NamedVolumeMode {
1310 self.mode
1311 }
1312
1313 pub fn name(&self) -> &str {
1315 &self.name
1316 }
1317
1318 pub fn kind(&self) -> VolumeKind {
1320 self.kind
1321 }
1322
1323 pub fn quota_mib(&self) -> Option<u32> {
1325 self.quota_mib
1326 }
1327
1328 pub fn capacity_mib(&self) -> Option<u32> {
1330 self.capacity_mib
1331 }
1332
1333 pub fn labels(&self) -> &[(String, String)] {
1335 &self.labels
1336 }
1337}
1338
1339impl VolumeMount {
1340 pub fn guest(&self) -> &str {
1342 match self {
1343 Self::Bind { guest, .. }
1344 | Self::Named { guest, .. }
1345 | Self::Tmpfs { guest, .. }
1346 | Self::DiskImage { guest, .. } => guest,
1347 }
1348 }
1349
1350 fn guest_mut(&mut self) -> &mut String {
1351 match self {
1352 Self::Bind { guest, .. }
1353 | Self::Named { guest, .. }
1354 | Self::Tmpfs { guest, .. }
1355 | Self::DiskImage { guest, .. } => guest,
1356 }
1357 }
1358
1359 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1361 match self {
1362 Self::Named { create, .. } => create.as_ref(),
1363 _ => None,
1364 }
1365 }
1366}
1367
1368pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1379 for mount in mounts.iter_mut() {
1380 let canonical = canonical_guest_mount_path(mount.guest())?;
1381 *mount.guest_mut() = canonical;
1382 }
1383
1384 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1385
1386 for pair in mounts.windows(2) {
1387 if pair[0].guest() == pair[1].guest() {
1388 return Err(TypesError::invalid_config(format!(
1389 "multiple volumes cannot mount the same guest path: {}",
1390 pair[0].guest()
1391 )));
1392 }
1393 }
1394
1395 Ok(())
1396}
1397
1398fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1399 let path = Utf8UnixPath::new(guest);
1400
1401 if !path.is_valid() {
1402 return Err(TypesError::invalid_config(format!(
1403 "guest mount path must be a valid Unix path: {guest}"
1404 )));
1405 }
1406 if !path.is_absolute() {
1407 return Err(TypesError::invalid_config(format!(
1408 "guest mount path must be absolute: {guest}"
1409 )));
1410 }
1411 if path
1412 .components()
1413 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1414 {
1415 return Err(TypesError::invalid_config(format!(
1416 "guest mount path must not contain '..': {guest}"
1417 )));
1418 }
1419 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1420 return Err(TypesError::invalid_config(format!(
1421 "guest mount path must not contain ':', ';', or ',': {guest}"
1422 )));
1423 }
1424
1425 let canonical = path.normalize().to_string();
1426 if canonical == "/" {
1427 return Err(TypesError::invalid_config(
1428 "cannot mount a volume at guest root /",
1429 ));
1430 }
1431
1432 Ok(canonical)
1433}
1434
1435fn guest_mount_order_key(guest: &str) -> (usize, String) {
1436 let path = Utf8UnixPath::new(guest);
1437 let depth = path.components().filter(Utf8Component::is_normal).count();
1438 (depth, guest.to_owned())
1439}
1440
1441impl RlimitResource {
1442 pub fn as_str(&self) -> &'static str {
1444 match self {
1445 Self::Cpu => "cpu",
1446 Self::Fsize => "fsize",
1447 Self::Data => "data",
1448 Self::Stack => "stack",
1449 Self::Core => "core",
1450 Self::Rss => "rss",
1451 Self::Nproc => "nproc",
1452 Self::Nofile => "nofile",
1453 Self::Memlock => "memlock",
1454 Self::As => "as",
1455 Self::Locks => "locks",
1456 Self::Sigpending => "sigpending",
1457 Self::Msgqueue => "msgqueue",
1458 Self::Nice => "nice",
1459 Self::Rtprio => "rtprio",
1460 Self::Rttime => "rttime",
1461 }
1462 }
1463}
1464
1465impl LogSource {
1466 pub fn effective(requested: &[Self]) -> Vec<Self> {
1468 if requested.is_empty() {
1469 vec![Self::Stdout, Self::Stderr, Self::Output]
1470 } else {
1471 let mut sources = requested.to_vec();
1472 sources.sort_by_key(|src| match src {
1473 Self::Stdout => 0,
1474 Self::Stderr => 1,
1475 Self::Output => 2,
1476 Self::System => 3,
1477 });
1478 sources.dedup();
1479 sources
1480 }
1481 }
1482}
1483
1484impl SandboxLogLevel {
1485 pub const fn as_str(self) -> &'static str {
1487 match self {
1488 Self::Error => "error",
1489 Self::Warn => "warn",
1490 Self::Info => "info",
1491 Self::Debug => "debug",
1492 Self::Trace => "trace",
1493 }
1494 }
1495}
1496
1497impl std::fmt::Display for DiskImageFormat {
1502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503 f.write_str(self.as_str())
1504 }
1505}
1506
1507impl FromStr for DiskImageFormat {
1508 type Err = String;
1509
1510 fn from_str(s: &str) -> Result<Self, Self::Err> {
1511 match s {
1512 "qcow2" => Ok(Self::Qcow2),
1513 "raw" => Ok(Self::Raw),
1514 "vmdk" => Ok(Self::Vmdk),
1515 _ => Err(format!("unknown disk image format: {s}")),
1516 }
1517 }
1518}
1519
1520impl fmt::Display for TransparentHugePagePolicy {
1521 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1522 f.write_str(self.as_str())
1523 }
1524}
1525
1526impl FromStr for TransparentHugePagePolicy {
1527 type Err = String;
1528
1529 fn from_str(value: &str) -> Result<Self, Self::Err> {
1530 match value {
1531 "always" => Ok(Self::Always),
1532 "madvise" => Ok(Self::Madvise),
1533 "never" => Ok(Self::Never),
1534 _ => Err(format!(
1535 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1536 )),
1537 }
1538 }
1539}
1540
1541impl Default for RootfsSource {
1542 fn default() -> Self {
1543 Self::oci(String::new())
1544 }
1545}
1546
1547impl Default for SandboxResources {
1548 fn default() -> Self {
1549 Self {
1550 cpus: DEFAULT_SANDBOX_CPUS,
1551 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1552 max_cpus: DEFAULT_SANDBOX_CPUS,
1553 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1554 cpu_placement: CpuPlacement::Inherit,
1555 placement_profile: None,
1556 thp: TransparentHugePagePolicy::Madvise,
1557 }
1558 }
1559}
1560
1561impl<'de> Deserialize<'de> for SandboxResources {
1562 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1563 where
1564 D: serde::Deserializer<'de>,
1565 {
1566 #[derive(Deserialize)]
1567 struct RawResources {
1568 #[serde(default = "default_sandbox_cpus")]
1569 cpus: u8,
1570 #[serde(default = "default_sandbox_memory_mib")]
1571 memory_mib: u32,
1572 max_cpus: Option<u8>,
1573 max_memory_mib: Option<u32>,
1574 #[serde(default)]
1575 cpu_placement: CpuPlacement,
1576 #[serde(default)]
1577 placement_profile: Option<String>,
1578 #[serde(default)]
1579 thp: TransparentHugePagePolicy,
1580 }
1581
1582 let raw = RawResources::deserialize(deserializer)?;
1583 Ok(Self {
1584 cpus: raw.cpus,
1585 memory_mib: raw.memory_mib,
1586 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1590 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1591 cpu_placement: raw.cpu_placement,
1592 placement_profile: raw.placement_profile,
1593 thp: raw.thp,
1594 })
1595 }
1596}
1597
1598impl CpuPlacement {
1599 pub const fn is_inherit(&self) -> bool {
1601 matches!(self, Self::Inherit)
1602 }
1603}
1604
1605impl std::fmt::Display for CpuPlacement {
1606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1607 f.write_str(match self {
1608 Self::Inherit => "inherit",
1609 Self::Auto => "auto",
1610 Self::Spread => "spread",
1611 Self::Compact => "compact",
1612 })
1613 }
1614}
1615
1616impl FromStr for CpuPlacement {
1617 type Err = String;
1618
1619 fn from_str(value: &str) -> Result<Self, Self::Err> {
1620 match value {
1621 "inherit" => Ok(Self::Inherit),
1622 "auto" => Ok(Self::Auto),
1623 "spread" => Ok(Self::Spread),
1624 "compact" => Ok(Self::Compact),
1625 _ => Err(format!(
1626 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1627 )),
1628 }
1629 }
1630}
1631
1632impl Default for SandboxRuntimeOptions {
1633 fn default() -> Self {
1634 Self {
1635 workdir: None,
1636 shell: None,
1637 scripts: BTreeMap::new(),
1638 entrypoint: None,
1639 cmd: None,
1640 hostname: None,
1641 user: None,
1642 log_level: None,
1643 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1644 disable_metrics_sample: false,
1645 }
1646 }
1647}
1648
1649impl Default for NetworkSpec {
1650 fn default() -> Self {
1651 Self {
1652 enabled: true,
1653 interface: None,
1654 ports: Vec::new(),
1655 policy: None,
1656 dns: None,
1657 tls: None,
1658 secrets: None,
1659 max_connections: None,
1660 rate_limiter: None,
1661 trust_host_cas: false,
1662 }
1663 }
1664}
1665
1666impl Default for PublishedPortSpec {
1667 fn default() -> Self {
1668 Self {
1669 host_port: 0,
1670 guest_port: 0,
1671 protocol: PortProtocol::Tcp,
1672 host_bind: "127.0.0.1".into(),
1673 }
1674 }
1675}
1676
1677impl From<(String, String)> for EnvVar {
1678 fn from((key, value): (String, String)) -> Self {
1679 Self { key, value }
1680 }
1681}
1682
1683impl From<EnvVar> for (String, String) {
1684 fn from(var: EnvVar) -> Self {
1685 (var.key, var.value)
1686 }
1687}
1688
1689impl FromStr for SandboxLogLevel {
1690 type Err = String;
1691
1692 fn from_str(s: &str) -> Result<Self, Self::Err> {
1693 match s {
1694 "error" => Ok(Self::Error),
1695 "warn" => Ok(Self::Warn),
1696 "info" => Ok(Self::Info),
1697 "debug" => Ok(Self::Debug),
1698 "trace" => Ok(Self::Trace),
1699 _ => Err(format!("unknown sandbox log level: {s}")),
1700 }
1701 }
1702}
1703
1704impl Serialize for VolumeMount {
1705 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1706 use serde::ser::SerializeMap;
1707
1708 match self {
1709 Self::Bind {
1710 host,
1711 guest,
1712 options,
1713 stat_virtualization,
1714 host_permissions,
1715 follow_root_symlinks,
1716 quota_mib,
1717 } => {
1718 let mut map = serializer.serialize_map(Some(8))?;
1719 map.serialize_entry("type", "Bind")?;
1720 map.serialize_entry("host", host)?;
1721 map.serialize_entry("guest", guest)?;
1722 map.serialize_entry("options", options)?;
1723 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1724 map.serialize_entry("host_permissions", host_permissions)?;
1725 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1726 map.serialize_entry("quota_mib", quota_mib)?;
1727 map.end()
1728 }
1729 Self::Named {
1730 name,
1731 guest,
1732 create: _,
1733 options,
1734 stat_virtualization,
1735 host_permissions,
1736 follow_root_symlinks,
1737 } => {
1738 let mut map = serializer.serialize_map(Some(7))?;
1739 map.serialize_entry("type", "Named")?;
1740 map.serialize_entry("name", name)?;
1741 map.serialize_entry("guest", guest)?;
1742 map.serialize_entry("options", options)?;
1743 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1744 map.serialize_entry("host_permissions", host_permissions)?;
1745 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1746 map.end()
1747 }
1748 Self::Tmpfs {
1749 guest,
1750 size_mib,
1751 options,
1752 } => {
1753 let mut map = serializer.serialize_map(Some(4))?;
1754 map.serialize_entry("type", "Tmpfs")?;
1755 map.serialize_entry("guest", guest)?;
1756 map.serialize_entry("size_mib", size_mib)?;
1757 map.serialize_entry("options", options)?;
1758 map.end()
1759 }
1760 Self::DiskImage {
1761 host,
1762 guest,
1763 format,
1764 fstype,
1765 options,
1766 } => {
1767 let mut map = serializer.serialize_map(Some(6))?;
1768 map.serialize_entry("type", "DiskImage")?;
1769 map.serialize_entry("host", host)?;
1770 map.serialize_entry("guest", guest)?;
1771 map.serialize_entry("format", format)?;
1772 map.serialize_entry("fstype", fstype)?;
1773 map.serialize_entry("options", options)?;
1774 map.end()
1775 }
1776 }
1777 }
1778}
1779
1780impl<'de> Deserialize<'de> for VolumeMount {
1781 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1782 fn default_strict() -> StatVirtualization {
1783 StatVirtualization::Strict
1784 }
1785
1786 fn default_private() -> HostPermissions {
1787 HostPermissions::Private
1788 }
1789
1790 #[derive(Deserialize)]
1791 #[serde(tag = "type")]
1792 enum VolumeMountHelper {
1793 Bind {
1794 host: PathBuf,
1795 guest: String,
1796 #[serde(default)]
1797 options: Option<MountOptions>,
1798 #[serde(default)]
1799 readonly: bool,
1800 #[serde(default = "default_strict")]
1801 stat_virtualization: StatVirtualization,
1802 #[serde(default = "default_private")]
1803 host_permissions: HostPermissions,
1804 #[serde(default)]
1805 follow_root_symlinks: bool,
1806 #[serde(default)]
1807 quota_mib: Option<u32>,
1808 },
1809 Named {
1810 name: String,
1811 guest: String,
1812 #[serde(default)]
1813 options: Option<MountOptions>,
1814 #[serde(default)]
1815 readonly: bool,
1816 #[serde(default = "default_strict")]
1817 stat_virtualization: StatVirtualization,
1818 #[serde(default = "default_private")]
1819 host_permissions: HostPermissions,
1820 #[serde(default)]
1821 follow_root_symlinks: bool,
1822 },
1823 Tmpfs {
1824 guest: String,
1825 #[serde(default)]
1826 size_mib: Option<u32>,
1827 #[serde(default)]
1828 options: Option<MountOptions>,
1829 #[serde(default)]
1830 readonly: bool,
1831 },
1832 DiskImage {
1833 host: PathBuf,
1834 guest: String,
1835 format: DiskImageFormat,
1836 #[serde(default)]
1837 fstype: Option<String>,
1838 #[serde(default)]
1839 options: Option<MountOptions>,
1840 #[serde(default)]
1841 readonly: bool,
1842 },
1843 }
1844
1845 let helper = VolumeMountHelper::deserialize(deserializer)?;
1846 Ok(match helper {
1847 VolumeMountHelper::Bind {
1848 host,
1849 guest,
1850 options,
1851 readonly,
1852 stat_virtualization,
1853 host_permissions,
1854 follow_root_symlinks,
1855 quota_mib,
1856 } => Self::Bind {
1857 host,
1858 guest,
1859 options: decode_mount_options(options, readonly),
1860 stat_virtualization,
1861 host_permissions,
1862 follow_root_symlinks,
1863 quota_mib,
1864 },
1865 VolumeMountHelper::Named {
1866 name,
1867 guest,
1868 options,
1869 readonly,
1870 stat_virtualization,
1871 host_permissions,
1872 follow_root_symlinks,
1873 } => Self::Named {
1874 name,
1875 guest,
1876 create: None,
1877 options: decode_mount_options(options, readonly),
1878 stat_virtualization,
1879 host_permissions,
1880 follow_root_symlinks,
1881 },
1882 VolumeMountHelper::Tmpfs {
1883 guest,
1884 size_mib,
1885 options,
1886 readonly,
1887 } => Self::Tmpfs {
1888 guest,
1889 size_mib,
1890 options: decode_mount_options(options, readonly),
1891 },
1892 VolumeMountHelper::DiskImage {
1893 host,
1894 guest,
1895 format,
1896 fstype,
1897 options,
1898 readonly,
1899 } => Self::DiskImage {
1900 host,
1901 guest,
1902 format,
1903 fstype,
1904 options: decode_mount_options(options, readonly),
1905 },
1906 })
1907 }
1908}
1909
1910impl fmt::Debug for VolumeMount {
1911 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1912 match self {
1913 Self::Bind {
1914 host,
1915 guest,
1916 options,
1917 stat_virtualization,
1918 host_permissions,
1919 follow_root_symlinks,
1920 quota_mib,
1921 } => f
1922 .debug_struct("Bind")
1923 .field("host", host)
1924 .field("guest", guest)
1925 .field("options", options)
1926 .field("stat_virtualization", stat_virtualization)
1927 .field("host_permissions", host_permissions)
1928 .field("follow_root_symlinks", follow_root_symlinks)
1929 .field("quota_mib", quota_mib)
1930 .finish(),
1931 Self::Named {
1932 name,
1933 guest,
1934 create,
1935 options,
1936 stat_virtualization,
1937 host_permissions,
1938 follow_root_symlinks,
1939 } => f
1940 .debug_struct("Named")
1941 .field("name", name)
1942 .field("guest", guest)
1943 .field("create", create)
1944 .field("options", options)
1945 .field("stat_virtualization", stat_virtualization)
1946 .field("host_permissions", host_permissions)
1947 .field("follow_root_symlinks", follow_root_symlinks)
1948 .finish(),
1949 Self::Tmpfs {
1950 guest,
1951 size_mib,
1952 options,
1953 } => f
1954 .debug_struct("Tmpfs")
1955 .field("guest", guest)
1956 .field("size_mib", size_mib)
1957 .field("options", options)
1958 .finish(),
1959 Self::DiskImage {
1960 host,
1961 guest,
1962 format,
1963 fstype,
1964 options,
1965 } => f
1966 .debug_struct("DiskImage")
1967 .field("host", host)
1968 .field("guest", guest)
1969 .field("format", format)
1970 .field("fstype", fstype)
1971 .field("options", options)
1972 .finish(),
1973 }
1974 }
1975}
1976
1977impl TryFrom<&str> for RlimitResource {
1979 type Error = String;
1980
1981 fn try_from(s: &str) -> Result<Self, Self::Error> {
1982 match s.to_ascii_lowercase().as_str() {
1983 "cpu" => Ok(Self::Cpu),
1984 "fsize" => Ok(Self::Fsize),
1985 "data" => Ok(Self::Data),
1986 "stack" => Ok(Self::Stack),
1987 "core" => Ok(Self::Core),
1988 "rss" => Ok(Self::Rss),
1989 "nproc" => Ok(Self::Nproc),
1990 "nofile" => Ok(Self::Nofile),
1991 "memlock" => Ok(Self::Memlock),
1992 "as" => Ok(Self::As),
1993 "locks" => Ok(Self::Locks),
1994 "sigpending" => Ok(Self::Sigpending),
1995 "msgqueue" => Ok(Self::Msgqueue),
1996 "nice" => Ok(Self::Nice),
1997 "rtprio" => Ok(Self::Rtprio),
1998 "rttime" => Ok(Self::Rttime),
1999 _ => Err(format!("unknown rlimit resource: {s}")),
2000 }
2001 }
2002}
2003
2004fn default_sandbox_cpus() -> u8 {
2009 DEFAULT_SANDBOX_CPUS
2010}
2011
2012fn default_sandbox_memory_mib() -> u32 {
2013 DEFAULT_SANDBOX_MEMORY_MIB
2014}
2015
2016fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2017 options.unwrap_or(MountOptions {
2018 readonly,
2019 ..MountOptions::default()
2020 })
2021}
2022
2023fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2024 for value in higher {
2025 match base.iter_mut().find(|current| current.key == value.key) {
2026 Some(current) => *current = value,
2027 None => base.push(value),
2028 }
2029 }
2030}
2031
2032fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2033 for value in higher {
2034 match base
2035 .iter_mut()
2036 .find(|current| current.env_var == value.env_var)
2037 {
2038 Some(current) => *current = value,
2039 None => base.push(value),
2040 }
2041 }
2042}
2043
2044pub(crate) fn default_strict() -> StatVirtualization {
2046 StatVirtualization::Strict
2047}
2048
2049pub(crate) fn default_private() -> HostPermissions {
2051 HostPermissions::Private
2052}
2053
2054pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2056
2057#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2064#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2065#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2066pub struct SecretsConfig {
2067 #[serde(default)]
2069 #[config_patch(merge_with = merge_secret_entries)]
2070 pub secrets: Vec<SecretEntry>,
2071
2072 #[serde(default)]
2074 pub on_violation: ViolationAction,
2075}
2076
2077#[derive(Clone, Serialize, Deserialize)]
2082#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2083#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2084pub struct SecretEntry {
2085 pub env_var: String,
2091
2092 #[serde(default = "empty_secret_value")]
2101 #[cfg_attr(feature = "ts", ts(type = "string"))]
2102 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2103 pub value: Zeroizing<String>,
2104
2105 #[serde(default, skip_serializing_if = "Option::is_none")]
2109 pub source: Option<SecretSource>,
2110
2111 pub placeholder: String,
2116
2117 #[serde(default)]
2119 pub allowed_hosts: Vec<HostPattern>,
2120
2121 #[serde(default)]
2123 pub injection: SecretInjection,
2124
2125 #[serde(default, skip_serializing_if = "Option::is_none")]
2127 pub on_violation: Option<ViolationAction>,
2128
2129 #[serde(default = "default_true")]
2134 pub require_tls_identity: bool,
2135}
2136
2137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2139#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2140#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2141#[serde(rename_all = "kebab-case")]
2142pub enum HostPattern {
2143 #[serde(alias = "Exact")]
2145 Exact(String),
2146 #[serde(alias = "Wildcard")]
2148 Wildcard(String),
2149 #[serde(alias = "Any")]
2151 Any,
2152}
2153
2154#[derive(Debug, Clone, Serialize, Deserialize)]
2156#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2157#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2158pub struct SecretInjection {
2159 #[serde(default = "default_true")]
2161 pub headers: bool,
2162
2163 #[serde(default = "default_true")]
2165 pub basic_auth: bool,
2166
2167 #[serde(default)]
2169 pub query_params: bool,
2170
2171 #[serde(default)]
2179 pub body: bool,
2180}
2181
2182#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2184#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2185#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2186#[serde(rename_all = "kebab-case")]
2187pub enum ViolationAction {
2188 #[serde(alias = "Block")]
2190 Block,
2191 #[default]
2193 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2194 BlockAndLog,
2195 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2197 BlockAndTerminate,
2198 #[serde(alias = "Passthrough")]
2200 Passthrough(Vec<HostPattern>),
2201}
2202
2203#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2205pub enum SecretConfigError {
2206 #[error("secret #{secret_index}: env_var must not be empty")]
2208 EmptyEnvVar {
2209 secret_index: usize,
2211 },
2212
2213 #[error("secret #{secret_index}: env_var must not contain `=`")]
2215 EnvVarContainsEquals {
2216 secret_index: usize,
2218 },
2219
2220 #[error("secret #{secret_index}: env_var must not contain NUL")]
2222 EnvVarContainsNul {
2223 secret_index: usize,
2225 },
2226
2227 #[error("secret #{secret_index}: at least one allowed host is required")]
2229 MissingAllowedHosts {
2230 secret_index: usize,
2232 },
2233
2234 #[error("secret #{secret_index}: placeholder must not be empty")]
2236 EmptyPlaceholder {
2237 secret_index: usize,
2239 },
2240
2241 #[error(
2243 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2244 )]
2245 PlaceholderTooLong {
2246 secret_index: usize,
2248 actual_bytes: usize,
2250 max_bytes: usize,
2252 },
2253
2254 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2256 PlaceholderContainsNul {
2257 secret_index: usize,
2259 },
2260
2261 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2263 PlaceholderContainsLineBreak {
2264 secret_index: usize,
2266 },
2267}
2268
2269impl SecretsConfig {
2270 pub fn validate(&self) -> Result<(), SecretConfigError> {
2272 for (index, secret) in self.secrets.iter().enumerate() {
2273 secret.validate(index)?;
2274 }
2275 Ok(())
2276 }
2277}
2278
2279impl SecretEntry {
2280 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2282 validate_env_var(&self.env_var, secret_index)?;
2283
2284 if self.allowed_hosts.is_empty() {
2285 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2286 }
2287
2288 validate_placeholder(&self.placeholder, secret_index)
2289 }
2290}
2291
2292impl fmt::Debug for SecretEntry {
2294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2295 f.debug_struct("SecretEntry")
2296 .field("env_var", &self.env_var)
2297 .field("value", &"[REDACTED]")
2298 .field("source", &self.source)
2299 .field("placeholder", &self.placeholder)
2300 .field("allowed_hosts", &self.allowed_hosts)
2301 .field("injection", &self.injection)
2302 .field("on_violation", &self.on_violation)
2303 .field("require_tls_identity", &self.require_tls_identity)
2304 .finish()
2305 }
2306}
2307
2308impl HostPattern {
2309 pub fn parse(host: &str) -> Self {
2312 if host == "*" {
2313 HostPattern::Any
2314 } else if host.starts_with("*.") {
2315 HostPattern::Wildcard(host.to_string())
2316 } else {
2317 HostPattern::Exact(host.to_string())
2318 }
2319 }
2320
2321 pub fn matches(&self, hostname: &str) -> bool {
2326 match self {
2327 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2328 HostPattern::Wildcard(pattern) => {
2329 if let Some(suffix) = pattern.strip_prefix("*.") {
2330 hostname.eq_ignore_ascii_case(suffix)
2331 || (hostname.len() > suffix.len() + 1
2332 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2333 && hostname[hostname.len() - suffix.len()..]
2334 .eq_ignore_ascii_case(suffix))
2335 } else {
2336 hostname.eq_ignore_ascii_case(pattern)
2337 }
2338 }
2339 HostPattern::Any => true,
2340 }
2341 }
2342}
2343
2344impl Default for SecretInjection {
2345 fn default() -> Self {
2346 Self {
2347 headers: true,
2348 basic_auth: true,
2349 query_params: false,
2350 body: false,
2351 }
2352 }
2353}
2354
2355fn default_true() -> bool {
2356 true
2357}
2358
2359fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2360 if env_var.is_empty() {
2361 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2362 }
2363 if env_var.contains('=') {
2364 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2365 }
2366 if env_var.contains('\0') {
2367 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2368 }
2369 Ok(())
2370}
2371
2372fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2373 if placeholder.is_empty() {
2374 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2375 }
2376
2377 let actual_bytes = placeholder.len();
2378 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2379 return Err(SecretConfigError::PlaceholderTooLong {
2380 secret_index,
2381 actual_bytes,
2382 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2383 });
2384 }
2385
2386 if placeholder.contains('\0') {
2387 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2388 }
2389 if placeholder.contains('\r') || placeholder.contains('\n') {
2390 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2391 }
2392
2393 Ok(())
2394}
2395
2396#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2406#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2407#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2408pub struct TlsConfig {
2409 #[serde(default)]
2411 pub enabled: bool,
2412
2413 #[serde(default = "default_intercepted_ports")]
2415 pub intercepted_ports: Vec<u16>,
2416
2417 #[serde(default)]
2419 pub bypass: Vec<String>,
2420
2421 #[serde(default = "default_true")]
2423 pub verify_upstream: bool,
2424
2425 #[serde(default = "default_true")]
2428 pub block_quic_on_intercept: bool,
2429
2430 #[serde(default)]
2432 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2433 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2434 pub upstream_ca_cert: Vec<PathBuf>,
2435
2436 #[serde(default, alias = "scoped_upstream_ca_certs")]
2438 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2439
2440 #[serde(default)]
2442 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2443
2444 #[serde(default, alias = "ca")]
2447 pub intercept_ca: InterceptCaConfig,
2448
2449 #[serde(default)]
2451 pub cache: CertCacheConfig,
2452}
2453
2454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2456#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2457#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2458pub struct InterceptCaConfig {
2459 #[serde(default)]
2462 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2463 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2464 pub cert_path: Option<PathBuf>,
2465
2466 #[serde(default)]
2469 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2470 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2471 pub key_path: Option<PathBuf>,
2472}
2473
2474#[derive(Debug, Clone, Serialize, Deserialize)]
2476#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2477#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2478pub struct CertCacheConfig {
2479 #[serde(default = "default_cache_capacity")]
2481 pub capacity: usize,
2482
2483 #[serde(default = "default_cert_validity_hours")]
2485 pub validity_hours: u64,
2486}
2487
2488#[derive(Debug, Clone, Serialize, Deserialize)]
2490#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2491#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2492pub struct ScopedUpstreamCaCert {
2493 pub pattern: String,
2495
2496 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2498 #[cfg_attr(feature = "ts", ts(type = "string"))]
2499 pub path: PathBuf,
2500}
2501
2502#[derive(Debug, Clone, Serialize, Deserialize)]
2504#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2505#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2506pub struct ScopedVerifyUpstream {
2507 pub pattern: String,
2509
2510 pub verify: bool,
2512}
2513
2514impl Default for TlsConfig {
2515 fn default() -> Self {
2516 Self {
2517 enabled: false,
2518 intercepted_ports: default_intercepted_ports(),
2519 bypass: Vec::new(),
2520 verify_upstream: true,
2521 block_quic_on_intercept: true,
2522 upstream_ca_cert: Vec::new(),
2523 scoped_upstream_ca_cert: Vec::new(),
2524 scoped_verify_upstream: Vec::new(),
2525 intercept_ca: InterceptCaConfig::default(),
2526 cache: CertCacheConfig::default(),
2527 }
2528 }
2529}
2530
2531impl Default for CertCacheConfig {
2532 fn default() -> Self {
2533 Self {
2534 capacity: default_cache_capacity(),
2535 validity_hours: default_cert_validity_hours(),
2536 }
2537 }
2538}
2539
2540fn default_intercepted_ports() -> Vec<u16> {
2541 vec![443]
2542}
2543
2544fn default_cache_capacity() -> usize {
2545 1000
2546}
2547
2548fn default_cert_validity_hours() -> u64 {
2549 24
2550}
2551
2552#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2558#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2559#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2560#[serde(rename_all = "snake_case")]
2561pub enum Action {
2562 Allow,
2564 Deny,
2566}
2567
2568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2570#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2571#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2572#[serde(rename_all = "snake_case")]
2573pub enum Direction {
2574 Egress,
2576 Ingress,
2578 Any,
2580}
2581
2582#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2584#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2585#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2586#[serde(rename_all = "snake_case")]
2587pub enum Protocol {
2588 Tcp,
2590 Udp,
2592 Icmpv4,
2594 Icmpv6,
2596}
2597
2598#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2600#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2601#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2602#[serde(rename_all = "snake_case")]
2603pub enum DestinationGroup {
2604 Public,
2606 Loopback,
2608 Private,
2610 LinkLocal,
2612 Metadata,
2614 Multicast,
2616 Host,
2618}
2619
2620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2627#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2628#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2629#[serde(rename_all = "snake_case")]
2630pub enum Destination {
2631 Any,
2633 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2635 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2636 Domain(String),
2638 DomainSuffix(String),
2640 Group(DestinationGroup),
2642}
2643
2644#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2646#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2647#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2648pub struct PortRange {
2649 pub start: u16,
2651 pub end: u16,
2653}
2654
2655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2658#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2659#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2660pub struct Rule {
2661 pub direction: Direction,
2663 pub destination: Destination,
2665 #[serde(default)]
2667 pub protocols: Vec<Protocol>,
2668 #[serde(default)]
2670 pub ports: Vec<PortRange>,
2671 pub action: Action,
2673}
2674
2675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2678#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2679#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2680pub struct NetworkPolicy {
2681 #[serde(default = "action_deny")]
2683 pub default_egress: Action,
2684 #[serde(default = "action_deny")]
2686 pub default_ingress: Action,
2687 #[serde(default)]
2689 pub rules: Vec<Rule>,
2690}
2691
2692fn action_deny() -> Action {
2695 Action::Deny
2696}
2697
2698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2704#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2705#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2706#[serde(default)]
2707pub struct DnsConfig {
2708 pub rebind_protection: bool,
2710 pub nameservers: Vec<String>,
2713 pub query_timeout_ms: u64,
2715}
2716
2717impl Default for DnsConfig {
2718 fn default() -> Self {
2719 Self {
2720 rebind_protection: true,
2721 nameservers: Vec::new(),
2722 query_timeout_ms: 5000,
2723 }
2724 }
2725}
2726
2727#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2731#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2732#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2733#[serde(default)]
2734pub struct InterfaceOverrides {
2735 #[serde(skip_serializing_if = "Option::is_none")]
2737 pub mac: Option<[u8; 6]>,
2738 #[serde(skip_serializing_if = "Option::is_none")]
2740 pub mtu: Option<u16>,
2741 #[serde(skip_serializing_if = "Option::is_none")]
2743 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2744 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2745 pub ipv4_address: Option<Ipv4Addr>,
2746 #[serde(skip_serializing_if = "Option::is_none")]
2748 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2749 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2750 pub ipv4_pool: Option<Ipv4Network>,
2751 #[serde(skip_serializing_if = "Option::is_none")]
2753 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2754 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2755 pub ipv6_address: Option<Ipv6Addr>,
2756 #[serde(skip_serializing_if = "Option::is_none")]
2758 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2759 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2760 pub ipv6_pool: Option<Ipv6Network>,
2761}
2762
2763fn empty_secret_value() -> Zeroizing<String> {
2764 Zeroizing::new(String::new())
2765}
2766
2767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2773pub enum NetworkRateLimitDirection {
2774 Egress,
2776 Ingress,
2778}
2779
2780#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2782#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2783#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2784#[serde(default)]
2785pub struct NetworkRateLimiterConfig {
2786 #[serde(skip_serializing_if = "Option::is_none")]
2788 pub egress: Option<RateLimiterConfig>,
2789
2790 #[serde(skip_serializing_if = "Option::is_none")]
2792 pub ingress: Option<RateLimiterConfig>,
2793}
2794
2795#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2801#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2802#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2803#[serde(default)]
2804pub struct RateLimiterConfig {
2805 #[serde(skip_serializing_if = "Option::is_none")]
2807 pub bandwidth: Option<TokenBucketConfig>,
2808
2809 #[serde(skip_serializing_if = "Option::is_none")]
2811 pub ops: Option<TokenBucketConfig>,
2812}
2813
2814#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2820#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2821#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2822pub struct TokenBucketConfig {
2823 pub size: u64,
2825
2826 pub refill_time_ms: u64,
2829
2830 #[serde(default)]
2832 pub one_time_burst: u64,
2833}
2834
2835#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2837pub enum RateLimitConfigError {
2838 #[error("rate limiter must configure at least one of bandwidth or ops")]
2840 EmptyLimiter,
2841
2842 #[error("{bucket} bucket: size must be greater than zero")]
2844 ZeroSize {
2845 bucket: &'static str,
2847 },
2848
2849 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
2851 ZeroRefillTime {
2852 bucket: &'static str,
2854 },
2855}
2856
2857impl RateLimiterConfig {
2858 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
2860 if self.bandwidth.is_none() && self.ops.is_none() {
2861 return Err(RateLimitConfigError::EmptyLimiter);
2862 }
2863 if let Some(bandwidth) = &self.bandwidth {
2864 bandwidth.validate("bandwidth")?;
2865 }
2866 if let Some(ops) = &self.ops {
2867 ops.validate("ops")?;
2868 }
2869 Ok(())
2870 }
2871}
2872
2873impl TokenBucketConfig {
2874 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
2876 if self.size == 0 {
2877 return Err(RateLimitConfigError::ZeroSize { bucket });
2878 }
2879 if self.refill_time_ms == 0 {
2880 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
2881 }
2882 Ok(())
2883 }
2884}
2885
2886impl fmt::Display for NetworkRateLimitDirection {
2887 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2888 match self {
2889 Self::Egress => f.write_str("egress"),
2890 Self::Ingress => f.write_str("ingress"),
2891 }
2892 }
2893}
2894
2895#[cfg(test)]
2900mod tests {
2901 use super::*;
2902
2903 fn tmpfs_mount(guest: &str) -> VolumeMount {
2904 VolumeMount::Tmpfs {
2905 guest: guest.to_owned(),
2906 size_mib: None,
2907 options: MountOptions::default(),
2908 }
2909 }
2910
2911 #[test]
2912 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
2913 let value = serde_json::to_value(MountOptions::default()).unwrap();
2914 assert!(value.get("override_uid").is_none());
2915 assert!(value.get("override_gid").is_none());
2916
2917 let decoded: MountOptions = serde_json::from_value(value).unwrap();
2918 assert_eq!(decoded.override_uid, None);
2919 assert_eq!(decoded.override_gid, None);
2920 }
2921
2922 #[test]
2923 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
2924 let mut mounts = vec![
2925 tmpfs_mount("/workspace//persist/./logs/"),
2926 tmpfs_mount("/alpha/z"),
2927 tmpfs_mount("/workspace"),
2928 ];
2929
2930 canonicalize_volume_mounts(&mut mounts).unwrap();
2931
2932 assert_eq!(
2933 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
2934 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
2935 );
2936 }
2937
2938 #[test]
2939 fn volume_mounts_reject_duplicate_canonical_paths() {
2940 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
2941
2942 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2943
2944 assert!(error.to_string().contains("same guest path: /data/cache"));
2945 }
2946
2947 #[test]
2948 fn volume_mounts_reject_parent_components_before_normalizing() {
2949 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
2950
2951 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2952
2953 assert!(error.to_string().contains("must not contain '..'"));
2954 }
2955
2956 #[test]
2957 fn disk_image_format_from_extension() {
2958 assert_eq!(
2959 DiskImageFormat::from_extension("qcow2"),
2960 Some(DiskImageFormat::Qcow2)
2961 );
2962 assert_eq!(
2963 DiskImageFormat::from_extension("raw"),
2964 Some(DiskImageFormat::Raw)
2965 );
2966 assert_eq!(
2967 DiskImageFormat::from_extension("vmdk"),
2968 Some(DiskImageFormat::Vmdk)
2969 );
2970 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
2971 assert_eq!(DiskImageFormat::from_extension(""), None);
2972 }
2973
2974 #[test]
2975 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
2976 let resources: SandboxResources =
2977 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
2978
2979 assert_eq!(resources.cpus, 4);
2980 assert_eq!(resources.max_cpus, 4);
2981 assert_eq!(resources.memory_mib, 2048);
2982 assert_eq!(resources.max_memory_mib, 2048);
2983 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
2984 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
2985 assert_eq!(
2986 serde_json::to_value(resources).unwrap(),
2987 serde_json::json!({
2988 "cpus": 4,
2989 "memory_mib": 2048,
2990 "max_cpus": 4,
2991 "max_memory_mib": 2048
2992 })
2993 );
2994 }
2995
2996 #[test]
2997 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
2998 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
2999 assert!(inherited.get("cpu_placement").is_none());
3000
3001 for policy in [
3002 CpuPlacement::Auto,
3003 CpuPlacement::Spread,
3004 CpuPlacement::Compact,
3005 ] {
3006 let resources = SandboxResources {
3007 cpu_placement: policy,
3008 ..Default::default()
3009 };
3010 let json = serde_json::to_string(&resources).unwrap();
3011 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3012
3013 assert_eq!(decoded.cpu_placement, policy);
3014 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3015 }
3016 }
3017
3018 #[test]
3019 fn transparent_huge_page_policy_roundtrips_non_default() {
3020 let resources: SandboxResources = serde_json::from_str(
3021 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3022 )
3023 .unwrap();
3024
3025 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3026 assert_eq!(
3027 serde_json::to_value(resources).unwrap()["thp"],
3028 serde_json::json!("always")
3029 );
3030 assert_eq!(
3031 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3032 TransparentHugePagePolicy::Never
3033 );
3034 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3035 }
3036
3037 #[test]
3038 fn disk_image_format_display_roundtrip() {
3039 for format in [
3040 DiskImageFormat::Qcow2,
3041 DiskImageFormat::Raw,
3042 DiskImageFormat::Vmdk,
3043 ] {
3044 let rendered = format.to_string();
3045 let parsed: DiskImageFormat = rendered.parse().unwrap();
3046 assert_eq!(parsed, format);
3047 }
3048 }
3049
3050 #[test]
3051 fn disk_image_format_from_str_unknown() {
3052 assert!("ext4".parse::<DiskImageFormat>().is_err());
3053 }
3054
3055 #[test]
3056 fn log_source_effective_uses_default_user_program_sources() {
3057 assert_eq!(
3058 LogSource::effective(&[]),
3059 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3060 );
3061 }
3062
3063 #[test]
3064 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3065 assert_eq!(
3066 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3067 vec![LogSource::Stdout, LogSource::System]
3068 );
3069 }
3070
3071 #[test]
3072 fn rlimit_resource_parses_case_insensitively() {
3073 assert_eq!(
3074 RlimitResource::try_from("NOFILE").unwrap(),
3075 RlimitResource::Nofile
3076 );
3077 assert!(RlimitResource::try_from("bogus").is_err());
3078 }
3079
3080 #[test]
3081 fn sandbox_policy_serde_roundtrip() {
3082 let policy = SandboxPolicy {
3083 ephemeral: true,
3084 max_duration_secs: Some(3600),
3085 idle_timeout_secs: Some(120),
3086 };
3087
3088 let json = serde_json::to_string(&policy).unwrap();
3089 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3090
3091 assert!(decoded.ephemeral);
3092 assert_eq!(decoded.max_duration_secs, Some(3600));
3093 assert_eq!(decoded.idle_timeout_secs, Some(120));
3094 }
3095
3096 #[test]
3097 fn sandbox_policy_defaults_to_persistent() {
3098 assert!(!SandboxPolicy::default().ephemeral);
3099 }
3100
3101 #[test]
3102 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3103 let decoded: SandboxPolicy =
3106 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3107 assert!(!decoded.ephemeral);
3108 assert_eq!(decoded.max_duration_secs, Some(60));
3109 }
3110
3111 #[test]
3112 fn sandbox_spec_default_uses_static_resource_defaults() {
3113 let spec = SandboxSpec::default();
3114
3115 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3116 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3117 assert_eq!(
3118 spec.runtime.metrics_sample_interval_ms,
3119 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3120 );
3121 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3122 }
3123
3124 #[test]
3125 fn deployment_profile_uses_stable_snake_case_wire_values() {
3126 assert_eq!(
3127 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3128 r#""multi_tenant""#
3129 );
3130 assert_eq!(
3131 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3132 DeploymentProfile::SingleTenant
3133 );
3134 }
3135
3136 #[test]
3137 fn sandbox_log_level_roundtrips_lowercase_values() {
3138 for (input, expected) in [
3139 ("error", SandboxLogLevel::Error),
3140 ("warn", SandboxLogLevel::Warn),
3141 ("info", SandboxLogLevel::Info),
3142 ("debug", SandboxLogLevel::Debug),
3143 ("trace", SandboxLogLevel::Trace),
3144 ] {
3145 let parsed: SandboxLogLevel = input.parse().unwrap();
3146 assert_eq!(parsed, expected);
3147 assert_eq!(parsed.as_str(), input);
3148 }
3149 }
3150}