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 sha2::{Digest, Sha256};
13use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath};
14use zeroize::Zeroizing;
15
16use crate::modify::SecretSource;
17use crate::{TypesError, TypesResult};
18
19pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
25
26pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
28
29pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
39#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
40pub enum DiskImageFormat {
41 Qcow2,
43 Raw,
45 Vmdk,
47}
48
49#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
52#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
53#[serde(rename_all = "kebab-case")]
54pub enum FlatClone {
55 #[default]
57 Auto,
58
59 Copy,
61
62 Reflink,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
69pub enum RootfsSource {
70 Bind {
72 #[cfg_attr(feature = "ts", ts(type = "string"))]
74 path: PathBuf,
75 #[serde(default)]
82 follow_root_symlinks: bool,
83 },
84
85 Oci(OciRootfsSource),
87
88 DiskImage {
90 #[cfg_attr(feature = "ts", ts(type = "string"))]
92 path: PathBuf,
93 format: DiskImageFormat,
95 fstype: Option<String>,
97 },
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
103#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
104pub struct OciRootfsSource {
105 pub reference: String,
107
108 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub root_disk: Option<RootDisk>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
120#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
121#[serde(tag = "kind", rename_all = "kebab-case")]
122pub enum RootDisk {
123 Managed {
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 size_mib: Option<u32>,
129 },
130
131 Tmpfs {
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 size_mib: Option<u32>,
137 },
138
139 DiskImage {
142 #[cfg_attr(feature = "ts", ts(type = "string"))]
144 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
145 path: PathBuf,
146 format: DiskImageFormat,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 fstype: Option<String>,
151 },
152
153 Flat {
158 #[serde(default, skip_serializing_if = "Option::is_none")]
161 size_mib: Option<u32>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 fstype: Option<String>,
165 #[serde(default, skip_serializing_if = "FlatClone::is_auto")]
167 clone: FlatClone,
168 },
169}
170
171#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
173#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
174#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
175pub enum PullPolicy {
176 #[default]
178 IfMissing,
179
180 Always,
182
183 Never,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
196#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
197#[serde(rename_all = "lowercase")]
198pub enum StatVirtualization {
199 Strict,
201 Relaxed,
203 Off,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
212#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
213#[serde(rename_all = "lowercase")]
214pub enum HostPermissions {
215 Private,
217 Mirror,
219}
220
221#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
223#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
224#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
225#[serde(rename_all = "lowercase")]
226pub enum SecurityProfile {
227 #[default]
231 Default,
232
233 Restricted,
237}
238
239#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
245#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
246#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
247#[serde(rename_all = "snake_case")]
248pub enum DeploymentProfile {
249 #[default]
251 SingleTenant,
252
253 MultiTenant,
255}
256
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
259#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
260#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
261#[serde(default)]
262pub struct MountOptions {
263 pub readonly: bool,
267
268 pub noexec: bool,
272
273 pub nosuid: bool,
275
276 pub nodev: bool,
278
279 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub override_uid: Option<u32>,
288
289 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub override_gid: Option<u32>,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
300pub enum VolumeKind {
301 Directory,
303
304 Disk,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
311#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
312pub struct VolumeSpec {
313 pub name: String,
315
316 pub kind: VolumeKind,
318
319 pub quota_mib: Option<u32>,
321
322 pub capacity_mib: Option<u32>,
324
325 pub labels: Vec<(String, String)>,
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
331#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
332#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
333pub enum NamedVolumeMode {
334 Existing,
336
337 Create,
339
340 EnsureExists,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
347#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
348pub struct NamedVolumeCreate {
349 pub mode: NamedVolumeMode,
351
352 pub name: String,
354
355 pub kind: VolumeKind,
357
358 pub quota_mib: Option<u32>,
360
361 pub capacity_mib: Option<u32>,
363
364 pub labels: Vec<(String, String)>,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
371#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
372#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
373pub enum OwnedVolumeStorage {
374 Directory {
376 quota_mib: Option<u32>,
378 },
379 Disk {
381 capacity_mib: u32,
383 },
384}
385
386#[derive(Clone)]
388#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
389#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
390#[cfg_attr(feature = "ts", ts(tag = "type"))]
391pub enum VolumeMount {
392 Owned {
394 guest: String,
396 storage: OwnedVolumeStorage,
398 options: MountOptions,
400 stat_virtualization: StatVirtualization,
402 host_permissions: HostPermissions,
404 },
405 Bind {
407 #[cfg_attr(feature = "ts", ts(type = "string"))]
409 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
410 host: PathBuf,
411 guest: String,
413 options: MountOptions,
415 stat_virtualization: StatVirtualization,
417 host_permissions: HostPermissions,
419 follow_root_symlinks: bool,
426 quota_mib: Option<u32>,
432 },
433
434 Named {
436 name: String,
438 guest: String,
440 create: Option<NamedVolumeCreate>,
444 options: MountOptions,
446 stat_virtualization: StatVirtualization,
448 host_permissions: HostPermissions,
450 follow_root_symlinks: bool,
455 },
456
457 Tmpfs {
459 guest: String,
461 size_mib: Option<u32>,
463 options: MountOptions,
465 },
466
467 DiskImage {
469 #[cfg_attr(feature = "ts", ts(type = "string"))]
471 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
472 host: PathBuf,
473 guest: String,
475 format: DiskImageFormat,
477 fstype: Option<String>,
479 options: MountOptions,
481 },
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
487#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
488pub enum Patch {
489 Text {
491 path: String,
493 content: String,
495 mode: Option<u32>,
497 replace: bool,
499 },
500
501 File {
503 path: String,
505 content: Vec<u8>,
507 mode: Option<u32>,
509 replace: bool,
511 },
512
513 CopyFile {
515 #[cfg_attr(feature = "ts", ts(type = "string"))]
517 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
518 src: PathBuf,
519 dst: String,
521 mode: Option<u32>,
523 replace: bool,
525 },
526
527 CopyDir {
529 #[cfg_attr(feature = "ts", ts(type = "string"))]
531 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
532 src: PathBuf,
533 dst: String,
535 replace: bool,
537 },
538
539 Symlink {
541 target: String,
543 link: String,
545 replace: bool,
547 },
548
549 Mkdir {
551 path: String,
553 mode: Option<u32>,
555 },
556
557 Remove {
559 path: String,
561 },
562
563 Append {
565 path: String,
567 content: String,
569 },
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
580#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
581#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
582#[serde(default)]
583pub struct NetworkSpec {
584 pub enabled: bool,
586
587 #[serde(skip_serializing_if = "Option::is_none")]
589 #[config_patch(nested)]
590 pub interface: Option<InterfaceOverrides>,
591
592 pub ports: Vec<PublishedPortSpec>,
594
595 #[serde(skip_serializing_if = "Option::is_none")]
597 pub policy: Option<NetworkPolicy>,
598
599 #[serde(skip_serializing_if = "Option::is_none")]
601 #[config_patch(nested)]
602 pub dns: Option<DnsConfig>,
603
604 #[serde(skip_serializing_if = "Option::is_none")]
606 #[config_patch(nested)]
607 pub tls: Option<TlsConfig>,
608
609 pub strict: bool,
611
612 #[serde(skip_serializing_if = "Option::is_none")]
614 #[config_patch(nested)]
615 pub secrets: Option<SecretsConfig>,
616
617 #[serde(rename = "max_connections", alias = "max_tcp_connections")]
620 pub max_tcp_connections: Option<usize>,
621
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub max_udp_connections: Option<usize>,
625
626 #[serde(skip_serializing_if = "Option::is_none")]
628 #[config_patch(nested)]
629 pub rate_limiter: Option<NetworkRateLimiterConfig>,
630
631 pub trust_host_cas: bool,
633
634 #[serde(skip_serializing_if = "Option::is_none")]
637 pub outbound_proxy: Option<OutboundProxy>,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
643#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
644#[serde(tag = "protocol", rename_all = "lowercase")]
645#[non_exhaustive]
646pub enum OutboundProxy {
647 Socks4 {
649 address: String,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
653 user_id: Option<String>,
654 },
655
656 Socks5 {
658 address: String,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
662 credentials: Option<Socks5Credentials>,
663 },
664}
665
666#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
671#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
672#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
673pub struct Socks5Credentials {
674 pub username: String,
676
677 pub password: SecretSource,
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
683#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
684#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
685pub struct PublishedPortSpec {
686 pub host_port: u16,
688
689 pub guest_port: u16,
691
692 #[serde(default)]
694 pub protocol: PortProtocol,
695
696 pub host_bind: String,
698}
699
700#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
702#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
703#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
704pub enum PortProtocol {
705 #[default]
707 #[serde(rename = "tcp")]
708 Tcp,
709
710 #[serde(rename = "udp")]
712 Udp,
713}
714
715#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
721#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
722#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
723#[serde(default)]
724pub struct VsockSpec {
725 pub routes: Vec<VsockRouteSpec>,
727}
728
729impl VsockSpec {
730 pub fn is_empty(&self) -> bool {
732 self.routes.is_empty()
733 }
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
738#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
739#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
740pub struct VsockRouteSpec {
741 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
743 pub host_socket: PathBuf,
744
745 pub port: u32,
747
748 #[serde(default)]
750 pub socket_type: VsockSocketType,
751}
752
753#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
755#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
756#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
757#[serde(rename_all = "snake_case")]
758pub enum VsockSocketType {
759 #[default]
761 Stream,
762
763 Dgram,
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize)]
773#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
774#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
775pub struct HandoffInit {
776 pub cmd: String,
780
781 #[serde(default)]
783 pub args: Vec<String>,
784
785 #[serde(default)]
787 pub env: Vec<(String, String)>,
788}
789
790#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
796#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
797#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
798pub struct SandboxPolicy {
799 #[serde(default)]
808 pub ephemeral: bool,
809
810 pub max_duration_secs: Option<u64>,
812
813 pub idle_timeout_secs: Option<u64>,
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
827#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
828pub struct SnapshotSpec {
829 pub name: String,
831
832 #[serde(default)]
834 pub group: Option<String>,
835
836 #[serde(default)]
838 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
839 pub dest_dir: Option<PathBuf>,
840
841 pub source_sandbox: String,
843
844 pub labels: Vec<(String, String)>,
846
847 pub force: bool,
849
850 pub record_integrity: bool,
852
853 #[serde(default)]
855 pub full: bool,
856}
857
858#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
866#[config_patch(name = SandboxConfigPatch)]
867#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
868#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
869#[serde(default)]
870pub struct SandboxSpec {
871 pub name: String,
873
874 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
876 pub image: RootfsSource,
877
878 #[config_patch(nested)]
880 pub resources: SandboxResources,
881
882 #[config_patch(nested)]
884 pub runtime: SandboxRuntimeOptions,
885
886 #[config_patch(merge_with = merge_env_vars)]
888 pub env: Vec<EnvVar>,
889
890 #[config_patch(merge)]
892 pub labels: BTreeMap<String, String>,
893
894 pub rlimits: Vec<Rlimit>,
896
897 pub mounts: Vec<VolumeMount>,
899
900 pub patches: Vec<Patch>,
902
903 #[config_patch(nested)]
905 pub network: NetworkSpec,
906
907 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
909 #[config_patch(nested)]
910 pub vsock: VsockSpec,
911
912 pub init: Option<HandoffInit>,
914
915 pub pull_policy: PullPolicy,
917
918 pub security_profile: SecurityProfile,
920
921 pub deployment_profile: DeploymentProfile,
927
928 #[config_patch(nested)]
930 pub lifecycle: SandboxPolicy,
931}
932
933#[derive(Debug, Clone, Serialize, ConfigPatch)]
935#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
936#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
937pub struct SandboxResources {
938 pub cpus: u8,
940
941 pub memory_mib: u32,
943
944 pub max_cpus: u8,
946
947 pub max_memory_mib: u32,
949
950 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
952 pub cpu_placement: CpuPlacement,
953
954 #[serde(default, skip_serializing_if = "Option::is_none")]
956 pub placement_profile: Option<String>,
957
958 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
960 pub thp: TransparentHugePagePolicy,
961}
962
963#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
965#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
966#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
967#[serde(rename_all = "lowercase")]
968pub enum CpuPlacement {
969 #[default]
971 Inherit,
972
973 Auto,
975
976 Spread,
978
979 Compact,
981}
982
983#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
985#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
986#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
987#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
988pub enum NumaPlacement {
989 PreferSingle,
991 StrictSingle,
993 Inherit,
995}
996
997#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
999#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1000#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1001#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1002pub enum MemoryPlacement {
1003 FollowCpu,
1005 Inherit,
1007}
1008
1009#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1011#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1012#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1013#[serde(deny_unknown_fields)]
1014pub struct PlacementProfile {
1015 pub numa: NumaPlacement,
1017 pub memory: MemoryPlacement,
1019}
1020
1021#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1023#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1024#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1025#[serde(rename_all = "lowercase")]
1026pub enum TransparentHugePagePolicy {
1027 Always,
1029
1030 #[default]
1032 Madvise,
1033
1034 Never,
1036}
1037
1038#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1040#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1041#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1042#[serde(default)]
1043pub struct SandboxRuntimeOptions {
1044 pub workdir: Option<String>,
1046
1047 pub shell: Option<String>,
1049
1050 #[config_patch(merge)]
1052 pub scripts: BTreeMap<String, String>,
1053
1054 pub entrypoint: Option<Vec<String>>,
1056
1057 pub cmd: Option<Vec<String>>,
1059
1060 pub hostname: Option<String>,
1062
1063 pub user: Option<String>,
1065
1066 pub log_level: Option<SandboxLogLevel>,
1068
1069 pub metrics_sample_interval_ms: Option<u64>,
1071
1072 pub disable_metrics_sample: bool,
1074}
1075
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1078#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1079#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1080pub struct EnvVar {
1081 pub key: String,
1083
1084 pub value: String,
1086}
1087
1088#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1090#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1091#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1092#[serde(rename_all = "lowercase")]
1093pub enum SandboxLogLevel {
1094 Error,
1096
1097 Warn,
1099
1100 Info,
1102
1103 Debug,
1105
1106 Trace,
1108}
1109
1110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1116#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1117#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1118pub enum RlimitResource {
1119 Cpu,
1121 Fsize,
1123 Data,
1125 Stack,
1127 Core,
1129 Rss,
1131 Nproc,
1133 Nofile,
1135 Memlock,
1137 As,
1139 Locks,
1141 Sigpending,
1143 Msgqueue,
1145 Nice,
1147 Rtprio,
1149 Rttime,
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1155#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1156#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1157pub struct Rlimit {
1158 pub resource: RlimitResource,
1160
1161 pub soft: u64,
1163
1164 pub hard: u64,
1166}
1167
1168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1174#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1175#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1176#[serde(rename_all = "lowercase")]
1177pub enum LogSource {
1178 Stdout,
1180
1181 Stderr,
1183
1184 Output,
1186
1187 System,
1189}
1190
1191impl SandboxResourcesPatch {
1196 pub fn has_cpus(&self) -> bool {
1198 self.cpus.is_some()
1199 }
1200
1201 pub fn has_memory_mib(&self) -> bool {
1203 self.memory_mib.is_some()
1204 }
1205
1206 pub fn has_max_cpus(&self) -> bool {
1208 self.max_cpus.is_some()
1209 }
1210
1211 pub fn has_max_memory_mib(&self) -> bool {
1213 self.max_memory_mib.is_some()
1214 }
1215}
1216
1217impl DiskImageFormat {
1218 pub fn as_str(&self) -> &'static str {
1220 match self {
1221 Self::Qcow2 => "qcow2",
1222 Self::Raw => "raw",
1223 Self::Vmdk => "vmdk",
1224 }
1225 }
1226
1227 pub fn from_extension(ext: &str) -> Option<Self> {
1231 match ext {
1232 "qcow2" => Some(Self::Qcow2),
1233 "raw" => Some(Self::Raw),
1234 "vmdk" => Some(Self::Vmdk),
1235 _ => None,
1236 }
1237 }
1238}
1239
1240impl OciRootfsSource {
1241 pub fn new(reference: impl Into<String>) -> Self {
1243 Self {
1244 reference: reference.into(),
1245 root_disk: None,
1246 }
1247 }
1248}
1249
1250impl TransparentHugePagePolicy {
1251 pub fn is_madvise(&self) -> bool {
1253 matches!(self, Self::Madvise)
1254 }
1255
1256 pub fn as_str(self) -> &'static str {
1258 match self {
1259 Self::Always => "always",
1260 Self::Madvise => "madvise",
1261 Self::Never => "never",
1262 }
1263 }
1264}
1265
1266impl RootDisk {
1267 pub fn managed(size_mib: u32) -> Self {
1269 Self::Managed {
1270 size_mib: Some(size_mib),
1271 }
1272 }
1273
1274 pub fn tmpfs(size_mib: u32) -> Self {
1276 Self::Tmpfs {
1277 size_mib: Some(size_mib),
1278 }
1279 }
1280
1281 pub fn flat(size_mib: u32) -> Self {
1283 Self::Flat {
1284 size_mib: Some(size_mib),
1285 fstype: None,
1286 clone: FlatClone::Auto,
1287 }
1288 }
1289
1290 pub fn size_mib(&self) -> Option<u32> {
1292 match self {
1293 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1294 *size_mib
1295 }
1296 Self::DiskImage { .. } => None,
1297 }
1298 }
1299
1300 pub fn kind_str(&self) -> &'static str {
1302 match self {
1303 Self::Managed { .. } => "managed",
1304 Self::Tmpfs { .. } => "tmpfs",
1305 Self::DiskImage { .. } => "disk-image",
1306 Self::Flat { .. } => "flat",
1307 }
1308 }
1309
1310 pub fn is_managed(&self) -> bool {
1312 matches!(self, Self::Managed { .. })
1313 }
1314}
1315
1316impl FlatClone {
1317 pub const fn as_str(self) -> &'static str {
1319 match self {
1320 Self::Auto => "auto",
1321 Self::Copy => "copy",
1322 Self::Reflink => "reflink",
1323 }
1324 }
1325
1326 pub const fn is_auto(&self) -> bool {
1328 matches!(self, Self::Auto)
1329 }
1330}
1331
1332impl RootfsSource {
1333 pub fn oci(reference: impl Into<String>) -> Self {
1335 Self::Oci(OciRootfsSource::new(reference))
1336 }
1337
1338 pub fn oci_reference(&self) -> Option<&str> {
1340 match self {
1341 Self::Oci(oci) => Some(&oci.reference),
1342 _ => None,
1343 }
1344 }
1345
1346 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1348 match self {
1349 Self::Oci(oci) => oci.root_disk.as_ref(),
1350 _ => None,
1351 }
1352 }
1353
1354 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1357 match self {
1358 Self::Oci(oci) => match &oci.root_disk {
1359 Some(RootDisk::Managed { size_mib }) => *size_mib,
1360 Some(_) => None,
1361 None => None,
1362 },
1363 _ => None,
1364 }
1365 }
1366}
1367
1368impl EnvVar {
1369 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1371 Self {
1372 key: key.into(),
1373 value: value.into(),
1374 }
1375 }
1376
1377 pub fn as_pair(&self) -> (&str, &str) {
1379 (&self.key, &self.value)
1380 }
1381}
1382
1383impl VolumeKind {
1384 pub fn as_str(self) -> &'static str {
1386 match self {
1387 Self::Directory => "dir",
1388 Self::Disk => "disk",
1389 }
1390 }
1391
1392 pub fn from_db_value(value: &str) -> Self {
1394 match value {
1395 "disk" => Self::Disk,
1396 _ => Self::Directory,
1397 }
1398 }
1399}
1400
1401impl VolumeSpec {
1402 pub fn new(name: impl Into<String>) -> Self {
1404 Self {
1405 name: name.into(),
1406 kind: VolumeKind::Directory,
1407 quota_mib: None,
1408 capacity_mib: None,
1409 labels: Vec::new(),
1410 }
1411 }
1412}
1413
1414impl NamedVolumeCreate {
1415 pub fn mode(&self) -> NamedVolumeMode {
1417 self.mode
1418 }
1419
1420 pub fn name(&self) -> &str {
1422 &self.name
1423 }
1424
1425 pub fn kind(&self) -> VolumeKind {
1427 self.kind
1428 }
1429
1430 pub fn quota_mib(&self) -> Option<u32> {
1432 self.quota_mib
1433 }
1434
1435 pub fn capacity_mib(&self) -> Option<u32> {
1437 self.capacity_mib
1438 }
1439
1440 pub fn labels(&self) -> &[(String, String)] {
1442 &self.labels
1443 }
1444}
1445
1446impl VolumeMount {
1447 pub fn guest(&self) -> &str {
1449 match self {
1450 Self::Bind { guest, .. }
1451 | Self::Owned { guest, .. }
1452 | Self::Named { guest, .. }
1453 | Self::Tmpfs { guest, .. }
1454 | Self::DiskImage { guest, .. } => guest,
1455 }
1456 }
1457
1458 fn guest_mut(&mut self) -> &mut String {
1459 match self {
1460 Self::Bind { guest, .. }
1461 | Self::Owned { guest, .. }
1462 | Self::Named { guest, .. }
1463 | Self::Tmpfs { guest, .. }
1464 | Self::DiskImage { guest, .. } => guest,
1465 }
1466 }
1467
1468 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1470 match self {
1471 Self::Named { create, .. } => create.as_ref(),
1472 _ => None,
1473 }
1474 }
1475}
1476
1477pub fn owned_volume_mount_id(guest: &str) -> String {
1484 use std::fmt::Write as _;
1485 let slug: String = guest
1486 .trim_start_matches('/')
1487 .chars()
1488 .take(11)
1489 .map(|character| {
1490 if character.is_ascii_alphanumeric() || character == '-' {
1491 character
1492 } else {
1493 '_'
1494 }
1495 })
1496 .collect();
1497 let mut id = if slug.is_empty() {
1498 String::new()
1499 } else {
1500 format!("{slug}_")
1501 };
1502 for byte in Sha256::digest(guest.as_bytes()).iter().take(4) {
1503 let _ = write!(id, "{byte:02x}");
1504 }
1505 id
1506}
1507
1508pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1515 for mount in mounts.iter_mut() {
1516 let canonical = canonical_guest_mount_path(mount.guest())?;
1517 *mount.guest_mut() = canonical;
1518 }
1519
1520 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1521
1522 for pair in mounts.windows(2) {
1523 if pair[0].guest() == pair[1].guest() {
1524 return Err(TypesError::invalid_config(format!(
1525 "multiple volumes cannot mount the same guest path: {}",
1526 pair[0].guest()
1527 )));
1528 }
1529 }
1530
1531 Ok(())
1532}
1533
1534fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1535 let path = Utf8UnixPath::new(guest);
1536
1537 if !path.is_valid() {
1538 return Err(TypesError::invalid_config(format!(
1539 "guest mount path must be a valid Unix path: {guest}"
1540 )));
1541 }
1542 if !path.is_absolute() {
1543 return Err(TypesError::invalid_config(format!(
1544 "guest mount path must be absolute: {guest}"
1545 )));
1546 }
1547 if path
1548 .components()
1549 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1550 {
1551 return Err(TypesError::invalid_config(format!(
1552 "guest mount path must not contain '..': {guest}"
1553 )));
1554 }
1555 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1556 return Err(TypesError::invalid_config(format!(
1557 "guest mount path must not contain ':', ';', or ',': {guest}"
1558 )));
1559 }
1560
1561 let canonical = path.normalize().to_string();
1562 if canonical == "/" {
1563 return Err(TypesError::invalid_config(
1564 "cannot mount a volume at guest root /",
1565 ));
1566 }
1567
1568 Ok(canonical)
1569}
1570
1571fn guest_mount_order_key(guest: &str) -> (usize, String) {
1572 let path = Utf8UnixPath::new(guest);
1573 let depth = path.components().filter(Utf8Component::is_normal).count();
1574 (depth, guest.to_owned())
1575}
1576
1577impl RlimitResource {
1578 pub fn as_str(&self) -> &'static str {
1580 match self {
1581 Self::Cpu => "cpu",
1582 Self::Fsize => "fsize",
1583 Self::Data => "data",
1584 Self::Stack => "stack",
1585 Self::Core => "core",
1586 Self::Rss => "rss",
1587 Self::Nproc => "nproc",
1588 Self::Nofile => "nofile",
1589 Self::Memlock => "memlock",
1590 Self::As => "as",
1591 Self::Locks => "locks",
1592 Self::Sigpending => "sigpending",
1593 Self::Msgqueue => "msgqueue",
1594 Self::Nice => "nice",
1595 Self::Rtprio => "rtprio",
1596 Self::Rttime => "rttime",
1597 }
1598 }
1599}
1600
1601impl LogSource {
1602 pub fn effective(requested: &[Self]) -> Vec<Self> {
1604 if requested.is_empty() {
1605 vec![Self::Stdout, Self::Stderr, Self::Output]
1606 } else {
1607 let mut sources = requested.to_vec();
1608 sources.sort_by_key(|src| match src {
1609 Self::Stdout => 0,
1610 Self::Stderr => 1,
1611 Self::Output => 2,
1612 Self::System => 3,
1613 });
1614 sources.dedup();
1615 sources
1616 }
1617 }
1618}
1619
1620impl SandboxLogLevel {
1621 pub const fn as_str(self) -> &'static str {
1623 match self {
1624 Self::Error => "error",
1625 Self::Warn => "warn",
1626 Self::Info => "info",
1627 Self::Debug => "debug",
1628 Self::Trace => "trace",
1629 }
1630 }
1631}
1632
1633impl std::fmt::Display for DiskImageFormat {
1638 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1639 f.write_str(self.as_str())
1640 }
1641}
1642
1643impl FromStr for DiskImageFormat {
1644 type Err = String;
1645
1646 fn from_str(s: &str) -> Result<Self, Self::Err> {
1647 match s {
1648 "qcow2" => Ok(Self::Qcow2),
1649 "raw" => Ok(Self::Raw),
1650 "vmdk" => Ok(Self::Vmdk),
1651 _ => Err(format!("unknown disk image format: {s}")),
1652 }
1653 }
1654}
1655
1656impl fmt::Display for TransparentHugePagePolicy {
1657 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1658 f.write_str(self.as_str())
1659 }
1660}
1661
1662impl FromStr for TransparentHugePagePolicy {
1663 type Err = String;
1664
1665 fn from_str(value: &str) -> Result<Self, Self::Err> {
1666 match value {
1667 "always" => Ok(Self::Always),
1668 "madvise" => Ok(Self::Madvise),
1669 "never" => Ok(Self::Never),
1670 _ => Err(format!(
1671 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1672 )),
1673 }
1674 }
1675}
1676
1677impl Default for RootfsSource {
1678 fn default() -> Self {
1679 Self::oci(String::new())
1680 }
1681}
1682
1683impl Default for SandboxResources {
1684 fn default() -> Self {
1685 Self {
1686 cpus: DEFAULT_SANDBOX_CPUS,
1687 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1688 max_cpus: DEFAULT_SANDBOX_CPUS,
1689 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1690 cpu_placement: CpuPlacement::Inherit,
1691 placement_profile: None,
1692 thp: TransparentHugePagePolicy::Madvise,
1693 }
1694 }
1695}
1696
1697impl<'de> Deserialize<'de> for SandboxResources {
1698 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1699 where
1700 D: serde::Deserializer<'de>,
1701 {
1702 #[derive(Deserialize)]
1703 struct RawResources {
1704 #[serde(default = "default_sandbox_cpus")]
1705 cpus: u8,
1706 #[serde(default = "default_sandbox_memory_mib")]
1707 memory_mib: u32,
1708 max_cpus: Option<u8>,
1709 max_memory_mib: Option<u32>,
1710 #[serde(default)]
1711 cpu_placement: CpuPlacement,
1712 #[serde(default)]
1713 placement_profile: Option<String>,
1714 #[serde(default)]
1715 thp: TransparentHugePagePolicy,
1716 }
1717
1718 let raw = RawResources::deserialize(deserializer)?;
1719 Ok(Self {
1720 cpus: raw.cpus,
1721 memory_mib: raw.memory_mib,
1722 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1726 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1727 cpu_placement: raw.cpu_placement,
1728 placement_profile: raw.placement_profile,
1729 thp: raw.thp,
1730 })
1731 }
1732}
1733
1734impl CpuPlacement {
1735 pub const fn is_inherit(&self) -> bool {
1737 matches!(self, Self::Inherit)
1738 }
1739}
1740
1741impl std::fmt::Display for CpuPlacement {
1742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1743 f.write_str(match self {
1744 Self::Inherit => "inherit",
1745 Self::Auto => "auto",
1746 Self::Spread => "spread",
1747 Self::Compact => "compact",
1748 })
1749 }
1750}
1751
1752impl FromStr for CpuPlacement {
1753 type Err = String;
1754
1755 fn from_str(value: &str) -> Result<Self, Self::Err> {
1756 match value {
1757 "inherit" => Ok(Self::Inherit),
1758 "auto" => Ok(Self::Auto),
1759 "spread" => Ok(Self::Spread),
1760 "compact" => Ok(Self::Compact),
1761 _ => Err(format!(
1762 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1763 )),
1764 }
1765 }
1766}
1767
1768impl Default for SandboxRuntimeOptions {
1769 fn default() -> Self {
1770 Self {
1771 workdir: None,
1772 shell: None,
1773 scripts: BTreeMap::new(),
1774 entrypoint: None,
1775 cmd: None,
1776 hostname: None,
1777 user: None,
1778 log_level: None,
1779 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1780 disable_metrics_sample: false,
1781 }
1782 }
1783}
1784
1785impl Default for NetworkSpec {
1786 fn default() -> Self {
1787 Self {
1788 enabled: true,
1789 interface: None,
1790 ports: Vec::new(),
1791 policy: None,
1792 dns: None,
1793 tls: None,
1794 strict: false,
1795 secrets: None,
1796 max_tcp_connections: None,
1797 max_udp_connections: None,
1798 rate_limiter: None,
1799 trust_host_cas: false,
1800 outbound_proxy: None,
1801 }
1802 }
1803}
1804
1805impl Default for PublishedPortSpec {
1806 fn default() -> Self {
1807 Self {
1808 host_port: 0,
1809 guest_port: 0,
1810 protocol: PortProtocol::Tcp,
1811 host_bind: "127.0.0.1".into(),
1812 }
1813 }
1814}
1815
1816impl From<(String, String)> for EnvVar {
1817 fn from((key, value): (String, String)) -> Self {
1818 Self { key, value }
1819 }
1820}
1821
1822impl From<EnvVar> for (String, String) {
1823 fn from(var: EnvVar) -> Self {
1824 (var.key, var.value)
1825 }
1826}
1827
1828impl FromStr for SandboxLogLevel {
1829 type Err = String;
1830
1831 fn from_str(s: &str) -> Result<Self, Self::Err> {
1832 match s {
1833 "error" => Ok(Self::Error),
1834 "warn" => Ok(Self::Warn),
1835 "info" => Ok(Self::Info),
1836 "debug" => Ok(Self::Debug),
1837 "trace" => Ok(Self::Trace),
1838 _ => Err(format!("unknown sandbox log level: {s}")),
1839 }
1840 }
1841}
1842
1843impl std::fmt::Display for SandboxLogLevel {
1844 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1845 formatter.write_str(self.as_str())
1846 }
1847}
1848
1849impl Serialize for VolumeMount {
1850 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1851 use serde::ser::SerializeMap;
1852
1853 match self {
1854 Self::Owned {
1855 guest,
1856 storage,
1857 options,
1858 stat_virtualization,
1859 host_permissions,
1860 } => {
1861 let mut map = serializer.serialize_map(Some(6))?;
1864 map.serialize_entry("type", "Owned")?;
1865 map.serialize_entry("guest", guest)?;
1866 map.serialize_entry("storage", storage)?;
1867 map.serialize_entry("options", options)?;
1868 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1869 map.serialize_entry("host_permissions", host_permissions)?;
1870 map.end()
1871 }
1872 Self::Bind {
1873 host,
1874 guest,
1875 options,
1876 stat_virtualization,
1877 host_permissions,
1878 follow_root_symlinks,
1879 quota_mib,
1880 } => {
1881 let mut map = serializer.serialize_map(Some(8))?;
1882 map.serialize_entry("type", "Bind")?;
1883 map.serialize_entry("host", host)?;
1884 map.serialize_entry("guest", guest)?;
1885 map.serialize_entry("options", options)?;
1886 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1887 map.serialize_entry("host_permissions", host_permissions)?;
1888 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1889 map.serialize_entry("quota_mib", quota_mib)?;
1890 map.end()
1891 }
1892 Self::Named {
1893 name,
1894 guest,
1895 create: _,
1896 options,
1897 stat_virtualization,
1898 host_permissions,
1899 follow_root_symlinks,
1900 } => {
1901 let mut map = serializer.serialize_map(Some(7))?;
1902 map.serialize_entry("type", "Named")?;
1903 map.serialize_entry("name", name)?;
1904 map.serialize_entry("guest", guest)?;
1905 map.serialize_entry("options", options)?;
1906 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1907 map.serialize_entry("host_permissions", host_permissions)?;
1908 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1909 map.end()
1910 }
1911 Self::Tmpfs {
1912 guest,
1913 size_mib,
1914 options,
1915 } => {
1916 let mut map = serializer.serialize_map(Some(4))?;
1917 map.serialize_entry("type", "Tmpfs")?;
1918 map.serialize_entry("guest", guest)?;
1919 map.serialize_entry("size_mib", size_mib)?;
1920 map.serialize_entry("options", options)?;
1921 map.end()
1922 }
1923 Self::DiskImage {
1924 host,
1925 guest,
1926 format,
1927 fstype,
1928 options,
1929 } => {
1930 let mut map = serializer.serialize_map(Some(6))?;
1931 map.serialize_entry("type", "DiskImage")?;
1932 map.serialize_entry("host", host)?;
1933 map.serialize_entry("guest", guest)?;
1934 map.serialize_entry("format", format)?;
1935 map.serialize_entry("fstype", fstype)?;
1936 map.serialize_entry("options", options)?;
1937 map.end()
1938 }
1939 }
1940 }
1941}
1942
1943impl<'de> Deserialize<'de> for VolumeMount {
1944 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1945 fn default_strict() -> StatVirtualization {
1946 StatVirtualization::Strict
1947 }
1948
1949 fn default_private() -> HostPermissions {
1950 HostPermissions::Private
1951 }
1952
1953 #[derive(Deserialize)]
1954 #[serde(tag = "type")]
1955 enum VolumeMountHelper {
1956 Owned {
1957 guest: String,
1958 storage: OwnedVolumeStorage,
1959 #[serde(default)]
1960 options: MountOptions,
1961 #[serde(default = "default_strict")]
1962 stat_virtualization: StatVirtualization,
1963 #[serde(default = "default_private")]
1964 host_permissions: HostPermissions,
1965 },
1966 Bind {
1967 host: PathBuf,
1968 guest: String,
1969 #[serde(default)]
1970 options: Option<MountOptions>,
1971 #[serde(default)]
1972 readonly: bool,
1973 #[serde(default = "default_strict")]
1974 stat_virtualization: StatVirtualization,
1975 #[serde(default = "default_private")]
1976 host_permissions: HostPermissions,
1977 #[serde(default)]
1978 follow_root_symlinks: bool,
1979 #[serde(default)]
1980 quota_mib: Option<u32>,
1981 },
1982 Named {
1983 name: String,
1984 guest: String,
1985 #[serde(default)]
1986 options: Option<MountOptions>,
1987 #[serde(default)]
1988 readonly: bool,
1989 #[serde(default = "default_strict")]
1990 stat_virtualization: StatVirtualization,
1991 #[serde(default = "default_private")]
1992 host_permissions: HostPermissions,
1993 #[serde(default)]
1994 follow_root_symlinks: bool,
1995 },
1996 Tmpfs {
1997 guest: String,
1998 #[serde(default)]
1999 size_mib: Option<u32>,
2000 #[serde(default)]
2001 options: Option<MountOptions>,
2002 #[serde(default)]
2003 readonly: bool,
2004 },
2005 DiskImage {
2006 host: PathBuf,
2007 guest: String,
2008 format: DiskImageFormat,
2009 #[serde(default)]
2010 fstype: Option<String>,
2011 #[serde(default)]
2012 options: Option<MountOptions>,
2013 #[serde(default)]
2014 readonly: bool,
2015 },
2016 }
2017
2018 let helper = VolumeMountHelper::deserialize(deserializer)?;
2019 Ok(match helper {
2020 VolumeMountHelper::Owned {
2021 guest,
2022 storage,
2023 options,
2024 stat_virtualization,
2025 host_permissions,
2026 } => Self::Owned {
2027 guest,
2028 storage,
2029 options,
2030 stat_virtualization,
2031 host_permissions,
2032 },
2033 VolumeMountHelper::Bind {
2034 host,
2035 guest,
2036 options,
2037 readonly,
2038 stat_virtualization,
2039 host_permissions,
2040 follow_root_symlinks,
2041 quota_mib,
2042 } => Self::Bind {
2043 host,
2044 guest,
2045 options: decode_mount_options(options, readonly),
2046 stat_virtualization,
2047 host_permissions,
2048 follow_root_symlinks,
2049 quota_mib,
2050 },
2051 VolumeMountHelper::Named {
2052 name,
2053 guest,
2054 options,
2055 readonly,
2056 stat_virtualization,
2057 host_permissions,
2058 follow_root_symlinks,
2059 } => Self::Named {
2060 name,
2061 guest,
2062 create: None,
2063 options: decode_mount_options(options, readonly),
2064 stat_virtualization,
2065 host_permissions,
2066 follow_root_symlinks,
2067 },
2068 VolumeMountHelper::Tmpfs {
2069 guest,
2070 size_mib,
2071 options,
2072 readonly,
2073 } => Self::Tmpfs {
2074 guest,
2075 size_mib,
2076 options: decode_mount_options(options, readonly),
2077 },
2078 VolumeMountHelper::DiskImage {
2079 host,
2080 guest,
2081 format,
2082 fstype,
2083 options,
2084 readonly,
2085 } => Self::DiskImage {
2086 host,
2087 guest,
2088 format,
2089 fstype,
2090 options: decode_mount_options(options, readonly),
2091 },
2092 })
2093 }
2094}
2095
2096impl fmt::Debug for VolumeMount {
2097 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2098 match self {
2099 Self::Owned {
2100 guest,
2101 storage,
2102 options,
2103 stat_virtualization,
2104 host_permissions,
2105 } => f
2106 .debug_struct("Owned")
2107 .field("guest", guest)
2108 .field("storage", storage)
2109 .field("options", options)
2110 .field("stat_virtualization", stat_virtualization)
2111 .field("host_permissions", host_permissions)
2112 .finish(),
2113 Self::Bind {
2114 host,
2115 guest,
2116 options,
2117 stat_virtualization,
2118 host_permissions,
2119 follow_root_symlinks,
2120 quota_mib,
2121 } => f
2122 .debug_struct("Bind")
2123 .field("host", host)
2124 .field("guest", guest)
2125 .field("options", options)
2126 .field("stat_virtualization", stat_virtualization)
2127 .field("host_permissions", host_permissions)
2128 .field("follow_root_symlinks", follow_root_symlinks)
2129 .field("quota_mib", quota_mib)
2130 .finish(),
2131 Self::Named {
2132 name,
2133 guest,
2134 create,
2135 options,
2136 stat_virtualization,
2137 host_permissions,
2138 follow_root_symlinks,
2139 } => f
2140 .debug_struct("Named")
2141 .field("name", name)
2142 .field("guest", guest)
2143 .field("create", create)
2144 .field("options", options)
2145 .field("stat_virtualization", stat_virtualization)
2146 .field("host_permissions", host_permissions)
2147 .field("follow_root_symlinks", follow_root_symlinks)
2148 .finish(),
2149 Self::Tmpfs {
2150 guest,
2151 size_mib,
2152 options,
2153 } => f
2154 .debug_struct("Tmpfs")
2155 .field("guest", guest)
2156 .field("size_mib", size_mib)
2157 .field("options", options)
2158 .finish(),
2159 Self::DiskImage {
2160 host,
2161 guest,
2162 format,
2163 fstype,
2164 options,
2165 } => f
2166 .debug_struct("DiskImage")
2167 .field("host", host)
2168 .field("guest", guest)
2169 .field("format", format)
2170 .field("fstype", fstype)
2171 .field("options", options)
2172 .finish(),
2173 }
2174 }
2175}
2176
2177impl TryFrom<&str> for RlimitResource {
2179 type Error = String;
2180
2181 fn try_from(s: &str) -> Result<Self, Self::Error> {
2182 match s.to_ascii_lowercase().as_str() {
2183 "cpu" => Ok(Self::Cpu),
2184 "fsize" => Ok(Self::Fsize),
2185 "data" => Ok(Self::Data),
2186 "stack" => Ok(Self::Stack),
2187 "core" => Ok(Self::Core),
2188 "rss" => Ok(Self::Rss),
2189 "nproc" => Ok(Self::Nproc),
2190 "nofile" => Ok(Self::Nofile),
2191 "memlock" => Ok(Self::Memlock),
2192 "as" => Ok(Self::As),
2193 "locks" => Ok(Self::Locks),
2194 "sigpending" => Ok(Self::Sigpending),
2195 "msgqueue" => Ok(Self::Msgqueue),
2196 "nice" => Ok(Self::Nice),
2197 "rtprio" => Ok(Self::Rtprio),
2198 "rttime" => Ok(Self::Rttime),
2199 _ => Err(format!("unknown rlimit resource: {s}")),
2200 }
2201 }
2202}
2203
2204fn default_sandbox_cpus() -> u8 {
2209 DEFAULT_SANDBOX_CPUS
2210}
2211
2212fn default_sandbox_memory_mib() -> u32 {
2213 DEFAULT_SANDBOX_MEMORY_MIB
2214}
2215
2216fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2217 options.unwrap_or(MountOptions {
2218 readonly,
2219 ..MountOptions::default()
2220 })
2221}
2222
2223fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2224 for value in higher {
2225 match base.iter_mut().find(|current| current.key == value.key) {
2226 Some(current) => *current = value,
2227 None => base.push(value),
2228 }
2229 }
2230}
2231
2232fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2233 for value in higher {
2234 match base
2235 .iter_mut()
2236 .find(|current| current.env_var == value.env_var)
2237 {
2238 Some(current) => *current = value,
2239 None => base.push(value),
2240 }
2241 }
2242}
2243
2244pub(crate) fn default_strict() -> StatVirtualization {
2246 StatVirtualization::Strict
2247}
2248
2249pub(crate) fn default_private() -> HostPermissions {
2251 HostPermissions::Private
2252}
2253
2254pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2256
2257#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2264#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2265#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2266pub struct SecretsConfig {
2267 #[serde(default)]
2269 #[config_patch(merge_with = merge_secret_entries)]
2270 pub secrets: Vec<SecretEntry>,
2271
2272 #[serde(default)]
2274 pub violation_action: SecretViolationAction,
2275}
2276
2277#[derive(Clone, Serialize, Deserialize)]
2282#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2283#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2284pub struct SecretEntry {
2285 pub env_var: String,
2291
2292 #[serde(default = "empty_secret_value")]
2301 #[cfg_attr(feature = "ts", ts(type = "string"))]
2302 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2303 pub value: Zeroizing<String>,
2304
2305 #[serde(default, skip_serializing_if = "Option::is_none")]
2309 pub source: Option<SecretSource>,
2310
2311 pub placeholder: String,
2316
2317 #[serde(default)]
2319 pub allowed_hosts: Vec<HostPattern>,
2320
2321 #[serde(default)]
2323 pub substitution: SecretSubstitution,
2324
2325 #[serde(default)]
2327 pub passthrough_hosts: Vec<HostPattern>,
2328
2329 #[serde(default, skip_serializing_if = "Option::is_none")]
2331 pub violation_action: Option<SecretViolationAction>,
2332
2333 #[serde(default = "default_true")]
2338 pub require_tls_identity: bool,
2339}
2340
2341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2343#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2344#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2345#[serde(rename_all = "kebab-case")]
2346pub enum HostPattern {
2347 #[serde(alias = "Exact")]
2349 Exact(String),
2350 #[serde(alias = "Wildcard")]
2352 Wildcard(String),
2353 #[serde(alias = "Any")]
2355 Any,
2356}
2357
2358#[derive(Debug, Clone, Serialize, Deserialize)]
2360#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2361#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2362pub struct SecretSubstitution {
2363 #[serde(default = "default_true")]
2365 pub headers: bool,
2366
2367 #[serde(default)]
2369 pub query: bool,
2370
2371 #[serde(default)]
2379 pub body: bool,
2380}
2381
2382#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2384#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2385#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2386#[serde(rename_all = "kebab-case")]
2387pub enum SecretViolationAction {
2388 #[serde(alias = "Block")]
2390 Block,
2391 #[default]
2393 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2394 BlockAndLog,
2395 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2397 BlockAndTerminate,
2398}
2399
2400#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2402pub enum SecretConfigError {
2403 #[error("secret #{secret_index}: env_var must not be empty")]
2405 EmptyEnvVar {
2406 secret_index: usize,
2408 },
2409
2410 #[error("secret #{secret_index}: env_var must not contain `=`")]
2412 EnvVarContainsEquals {
2413 secret_index: usize,
2415 },
2416
2417 #[error("secret #{secret_index}: env_var must not contain NUL")]
2419 EnvVarContainsNul {
2420 secret_index: usize,
2422 },
2423
2424 #[error("secret #{secret_index}: at least one allowed host is required")]
2426 MissingAllowedHosts {
2427 secret_index: usize,
2429 },
2430
2431 #[error("secret #{secret_index}: at least one substitution location is required")]
2433 MissingSubstitutionLocation {
2434 secret_index: usize,
2436 },
2437
2438 #[error("secret #{secret_index}: placeholder must not be empty")]
2440 EmptyPlaceholder {
2441 secret_index: usize,
2443 },
2444
2445 #[error(
2447 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2448 )]
2449 PlaceholderTooLong {
2450 secret_index: usize,
2452 actual_bytes: usize,
2454 max_bytes: usize,
2456 },
2457
2458 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2460 PlaceholderContainsNul {
2461 secret_index: usize,
2463 },
2464
2465 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2467 PlaceholderContainsLineBreak {
2468 secret_index: usize,
2470 },
2471}
2472
2473impl SecretsConfig {
2474 pub fn validate(&self) -> Result<(), SecretConfigError> {
2476 for (index, secret) in self.secrets.iter().enumerate() {
2477 secret.validate(index)?;
2478 }
2479 Ok(())
2480 }
2481}
2482
2483impl SecretEntry {
2484 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2486 validate_env_var(&self.env_var, secret_index)?;
2487
2488 if self.allowed_hosts.is_empty() {
2489 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2490 }
2491
2492 if !self.substitution.headers && !self.substitution.query && !self.substitution.body {
2493 return Err(SecretConfigError::MissingSubstitutionLocation { secret_index });
2494 }
2495
2496 validate_placeholder(&self.placeholder, secret_index)
2497 }
2498}
2499
2500impl fmt::Debug for SecretEntry {
2502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2503 f.debug_struct("SecretEntry")
2504 .field("env_var", &self.env_var)
2505 .field("value", &"[REDACTED]")
2506 .field("source", &self.source)
2507 .field("placeholder", &self.placeholder)
2508 .field("allowed_hosts", &self.allowed_hosts)
2509 .field("substitution", &self.substitution)
2510 .field("passthrough_hosts", &self.passthrough_hosts)
2511 .field("violation_action", &self.violation_action)
2512 .field("require_tls_identity", &self.require_tls_identity)
2513 .finish()
2514 }
2515}
2516
2517impl HostPattern {
2518 pub fn parse(host: &str) -> Self {
2521 if host == "*" {
2522 HostPattern::Any
2523 } else if host.starts_with("*.") {
2524 HostPattern::Wildcard(host.to_string())
2525 } else {
2526 HostPattern::Exact(host.to_string())
2527 }
2528 }
2529
2530 pub fn matches(&self, hostname: &str) -> bool {
2535 match self {
2536 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2537 HostPattern::Wildcard(pattern) => {
2538 if let Some(suffix) = pattern.strip_prefix("*.") {
2539 hostname.eq_ignore_ascii_case(suffix)
2540 || (hostname.len() > suffix.len() + 1
2541 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2542 && hostname[hostname.len() - suffix.len()..]
2543 .eq_ignore_ascii_case(suffix))
2544 } else {
2545 hostname.eq_ignore_ascii_case(pattern)
2546 }
2547 }
2548 HostPattern::Any => true,
2549 }
2550 }
2551}
2552
2553impl Default for SecretSubstitution {
2554 fn default() -> Self {
2555 Self {
2556 headers: true,
2557 query: false,
2558 body: false,
2559 }
2560 }
2561}
2562
2563fn default_true() -> bool {
2564 true
2565}
2566
2567fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2568 if env_var.is_empty() {
2569 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2570 }
2571 if env_var.contains('=') {
2572 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2573 }
2574 if env_var.contains('\0') {
2575 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2576 }
2577 Ok(())
2578}
2579
2580fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2581 if placeholder.is_empty() {
2582 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2583 }
2584
2585 let actual_bytes = placeholder.len();
2586 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2587 return Err(SecretConfigError::PlaceholderTooLong {
2588 secret_index,
2589 actual_bytes,
2590 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2591 });
2592 }
2593
2594 if placeholder.contains('\0') {
2595 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2596 }
2597 if placeholder.contains('\r') || placeholder.contains('\n') {
2598 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2599 }
2600
2601 Ok(())
2602}
2603
2604#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2614#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2615#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2616pub struct TlsConfig {
2617 #[serde(default)]
2619 pub enabled: bool,
2620
2621 #[serde(default = "default_intercepted_ports")]
2623 pub intercepted_ports: Vec<u16>,
2624
2625 #[serde(default)]
2627 pub bypass: Vec<String>,
2628
2629 #[serde(default = "default_true")]
2631 pub verify_upstream: bool,
2632
2633 #[serde(default = "default_true")]
2636 pub block_quic_on_intercept: bool,
2637
2638 #[serde(default)]
2640 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2641 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2642 pub upstream_ca_cert: Vec<PathBuf>,
2643
2644 #[serde(default, alias = "scoped_upstream_ca_certs")]
2646 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2647
2648 #[serde(default)]
2650 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2651
2652 #[serde(default, alias = "ca")]
2655 pub intercept_ca: InterceptCaConfig,
2656
2657 #[serde(default)]
2659 pub cache: CertCacheConfig,
2660}
2661
2662#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2664#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2665#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2666pub struct InterceptCaConfig {
2667 #[serde(default)]
2670 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2671 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2672 pub cert_path: Option<PathBuf>,
2673
2674 #[serde(default)]
2677 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2678 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2679 pub key_path: Option<PathBuf>,
2680}
2681
2682#[derive(Debug, Clone, Serialize, Deserialize)]
2684#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2685#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2686pub struct CertCacheConfig {
2687 #[serde(default = "default_cache_capacity")]
2689 pub capacity: usize,
2690
2691 #[serde(default = "default_cert_validity_hours")]
2693 pub validity_hours: u64,
2694}
2695
2696#[derive(Debug, Clone, Serialize, Deserialize)]
2698#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2699#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2700pub struct ScopedUpstreamCaCert {
2701 pub pattern: String,
2703
2704 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2706 #[cfg_attr(feature = "ts", ts(type = "string"))]
2707 pub path: PathBuf,
2708}
2709
2710#[derive(Debug, Clone, Serialize, Deserialize)]
2712#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2713#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2714pub struct ScopedVerifyUpstream {
2715 pub pattern: String,
2717
2718 pub verify: bool,
2720}
2721
2722impl Default for TlsConfig {
2723 fn default() -> Self {
2724 Self {
2725 enabled: false,
2726 intercepted_ports: default_intercepted_ports(),
2727 bypass: Vec::new(),
2728 verify_upstream: true,
2729 block_quic_on_intercept: true,
2730 upstream_ca_cert: Vec::new(),
2731 scoped_upstream_ca_cert: Vec::new(),
2732 scoped_verify_upstream: Vec::new(),
2733 intercept_ca: InterceptCaConfig::default(),
2734 cache: CertCacheConfig::default(),
2735 }
2736 }
2737}
2738
2739impl Default for CertCacheConfig {
2740 fn default() -> Self {
2741 Self {
2742 capacity: default_cache_capacity(),
2743 validity_hours: default_cert_validity_hours(),
2744 }
2745 }
2746}
2747
2748fn default_intercepted_ports() -> Vec<u16> {
2749 vec![443]
2750}
2751
2752fn default_cache_capacity() -> usize {
2753 1000
2754}
2755
2756fn default_cert_validity_hours() -> u64 {
2757 24
2758}
2759
2760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2766#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2767#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2768#[serde(rename_all = "snake_case")]
2769pub enum Action {
2770 Allow,
2772 Deny,
2774}
2775
2776#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2778#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2779#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2780#[serde(rename_all = "snake_case")]
2781pub enum Direction {
2782 Egress,
2784 Ingress,
2786 Any,
2788}
2789
2790#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2792#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2793#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2794#[serde(rename_all = "snake_case")]
2795pub enum Protocol {
2796 Tcp,
2798 Udp,
2800 Icmpv4,
2802 Icmpv6,
2804}
2805
2806#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2808#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2809#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2810#[serde(rename_all = "snake_case")]
2811pub enum DestinationGroup {
2812 Public,
2814 Loopback,
2816 Private,
2818 LinkLocal,
2820 Metadata,
2822 Multicast,
2824 Host,
2826}
2827
2828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2835#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2836#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2837#[serde(rename_all = "snake_case")]
2838pub enum Destination {
2839 Any,
2841 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2843 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2844 Domain(String),
2846 DomainSuffix(String),
2848 Group(DestinationGroup),
2850}
2851
2852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2854#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2855#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2856pub struct PortRange {
2857 pub start: u16,
2859 pub end: u16,
2861}
2862
2863#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2866#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2867#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2868pub struct Rule {
2869 pub direction: Direction,
2871 pub destination: Destination,
2873 #[serde(default)]
2875 pub protocols: Vec<Protocol>,
2876 #[serde(default)]
2878 pub ports: Vec<PortRange>,
2879 pub action: Action,
2881}
2882
2883#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2886#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2887#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2888pub struct NetworkPolicy {
2889 #[serde(default = "action_deny")]
2891 pub default_egress: Action,
2892 #[serde(default = "action_deny")]
2894 pub default_ingress: Action,
2895 #[serde(default)]
2897 pub rules: Vec<Rule>,
2898}
2899
2900fn action_deny() -> Action {
2903 Action::Deny
2904}
2905
2906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2912#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2913#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2914#[serde(default)]
2915pub struct DnsConfig {
2916 pub rebind_protection: bool,
2918 pub nameservers: Vec<String>,
2921 pub query_timeout_ms: u64,
2923}
2924
2925impl Default for DnsConfig {
2926 fn default() -> Self {
2927 Self {
2928 rebind_protection: true,
2929 nameservers: Vec::new(),
2930 query_timeout_ms: 5000,
2931 }
2932 }
2933}
2934
2935#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2939#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2940#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2941#[serde(default)]
2942pub struct InterfaceOverrides {
2943 #[serde(skip_serializing_if = "Option::is_none")]
2945 pub mac: Option<[u8; 6]>,
2946 #[serde(skip_serializing_if = "Option::is_none")]
2948 pub mtu: Option<u16>,
2949 #[serde(skip_serializing_if = "Option::is_none")]
2951 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2952 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2953 pub ipv4_address: Option<Ipv4Addr>,
2954 #[serde(skip_serializing_if = "Option::is_none")]
2956 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2957 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2958 pub ipv4_pool: Option<Ipv4Network>,
2959 #[serde(skip_serializing_if = "Option::is_none")]
2961 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2962 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2963 pub ipv6_address: Option<Ipv6Addr>,
2964 #[serde(skip_serializing_if = "Option::is_none")]
2966 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2967 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2968 pub ipv6_pool: Option<Ipv6Network>,
2969}
2970
2971fn empty_secret_value() -> Zeroizing<String> {
2972 Zeroizing::new(String::new())
2973}
2974
2975#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2981pub enum NetworkRateLimitDirection {
2982 Egress,
2984 Ingress,
2986}
2987
2988#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2990#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2991#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2992#[serde(default)]
2993pub struct NetworkRateLimiterConfig {
2994 #[serde(skip_serializing_if = "Option::is_none")]
2996 pub egress: Option<RateLimiterConfig>,
2997
2998 #[serde(skip_serializing_if = "Option::is_none")]
3000 pub ingress: Option<RateLimiterConfig>,
3001}
3002
3003#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3009#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3010#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3011#[serde(default)]
3012pub struct RateLimiterConfig {
3013 #[serde(skip_serializing_if = "Option::is_none")]
3015 pub bandwidth: Option<TokenBucketConfig>,
3016
3017 #[serde(skip_serializing_if = "Option::is_none")]
3019 pub ops: Option<TokenBucketConfig>,
3020}
3021
3022#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3028#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3029#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3030pub struct TokenBucketConfig {
3031 pub size: u64,
3033
3034 pub refill_time_ms: u64,
3037
3038 #[serde(default)]
3040 pub one_time_burst: u64,
3041}
3042
3043#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3045pub enum RateLimitConfigError {
3046 #[error("rate limiter must configure at least one of bandwidth or ops")]
3048 EmptyLimiter,
3049
3050 #[error("{bucket} bucket: size must be greater than zero")]
3052 ZeroSize {
3053 bucket: &'static str,
3055 },
3056
3057 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
3059 ZeroRefillTime {
3060 bucket: &'static str,
3062 },
3063}
3064
3065impl RateLimiterConfig {
3066 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
3068 if self.bandwidth.is_none() && self.ops.is_none() {
3069 return Err(RateLimitConfigError::EmptyLimiter);
3070 }
3071 if let Some(bandwidth) = &self.bandwidth {
3072 bandwidth.validate("bandwidth")?;
3073 }
3074 if let Some(ops) = &self.ops {
3075 ops.validate("ops")?;
3076 }
3077 Ok(())
3078 }
3079}
3080
3081impl TokenBucketConfig {
3082 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
3084 if self.size == 0 {
3085 return Err(RateLimitConfigError::ZeroSize { bucket });
3086 }
3087 if self.refill_time_ms == 0 {
3088 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
3089 }
3090 Ok(())
3091 }
3092}
3093
3094impl fmt::Display for NetworkRateLimitDirection {
3095 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3096 match self {
3097 Self::Egress => f.write_str("egress"),
3098 Self::Ingress => f.write_str("ingress"),
3099 }
3100 }
3101}
3102
3103#[cfg(test)]
3108mod tests {
3109 use super::*;
3110
3111 fn tmpfs_mount(guest: &str) -> VolumeMount {
3112 VolumeMount::Tmpfs {
3113 guest: guest.to_owned(),
3114 size_mib: None,
3115 options: MountOptions::default(),
3116 }
3117 }
3118
3119 #[test]
3120 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
3121 let value = serde_json::to_value(MountOptions::default()).unwrap();
3122 assert!(value.get("override_uid").is_none());
3123 assert!(value.get("override_gid").is_none());
3124
3125 let decoded: MountOptions = serde_json::from_value(value).unwrap();
3126 assert_eq!(decoded.override_uid, None);
3127 assert_eq!(decoded.override_gid, None);
3128 }
3129
3130 #[test]
3131 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
3132 let mut mounts = vec![
3133 tmpfs_mount("/workspace//persist/./logs/"),
3134 tmpfs_mount("/alpha/z"),
3135 tmpfs_mount("/workspace"),
3136 ];
3137
3138 canonicalize_volume_mounts(&mut mounts).unwrap();
3139
3140 assert_eq!(
3141 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
3142 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
3143 );
3144 }
3145
3146 #[test]
3147 fn volume_mounts_reject_duplicate_canonical_paths() {
3148 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
3149
3150 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3151
3152 assert!(error.to_string().contains("same guest path: /data/cache"));
3153 }
3154
3155 #[test]
3156 fn volume_mounts_reject_parent_components_before_normalizing() {
3157 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3158
3159 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3160
3161 assert!(error.to_string().contains("must not contain '..'"));
3162 }
3163
3164 #[test]
3165 fn disk_image_format_from_extension() {
3166 assert_eq!(
3167 DiskImageFormat::from_extension("qcow2"),
3168 Some(DiskImageFormat::Qcow2)
3169 );
3170 assert_eq!(
3171 DiskImageFormat::from_extension("raw"),
3172 Some(DiskImageFormat::Raw)
3173 );
3174 assert_eq!(
3175 DiskImageFormat::from_extension("vmdk"),
3176 Some(DiskImageFormat::Vmdk)
3177 );
3178 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3179 assert_eq!(DiskImageFormat::from_extension(""), None);
3180 }
3181
3182 #[test]
3183 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3184 let resources: SandboxResources =
3185 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3186
3187 assert_eq!(resources.cpus, 4);
3188 assert_eq!(resources.max_cpus, 4);
3189 assert_eq!(resources.memory_mib, 2048);
3190 assert_eq!(resources.max_memory_mib, 2048);
3191 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3192 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3193 assert_eq!(
3194 serde_json::to_value(resources).unwrap(),
3195 serde_json::json!({
3196 "cpus": 4,
3197 "memory_mib": 2048,
3198 "max_cpus": 4,
3199 "max_memory_mib": 2048
3200 })
3201 );
3202 }
3203
3204 #[test]
3205 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3206 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3207 assert!(inherited.get("cpu_placement").is_none());
3208
3209 for policy in [
3210 CpuPlacement::Auto,
3211 CpuPlacement::Spread,
3212 CpuPlacement::Compact,
3213 ] {
3214 let resources = SandboxResources {
3215 cpu_placement: policy,
3216 ..Default::default()
3217 };
3218 let json = serde_json::to_string(&resources).unwrap();
3219 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3220
3221 assert_eq!(decoded.cpu_placement, policy);
3222 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3223 }
3224 }
3225
3226 #[test]
3227 fn transparent_huge_page_policy_roundtrips_non_default() {
3228 let resources: SandboxResources = serde_json::from_str(
3229 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3230 )
3231 .unwrap();
3232
3233 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3234 assert_eq!(
3235 serde_json::to_value(resources).unwrap()["thp"],
3236 serde_json::json!("always")
3237 );
3238 assert_eq!(
3239 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3240 TransparentHugePagePolicy::Never
3241 );
3242 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3243 }
3244
3245 #[test]
3246 fn disk_image_format_display_roundtrip() {
3247 for format in [
3248 DiskImageFormat::Qcow2,
3249 DiskImageFormat::Raw,
3250 DiskImageFormat::Vmdk,
3251 ] {
3252 let rendered = format.to_string();
3253 let parsed: DiskImageFormat = rendered.parse().unwrap();
3254 assert_eq!(parsed, format);
3255 }
3256 }
3257
3258 #[test]
3259 fn disk_image_format_from_str_unknown() {
3260 assert!("ext4".parse::<DiskImageFormat>().is_err());
3261 }
3262
3263 #[test]
3264 fn log_source_effective_uses_default_user_program_sources() {
3265 assert_eq!(
3266 LogSource::effective(&[]),
3267 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3268 );
3269 }
3270
3271 #[test]
3272 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3273 assert_eq!(
3274 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3275 vec![LogSource::Stdout, LogSource::System]
3276 );
3277 }
3278
3279 #[test]
3280 fn rlimit_resource_parses_case_insensitively() {
3281 assert_eq!(
3282 RlimitResource::try_from("NOFILE").unwrap(),
3283 RlimitResource::Nofile
3284 );
3285 assert!(RlimitResource::try_from("bogus").is_err());
3286 }
3287
3288 #[test]
3289 fn sandbox_policy_serde_roundtrip() {
3290 let policy = SandboxPolicy {
3291 ephemeral: true,
3292 max_duration_secs: Some(3600),
3293 idle_timeout_secs: Some(120),
3294 };
3295
3296 let json = serde_json::to_string(&policy).unwrap();
3297 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3298
3299 assert!(decoded.ephemeral);
3300 assert_eq!(decoded.max_duration_secs, Some(3600));
3301 assert_eq!(decoded.idle_timeout_secs, Some(120));
3302 }
3303
3304 #[test]
3305 fn sandbox_policy_defaults_to_persistent() {
3306 assert!(!SandboxPolicy::default().ephemeral);
3307 }
3308
3309 #[test]
3310 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3311 let decoded: SandboxPolicy =
3314 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3315 assert!(!decoded.ephemeral);
3316 assert_eq!(decoded.max_duration_secs, Some(60));
3317 }
3318
3319 #[test]
3320 fn sandbox_spec_default_uses_static_resource_defaults() {
3321 let spec = SandboxSpec::default();
3322
3323 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3324 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3325 assert_eq!(
3326 spec.runtime.metrics_sample_interval_ms,
3327 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3328 );
3329 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3330 }
3331
3332 #[test]
3333 fn deployment_profile_uses_stable_snake_case_wire_values() {
3334 assert_eq!(
3335 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3336 r#""multi_tenant""#
3337 );
3338 assert_eq!(
3339 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3340 DeploymentProfile::SingleTenant
3341 );
3342 }
3343
3344 #[test]
3345 fn sandbox_log_level_roundtrips_lowercase_values() {
3346 for (input, expected) in [
3347 ("error", SandboxLogLevel::Error),
3348 ("warn", SandboxLogLevel::Warn),
3349 ("info", SandboxLogLevel::Info),
3350 ("debug", SandboxLogLevel::Debug),
3351 ("trace", SandboxLogLevel::Trace),
3352 ] {
3353 let parsed: SandboxLogLevel = input.parse().unwrap();
3354 assert_eq!(parsed, expected);
3355 assert_eq!(parsed.as_str(), input);
3356 }
3357 }
3358}