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 #[serde(default)]
831 pub guest_flush: crate::GuestFlush,
832 pub name: String,
834
835 #[serde(default)]
837 pub group: Option<String>,
838
839 #[serde(default)]
841 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
842 pub dest_dir: Option<PathBuf>,
843
844 pub source_sandbox: String,
846
847 pub labels: Vec<(String, String)>,
849
850 pub force: bool,
852
853 pub record_integrity: bool,
855
856 #[serde(default)]
858 pub full: bool,
859}
860
861#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
869#[config_patch(name = SandboxConfigPatch)]
870#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
871#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
872#[serde(default)]
873pub struct SandboxSpec {
874 pub name: String,
876
877 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
879 pub image: RootfsSource,
880
881 #[config_patch(nested)]
883 pub resources: SandboxResources,
884
885 #[config_patch(nested)]
887 pub runtime: SandboxRuntimeOptions,
888
889 #[config_patch(merge_with = merge_env_vars)]
891 pub env: Vec<EnvVar>,
892
893 #[config_patch(merge)]
895 pub labels: BTreeMap<String, String>,
896
897 pub rlimits: Vec<Rlimit>,
899
900 pub mounts: Vec<VolumeMount>,
902
903 pub patches: Vec<Patch>,
905
906 #[config_patch(nested)]
908 pub network: NetworkSpec,
909
910 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
912 #[config_patch(nested)]
913 pub vsock: VsockSpec,
914
915 pub init: Option<HandoffInit>,
917
918 pub pull_policy: PullPolicy,
920
921 pub security_profile: SecurityProfile,
923
924 pub deployment_profile: DeploymentProfile,
930
931 #[config_patch(nested)]
933 pub lifecycle: SandboxPolicy,
934}
935
936#[derive(Debug, Clone, Serialize, ConfigPatch)]
938#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
939#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
940pub struct SandboxResources {
941 pub cpus: u8,
943
944 pub memory_mib: u32,
946
947 pub max_cpus: u8,
949
950 pub max_memory_mib: u32,
952
953 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
955 pub cpu_placement: CpuPlacement,
956
957 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub placement_profile: Option<String>,
960
961 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
963 pub thp: TransparentHugePagePolicy,
964}
965
966#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
968#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
969#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
970#[serde(rename_all = "lowercase")]
971pub enum CpuPlacement {
972 #[default]
974 Inherit,
975
976 Auto,
978
979 Spread,
981
982 Compact,
984}
985
986#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
988#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
989#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
990#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
991pub enum NumaPlacement {
992 PreferSingle,
994 StrictSingle,
996 Inherit,
998}
999
1000#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1002#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1003#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1004#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1005pub enum MemoryPlacement {
1006 FollowCpu,
1008 Inherit,
1010}
1011
1012#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1014#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1015#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1016#[serde(deny_unknown_fields)]
1017pub struct PlacementProfile {
1018 pub numa: NumaPlacement,
1020 pub memory: MemoryPlacement,
1022}
1023
1024#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1026#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1027#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1028#[serde(rename_all = "lowercase")]
1029pub enum TransparentHugePagePolicy {
1030 Always,
1032
1033 #[default]
1035 Madvise,
1036
1037 Never,
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1043#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1044#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1045#[serde(default)]
1046pub struct SandboxRuntimeOptions {
1047 pub workdir: Option<String>,
1049
1050 pub shell: Option<String>,
1052
1053 #[config_patch(merge)]
1055 pub scripts: BTreeMap<String, String>,
1056
1057 pub entrypoint: Option<Vec<String>>,
1059
1060 pub cmd: Option<Vec<String>>,
1062
1063 pub hostname: Option<String>,
1065
1066 pub user: Option<String>,
1068
1069 pub log_level: Option<SandboxLogLevel>,
1071
1072 pub metrics_sample_interval_ms: Option<u64>,
1074
1075 pub disable_metrics_sample: bool,
1077}
1078
1079#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1081#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1082#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1083pub struct EnvVar {
1084 pub key: String,
1086
1087 pub value: String,
1089}
1090
1091#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1093#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1094#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1095#[serde(rename_all = "lowercase")]
1096pub enum SandboxLogLevel {
1097 Error,
1099
1100 Warn,
1102
1103 Info,
1105
1106 Debug,
1108
1109 Trace,
1111}
1112
1113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1119#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1120#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1121pub enum RlimitResource {
1122 Cpu,
1124 Fsize,
1126 Data,
1128 Stack,
1130 Core,
1132 Rss,
1134 Nproc,
1136 Nofile,
1138 Memlock,
1140 As,
1142 Locks,
1144 Sigpending,
1146 Msgqueue,
1148 Nice,
1150 Rtprio,
1152 Rttime,
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize)]
1158#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1159#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1160pub struct Rlimit {
1161 pub resource: RlimitResource,
1163
1164 pub soft: u64,
1166
1167 pub hard: u64,
1169}
1170
1171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1177#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1178#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1179#[serde(rename_all = "lowercase")]
1180pub enum LogSource {
1181 Stdout,
1183
1184 Stderr,
1186
1187 Output,
1189
1190 System,
1192}
1193
1194impl SandboxResourcesPatch {
1199 pub fn has_cpus(&self) -> bool {
1201 self.cpus.is_some()
1202 }
1203
1204 pub fn has_memory_mib(&self) -> bool {
1206 self.memory_mib.is_some()
1207 }
1208
1209 pub fn has_max_cpus(&self) -> bool {
1211 self.max_cpus.is_some()
1212 }
1213
1214 pub fn has_max_memory_mib(&self) -> bool {
1216 self.max_memory_mib.is_some()
1217 }
1218}
1219
1220impl DiskImageFormat {
1221 pub fn as_str(&self) -> &'static str {
1223 match self {
1224 Self::Qcow2 => "qcow2",
1225 Self::Raw => "raw",
1226 Self::Vmdk => "vmdk",
1227 }
1228 }
1229
1230 pub fn from_extension(ext: &str) -> Option<Self> {
1234 match ext {
1235 "qcow2" => Some(Self::Qcow2),
1236 "raw" => Some(Self::Raw),
1237 "vmdk" => Some(Self::Vmdk),
1238 _ => None,
1239 }
1240 }
1241}
1242
1243impl OciRootfsSource {
1244 pub fn new(reference: impl Into<String>) -> Self {
1246 Self {
1247 reference: reference.into(),
1248 root_disk: None,
1249 }
1250 }
1251}
1252
1253impl TransparentHugePagePolicy {
1254 pub fn is_madvise(&self) -> bool {
1256 matches!(self, Self::Madvise)
1257 }
1258
1259 pub fn as_str(self) -> &'static str {
1261 match self {
1262 Self::Always => "always",
1263 Self::Madvise => "madvise",
1264 Self::Never => "never",
1265 }
1266 }
1267}
1268
1269impl RootDisk {
1270 pub fn managed(size_mib: u32) -> Self {
1272 Self::Managed {
1273 size_mib: Some(size_mib),
1274 }
1275 }
1276
1277 pub fn tmpfs(size_mib: u32) -> Self {
1279 Self::Tmpfs {
1280 size_mib: Some(size_mib),
1281 }
1282 }
1283
1284 pub fn flat(size_mib: u32) -> Self {
1286 Self::Flat {
1287 size_mib: Some(size_mib),
1288 fstype: None,
1289 clone: FlatClone::Auto,
1290 }
1291 }
1292
1293 pub fn size_mib(&self) -> Option<u32> {
1295 match self {
1296 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1297 *size_mib
1298 }
1299 Self::DiskImage { .. } => None,
1300 }
1301 }
1302
1303 pub fn kind_str(&self) -> &'static str {
1305 match self {
1306 Self::Managed { .. } => "managed",
1307 Self::Tmpfs { .. } => "tmpfs",
1308 Self::DiskImage { .. } => "disk-image",
1309 Self::Flat { .. } => "flat",
1310 }
1311 }
1312
1313 pub fn is_managed(&self) -> bool {
1315 matches!(self, Self::Managed { .. })
1316 }
1317}
1318
1319impl FlatClone {
1320 pub const fn as_str(self) -> &'static str {
1322 match self {
1323 Self::Auto => "auto",
1324 Self::Copy => "copy",
1325 Self::Reflink => "reflink",
1326 }
1327 }
1328
1329 pub const fn is_auto(&self) -> bool {
1331 matches!(self, Self::Auto)
1332 }
1333}
1334
1335impl RootfsSource {
1336 pub fn oci(reference: impl Into<String>) -> Self {
1338 Self::Oci(OciRootfsSource::new(reference))
1339 }
1340
1341 pub fn oci_reference(&self) -> Option<&str> {
1343 match self {
1344 Self::Oci(oci) => Some(&oci.reference),
1345 _ => None,
1346 }
1347 }
1348
1349 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1351 match self {
1352 Self::Oci(oci) => oci.root_disk.as_ref(),
1353 _ => None,
1354 }
1355 }
1356
1357 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1360 match self {
1361 Self::Oci(oci) => match &oci.root_disk {
1362 Some(RootDisk::Managed { size_mib }) => *size_mib,
1363 Some(_) => None,
1364 None => None,
1365 },
1366 _ => None,
1367 }
1368 }
1369}
1370
1371impl EnvVar {
1372 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1374 Self {
1375 key: key.into(),
1376 value: value.into(),
1377 }
1378 }
1379
1380 pub fn as_pair(&self) -> (&str, &str) {
1382 (&self.key, &self.value)
1383 }
1384}
1385
1386impl VolumeKind {
1387 pub fn as_str(self) -> &'static str {
1389 match self {
1390 Self::Directory => "dir",
1391 Self::Disk => "disk",
1392 }
1393 }
1394
1395 pub fn from_db_value(value: &str) -> Self {
1397 match value {
1398 "disk" => Self::Disk,
1399 _ => Self::Directory,
1400 }
1401 }
1402}
1403
1404impl VolumeSpec {
1405 pub fn new(name: impl Into<String>) -> Self {
1407 Self {
1408 name: name.into(),
1409 kind: VolumeKind::Directory,
1410 quota_mib: None,
1411 capacity_mib: None,
1412 labels: Vec::new(),
1413 }
1414 }
1415}
1416
1417impl NamedVolumeCreate {
1418 pub fn mode(&self) -> NamedVolumeMode {
1420 self.mode
1421 }
1422
1423 pub fn name(&self) -> &str {
1425 &self.name
1426 }
1427
1428 pub fn kind(&self) -> VolumeKind {
1430 self.kind
1431 }
1432
1433 pub fn quota_mib(&self) -> Option<u32> {
1435 self.quota_mib
1436 }
1437
1438 pub fn capacity_mib(&self) -> Option<u32> {
1440 self.capacity_mib
1441 }
1442
1443 pub fn labels(&self) -> &[(String, String)] {
1445 &self.labels
1446 }
1447}
1448
1449impl VolumeMount {
1450 pub fn guest(&self) -> &str {
1452 match self {
1453 Self::Bind { guest, .. }
1454 | Self::Owned { guest, .. }
1455 | Self::Named { guest, .. }
1456 | Self::Tmpfs { guest, .. }
1457 | Self::DiskImage { guest, .. } => guest,
1458 }
1459 }
1460
1461 fn guest_mut(&mut self) -> &mut String {
1462 match self {
1463 Self::Bind { guest, .. }
1464 | Self::Owned { guest, .. }
1465 | Self::Named { guest, .. }
1466 | Self::Tmpfs { guest, .. }
1467 | Self::DiskImage { guest, .. } => guest,
1468 }
1469 }
1470
1471 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1473 match self {
1474 Self::Named { create, .. } => create.as_ref(),
1475 _ => None,
1476 }
1477 }
1478}
1479
1480pub fn owned_volume_mount_id(guest: &str) -> String {
1487 use std::fmt::Write as _;
1488 let slug: String = guest
1489 .trim_start_matches('/')
1490 .chars()
1491 .take(11)
1492 .map(|character| {
1493 if character.is_ascii_alphanumeric() || character == '-' {
1494 character
1495 } else {
1496 '_'
1497 }
1498 })
1499 .collect();
1500 let mut id = if slug.is_empty() {
1501 String::new()
1502 } else {
1503 format!("{slug}_")
1504 };
1505 for byte in Sha256::digest(guest.as_bytes()).iter().take(4) {
1506 let _ = write!(id, "{byte:02x}");
1507 }
1508 id
1509}
1510
1511pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1518 for mount in mounts.iter_mut() {
1519 let canonical = canonical_guest_mount_path(mount.guest())?;
1520 *mount.guest_mut() = canonical;
1521 }
1522
1523 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1524
1525 for pair in mounts.windows(2) {
1526 if pair[0].guest() == pair[1].guest() {
1527 return Err(TypesError::invalid_config(format!(
1528 "multiple volumes cannot mount the same guest path: {}",
1529 pair[0].guest()
1530 )));
1531 }
1532 }
1533
1534 Ok(())
1535}
1536
1537fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1538 let path = Utf8UnixPath::new(guest);
1539
1540 if !path.is_valid() {
1541 return Err(TypesError::invalid_config(format!(
1542 "guest mount path must be a valid Unix path: {guest}"
1543 )));
1544 }
1545 if !path.is_absolute() {
1546 return Err(TypesError::invalid_config(format!(
1547 "guest mount path must be absolute: {guest}"
1548 )));
1549 }
1550 if path
1551 .components()
1552 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1553 {
1554 return Err(TypesError::invalid_config(format!(
1555 "guest mount path must not contain '..': {guest}"
1556 )));
1557 }
1558 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1559 return Err(TypesError::invalid_config(format!(
1560 "guest mount path must not contain ':', ';', or ',': {guest}"
1561 )));
1562 }
1563
1564 let canonical = path.normalize().to_string();
1565 if canonical == "/" {
1566 return Err(TypesError::invalid_config(
1567 "cannot mount a volume at guest root /",
1568 ));
1569 }
1570
1571 Ok(canonical)
1572}
1573
1574fn guest_mount_order_key(guest: &str) -> (usize, String) {
1575 let path = Utf8UnixPath::new(guest);
1576 let depth = path.components().filter(Utf8Component::is_normal).count();
1577 (depth, guest.to_owned())
1578}
1579
1580impl RlimitResource {
1581 pub fn as_str(&self) -> &'static str {
1583 match self {
1584 Self::Cpu => "cpu",
1585 Self::Fsize => "fsize",
1586 Self::Data => "data",
1587 Self::Stack => "stack",
1588 Self::Core => "core",
1589 Self::Rss => "rss",
1590 Self::Nproc => "nproc",
1591 Self::Nofile => "nofile",
1592 Self::Memlock => "memlock",
1593 Self::As => "as",
1594 Self::Locks => "locks",
1595 Self::Sigpending => "sigpending",
1596 Self::Msgqueue => "msgqueue",
1597 Self::Nice => "nice",
1598 Self::Rtprio => "rtprio",
1599 Self::Rttime => "rttime",
1600 }
1601 }
1602}
1603
1604impl LogSource {
1605 pub fn effective(requested: &[Self]) -> Vec<Self> {
1607 if requested.is_empty() {
1608 vec![Self::Stdout, Self::Stderr, Self::Output]
1609 } else {
1610 let mut sources = requested.to_vec();
1611 sources.sort_by_key(|src| match src {
1612 Self::Stdout => 0,
1613 Self::Stderr => 1,
1614 Self::Output => 2,
1615 Self::System => 3,
1616 });
1617 sources.dedup();
1618 sources
1619 }
1620 }
1621}
1622
1623impl SandboxLogLevel {
1624 pub const fn as_str(self) -> &'static str {
1626 match self {
1627 Self::Error => "error",
1628 Self::Warn => "warn",
1629 Self::Info => "info",
1630 Self::Debug => "debug",
1631 Self::Trace => "trace",
1632 }
1633 }
1634}
1635
1636impl std::fmt::Display for DiskImageFormat {
1641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1642 f.write_str(self.as_str())
1643 }
1644}
1645
1646impl FromStr for DiskImageFormat {
1647 type Err = String;
1648
1649 fn from_str(s: &str) -> Result<Self, Self::Err> {
1650 match s {
1651 "qcow2" => Ok(Self::Qcow2),
1652 "raw" => Ok(Self::Raw),
1653 "vmdk" => Ok(Self::Vmdk),
1654 _ => Err(format!("unknown disk image format: {s}")),
1655 }
1656 }
1657}
1658
1659impl fmt::Display for TransparentHugePagePolicy {
1660 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1661 f.write_str(self.as_str())
1662 }
1663}
1664
1665impl FromStr for TransparentHugePagePolicy {
1666 type Err = String;
1667
1668 fn from_str(value: &str) -> Result<Self, Self::Err> {
1669 match value {
1670 "always" => Ok(Self::Always),
1671 "madvise" => Ok(Self::Madvise),
1672 "never" => Ok(Self::Never),
1673 _ => Err(format!(
1674 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1675 )),
1676 }
1677 }
1678}
1679
1680impl Default for RootfsSource {
1681 fn default() -> Self {
1682 Self::oci(String::new())
1683 }
1684}
1685
1686impl Default for SandboxResources {
1687 fn default() -> Self {
1688 Self {
1689 cpus: DEFAULT_SANDBOX_CPUS,
1690 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1691 max_cpus: DEFAULT_SANDBOX_CPUS,
1692 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1693 cpu_placement: CpuPlacement::Inherit,
1694 placement_profile: None,
1695 thp: TransparentHugePagePolicy::Madvise,
1696 }
1697 }
1698}
1699
1700impl<'de> Deserialize<'de> for SandboxResources {
1701 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1702 where
1703 D: serde::Deserializer<'de>,
1704 {
1705 #[derive(Deserialize)]
1706 struct RawResources {
1707 #[serde(default = "default_sandbox_cpus")]
1708 cpus: u8,
1709 #[serde(default = "default_sandbox_memory_mib")]
1710 memory_mib: u32,
1711 max_cpus: Option<u8>,
1712 max_memory_mib: Option<u32>,
1713 #[serde(default)]
1714 cpu_placement: CpuPlacement,
1715 #[serde(default)]
1716 placement_profile: Option<String>,
1717 #[serde(default)]
1718 thp: TransparentHugePagePolicy,
1719 }
1720
1721 let raw = RawResources::deserialize(deserializer)?;
1722 Ok(Self {
1723 cpus: raw.cpus,
1724 memory_mib: raw.memory_mib,
1725 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1729 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1730 cpu_placement: raw.cpu_placement,
1731 placement_profile: raw.placement_profile,
1732 thp: raw.thp,
1733 })
1734 }
1735}
1736
1737impl CpuPlacement {
1738 pub const fn is_inherit(&self) -> bool {
1740 matches!(self, Self::Inherit)
1741 }
1742}
1743
1744impl std::fmt::Display for CpuPlacement {
1745 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1746 f.write_str(match self {
1747 Self::Inherit => "inherit",
1748 Self::Auto => "auto",
1749 Self::Spread => "spread",
1750 Self::Compact => "compact",
1751 })
1752 }
1753}
1754
1755impl FromStr for CpuPlacement {
1756 type Err = String;
1757
1758 fn from_str(value: &str) -> Result<Self, Self::Err> {
1759 match value {
1760 "inherit" => Ok(Self::Inherit),
1761 "auto" => Ok(Self::Auto),
1762 "spread" => Ok(Self::Spread),
1763 "compact" => Ok(Self::Compact),
1764 _ => Err(format!(
1765 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1766 )),
1767 }
1768 }
1769}
1770
1771impl Default for SandboxRuntimeOptions {
1772 fn default() -> Self {
1773 Self {
1774 workdir: None,
1775 shell: None,
1776 scripts: BTreeMap::new(),
1777 entrypoint: None,
1778 cmd: None,
1779 hostname: None,
1780 user: None,
1781 log_level: None,
1782 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1783 disable_metrics_sample: false,
1784 }
1785 }
1786}
1787
1788impl Default for NetworkSpec {
1789 fn default() -> Self {
1790 Self {
1791 enabled: true,
1792 interface: None,
1793 ports: Vec::new(),
1794 policy: None,
1795 dns: None,
1796 tls: None,
1797 strict: false,
1798 secrets: None,
1799 max_tcp_connections: None,
1800 max_udp_connections: None,
1801 rate_limiter: None,
1802 trust_host_cas: false,
1803 outbound_proxy: None,
1804 }
1805 }
1806}
1807
1808impl Default for PublishedPortSpec {
1809 fn default() -> Self {
1810 Self {
1811 host_port: 0,
1812 guest_port: 0,
1813 protocol: PortProtocol::Tcp,
1814 host_bind: "127.0.0.1".into(),
1815 }
1816 }
1817}
1818
1819impl From<(String, String)> for EnvVar {
1820 fn from((key, value): (String, String)) -> Self {
1821 Self { key, value }
1822 }
1823}
1824
1825impl From<EnvVar> for (String, String) {
1826 fn from(var: EnvVar) -> Self {
1827 (var.key, var.value)
1828 }
1829}
1830
1831impl FromStr for SandboxLogLevel {
1832 type Err = String;
1833
1834 fn from_str(s: &str) -> Result<Self, Self::Err> {
1835 match s {
1836 "error" => Ok(Self::Error),
1837 "warn" => Ok(Self::Warn),
1838 "info" => Ok(Self::Info),
1839 "debug" => Ok(Self::Debug),
1840 "trace" => Ok(Self::Trace),
1841 _ => Err(format!("unknown sandbox log level: {s}")),
1842 }
1843 }
1844}
1845
1846impl std::fmt::Display for SandboxLogLevel {
1847 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1848 formatter.write_str(self.as_str())
1849 }
1850}
1851
1852impl Serialize for VolumeMount {
1853 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1854 use serde::ser::SerializeMap;
1855
1856 match self {
1857 Self::Owned {
1858 guest,
1859 storage,
1860 options,
1861 stat_virtualization,
1862 host_permissions,
1863 } => {
1864 let mut map = serializer.serialize_map(Some(6))?;
1867 map.serialize_entry("type", "Owned")?;
1868 map.serialize_entry("guest", guest)?;
1869 map.serialize_entry("storage", storage)?;
1870 map.serialize_entry("options", options)?;
1871 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1872 map.serialize_entry("host_permissions", host_permissions)?;
1873 map.end()
1874 }
1875 Self::Bind {
1876 host,
1877 guest,
1878 options,
1879 stat_virtualization,
1880 host_permissions,
1881 follow_root_symlinks,
1882 quota_mib,
1883 } => {
1884 let mut map = serializer.serialize_map(Some(8))?;
1885 map.serialize_entry("type", "Bind")?;
1886 map.serialize_entry("host", host)?;
1887 map.serialize_entry("guest", guest)?;
1888 map.serialize_entry("options", options)?;
1889 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1890 map.serialize_entry("host_permissions", host_permissions)?;
1891 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1892 map.serialize_entry("quota_mib", quota_mib)?;
1893 map.end()
1894 }
1895 Self::Named {
1896 name,
1897 guest,
1898 create: _,
1899 options,
1900 stat_virtualization,
1901 host_permissions,
1902 follow_root_symlinks,
1903 } => {
1904 let mut map = serializer.serialize_map(Some(7))?;
1905 map.serialize_entry("type", "Named")?;
1906 map.serialize_entry("name", name)?;
1907 map.serialize_entry("guest", guest)?;
1908 map.serialize_entry("options", options)?;
1909 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1910 map.serialize_entry("host_permissions", host_permissions)?;
1911 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1912 map.end()
1913 }
1914 Self::Tmpfs {
1915 guest,
1916 size_mib,
1917 options,
1918 } => {
1919 let mut map = serializer.serialize_map(Some(4))?;
1920 map.serialize_entry("type", "Tmpfs")?;
1921 map.serialize_entry("guest", guest)?;
1922 map.serialize_entry("size_mib", size_mib)?;
1923 map.serialize_entry("options", options)?;
1924 map.end()
1925 }
1926 Self::DiskImage {
1927 host,
1928 guest,
1929 format,
1930 fstype,
1931 options,
1932 } => {
1933 let mut map = serializer.serialize_map(Some(6))?;
1934 map.serialize_entry("type", "DiskImage")?;
1935 map.serialize_entry("host", host)?;
1936 map.serialize_entry("guest", guest)?;
1937 map.serialize_entry("format", format)?;
1938 map.serialize_entry("fstype", fstype)?;
1939 map.serialize_entry("options", options)?;
1940 map.end()
1941 }
1942 }
1943 }
1944}
1945
1946impl<'de> Deserialize<'de> for VolumeMount {
1947 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1948 fn default_strict() -> StatVirtualization {
1949 StatVirtualization::Strict
1950 }
1951
1952 fn default_private() -> HostPermissions {
1953 HostPermissions::Private
1954 }
1955
1956 #[derive(Deserialize)]
1957 #[serde(tag = "type")]
1958 enum VolumeMountHelper {
1959 Owned {
1960 guest: String,
1961 storage: OwnedVolumeStorage,
1962 #[serde(default)]
1963 options: MountOptions,
1964 #[serde(default = "default_strict")]
1965 stat_virtualization: StatVirtualization,
1966 #[serde(default = "default_private")]
1967 host_permissions: HostPermissions,
1968 },
1969 Bind {
1970 host: PathBuf,
1971 guest: String,
1972 #[serde(default)]
1973 options: Option<MountOptions>,
1974 #[serde(default)]
1975 readonly: bool,
1976 #[serde(default = "default_strict")]
1977 stat_virtualization: StatVirtualization,
1978 #[serde(default = "default_private")]
1979 host_permissions: HostPermissions,
1980 #[serde(default)]
1981 follow_root_symlinks: bool,
1982 #[serde(default)]
1983 quota_mib: Option<u32>,
1984 },
1985 Named {
1986 name: String,
1987 guest: String,
1988 #[serde(default)]
1989 options: Option<MountOptions>,
1990 #[serde(default)]
1991 readonly: bool,
1992 #[serde(default = "default_strict")]
1993 stat_virtualization: StatVirtualization,
1994 #[serde(default = "default_private")]
1995 host_permissions: HostPermissions,
1996 #[serde(default)]
1997 follow_root_symlinks: bool,
1998 },
1999 Tmpfs {
2000 guest: String,
2001 #[serde(default)]
2002 size_mib: Option<u32>,
2003 #[serde(default)]
2004 options: Option<MountOptions>,
2005 #[serde(default)]
2006 readonly: bool,
2007 },
2008 DiskImage {
2009 host: PathBuf,
2010 guest: String,
2011 format: DiskImageFormat,
2012 #[serde(default)]
2013 fstype: Option<String>,
2014 #[serde(default)]
2015 options: Option<MountOptions>,
2016 #[serde(default)]
2017 readonly: bool,
2018 },
2019 }
2020
2021 let helper = VolumeMountHelper::deserialize(deserializer)?;
2022 Ok(match helper {
2023 VolumeMountHelper::Owned {
2024 guest,
2025 storage,
2026 options,
2027 stat_virtualization,
2028 host_permissions,
2029 } => Self::Owned {
2030 guest,
2031 storage,
2032 options,
2033 stat_virtualization,
2034 host_permissions,
2035 },
2036 VolumeMountHelper::Bind {
2037 host,
2038 guest,
2039 options,
2040 readonly,
2041 stat_virtualization,
2042 host_permissions,
2043 follow_root_symlinks,
2044 quota_mib,
2045 } => Self::Bind {
2046 host,
2047 guest,
2048 options: decode_mount_options(options, readonly),
2049 stat_virtualization,
2050 host_permissions,
2051 follow_root_symlinks,
2052 quota_mib,
2053 },
2054 VolumeMountHelper::Named {
2055 name,
2056 guest,
2057 options,
2058 readonly,
2059 stat_virtualization,
2060 host_permissions,
2061 follow_root_symlinks,
2062 } => Self::Named {
2063 name,
2064 guest,
2065 create: None,
2066 options: decode_mount_options(options, readonly),
2067 stat_virtualization,
2068 host_permissions,
2069 follow_root_symlinks,
2070 },
2071 VolumeMountHelper::Tmpfs {
2072 guest,
2073 size_mib,
2074 options,
2075 readonly,
2076 } => Self::Tmpfs {
2077 guest,
2078 size_mib,
2079 options: decode_mount_options(options, readonly),
2080 },
2081 VolumeMountHelper::DiskImage {
2082 host,
2083 guest,
2084 format,
2085 fstype,
2086 options,
2087 readonly,
2088 } => Self::DiskImage {
2089 host,
2090 guest,
2091 format,
2092 fstype,
2093 options: decode_mount_options(options, readonly),
2094 },
2095 })
2096 }
2097}
2098
2099impl fmt::Debug for VolumeMount {
2100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2101 match self {
2102 Self::Owned {
2103 guest,
2104 storage,
2105 options,
2106 stat_virtualization,
2107 host_permissions,
2108 } => f
2109 .debug_struct("Owned")
2110 .field("guest", guest)
2111 .field("storage", storage)
2112 .field("options", options)
2113 .field("stat_virtualization", stat_virtualization)
2114 .field("host_permissions", host_permissions)
2115 .finish(),
2116 Self::Bind {
2117 host,
2118 guest,
2119 options,
2120 stat_virtualization,
2121 host_permissions,
2122 follow_root_symlinks,
2123 quota_mib,
2124 } => f
2125 .debug_struct("Bind")
2126 .field("host", host)
2127 .field("guest", guest)
2128 .field("options", options)
2129 .field("stat_virtualization", stat_virtualization)
2130 .field("host_permissions", host_permissions)
2131 .field("follow_root_symlinks", follow_root_symlinks)
2132 .field("quota_mib", quota_mib)
2133 .finish(),
2134 Self::Named {
2135 name,
2136 guest,
2137 create,
2138 options,
2139 stat_virtualization,
2140 host_permissions,
2141 follow_root_symlinks,
2142 } => f
2143 .debug_struct("Named")
2144 .field("name", name)
2145 .field("guest", guest)
2146 .field("create", create)
2147 .field("options", options)
2148 .field("stat_virtualization", stat_virtualization)
2149 .field("host_permissions", host_permissions)
2150 .field("follow_root_symlinks", follow_root_symlinks)
2151 .finish(),
2152 Self::Tmpfs {
2153 guest,
2154 size_mib,
2155 options,
2156 } => f
2157 .debug_struct("Tmpfs")
2158 .field("guest", guest)
2159 .field("size_mib", size_mib)
2160 .field("options", options)
2161 .finish(),
2162 Self::DiskImage {
2163 host,
2164 guest,
2165 format,
2166 fstype,
2167 options,
2168 } => f
2169 .debug_struct("DiskImage")
2170 .field("host", host)
2171 .field("guest", guest)
2172 .field("format", format)
2173 .field("fstype", fstype)
2174 .field("options", options)
2175 .finish(),
2176 }
2177 }
2178}
2179
2180impl TryFrom<&str> for RlimitResource {
2182 type Error = String;
2183
2184 fn try_from(s: &str) -> Result<Self, Self::Error> {
2185 match s.to_ascii_lowercase().as_str() {
2186 "cpu" => Ok(Self::Cpu),
2187 "fsize" => Ok(Self::Fsize),
2188 "data" => Ok(Self::Data),
2189 "stack" => Ok(Self::Stack),
2190 "core" => Ok(Self::Core),
2191 "rss" => Ok(Self::Rss),
2192 "nproc" => Ok(Self::Nproc),
2193 "nofile" => Ok(Self::Nofile),
2194 "memlock" => Ok(Self::Memlock),
2195 "as" => Ok(Self::As),
2196 "locks" => Ok(Self::Locks),
2197 "sigpending" => Ok(Self::Sigpending),
2198 "msgqueue" => Ok(Self::Msgqueue),
2199 "nice" => Ok(Self::Nice),
2200 "rtprio" => Ok(Self::Rtprio),
2201 "rttime" => Ok(Self::Rttime),
2202 _ => Err(format!("unknown rlimit resource: {s}")),
2203 }
2204 }
2205}
2206
2207fn default_sandbox_cpus() -> u8 {
2212 DEFAULT_SANDBOX_CPUS
2213}
2214
2215fn default_sandbox_memory_mib() -> u32 {
2216 DEFAULT_SANDBOX_MEMORY_MIB
2217}
2218
2219fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2220 options.unwrap_or(MountOptions {
2221 readonly,
2222 ..MountOptions::default()
2223 })
2224}
2225
2226fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2227 for value in higher {
2228 match base.iter_mut().find(|current| current.key == value.key) {
2229 Some(current) => *current = value,
2230 None => base.push(value),
2231 }
2232 }
2233}
2234
2235fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2236 for value in higher {
2237 match base
2238 .iter_mut()
2239 .find(|current| current.env_var == value.env_var)
2240 {
2241 Some(current) => *current = value,
2242 None => base.push(value),
2243 }
2244 }
2245}
2246
2247pub(crate) fn default_strict() -> StatVirtualization {
2249 StatVirtualization::Strict
2250}
2251
2252pub(crate) fn default_private() -> HostPermissions {
2254 HostPermissions::Private
2255}
2256
2257pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2259
2260#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2267#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2268#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2269pub struct SecretsConfig {
2270 #[serde(default)]
2272 #[config_patch(merge_with = merge_secret_entries)]
2273 pub secrets: Vec<SecretEntry>,
2274
2275 #[serde(default)]
2277 pub violation_action: SecretViolationAction,
2278}
2279
2280#[derive(Clone, Serialize, Deserialize)]
2285#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2286#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2287pub struct SecretEntry {
2288 pub env_var: String,
2294
2295 #[serde(default = "empty_secret_value")]
2304 #[cfg_attr(feature = "ts", ts(type = "string"))]
2305 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2306 pub value: Zeroizing<String>,
2307
2308 #[serde(default, skip_serializing_if = "Option::is_none")]
2312 pub source: Option<SecretSource>,
2313
2314 pub placeholder: String,
2319
2320 #[serde(default)]
2322 pub allowed_hosts: Vec<HostPattern>,
2323
2324 #[serde(default)]
2326 pub substitution: SecretSubstitution,
2327
2328 #[serde(default)]
2330 pub passthrough_hosts: Vec<HostPattern>,
2331
2332 #[serde(default, skip_serializing_if = "Option::is_none")]
2334 pub violation_action: Option<SecretViolationAction>,
2335
2336 #[serde(default = "default_true")]
2341 pub require_tls_identity: bool,
2342}
2343
2344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2346#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2347#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2348#[serde(rename_all = "kebab-case")]
2349pub enum HostPattern {
2350 #[serde(alias = "Exact")]
2352 Exact(String),
2353 #[serde(alias = "Wildcard")]
2355 Wildcard(String),
2356 #[serde(alias = "Any")]
2358 Any,
2359}
2360
2361#[derive(Debug, Clone, Serialize, Deserialize)]
2363#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2364#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2365pub struct SecretSubstitution {
2366 #[serde(default = "default_true")]
2368 pub headers: bool,
2369
2370 #[serde(default)]
2372 pub query: bool,
2373
2374 #[serde(default)]
2382 pub body: bool,
2383}
2384
2385#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2387#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2388#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2389#[serde(rename_all = "kebab-case")]
2390pub enum SecretViolationAction {
2391 #[serde(alias = "Block")]
2393 Block,
2394 #[default]
2396 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2397 BlockAndLog,
2398 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2400 BlockAndTerminate,
2401}
2402
2403#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2405pub enum SecretConfigError {
2406 #[error("secret #{secret_index}: env_var must not be empty")]
2408 EmptyEnvVar {
2409 secret_index: usize,
2411 },
2412
2413 #[error("secret #{secret_index}: env_var must not contain `=`")]
2415 EnvVarContainsEquals {
2416 secret_index: usize,
2418 },
2419
2420 #[error("secret #{secret_index}: env_var must not contain NUL")]
2422 EnvVarContainsNul {
2423 secret_index: usize,
2425 },
2426
2427 #[error("secret #{secret_index}: at least one allowed host is required")]
2429 MissingAllowedHosts {
2430 secret_index: usize,
2432 },
2433
2434 #[error("secret #{secret_index}: at least one substitution location is required")]
2436 MissingSubstitutionLocation {
2437 secret_index: usize,
2439 },
2440
2441 #[error("secret #{secret_index}: placeholder must not be empty")]
2443 EmptyPlaceholder {
2444 secret_index: usize,
2446 },
2447
2448 #[error(
2450 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2451 )]
2452 PlaceholderTooLong {
2453 secret_index: usize,
2455 actual_bytes: usize,
2457 max_bytes: usize,
2459 },
2460
2461 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2463 PlaceholderContainsNul {
2464 secret_index: usize,
2466 },
2467
2468 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2470 PlaceholderContainsLineBreak {
2471 secret_index: usize,
2473 },
2474}
2475
2476impl SecretsConfig {
2477 pub fn validate(&self) -> Result<(), SecretConfigError> {
2479 for (index, secret) in self.secrets.iter().enumerate() {
2480 secret.validate(index)?;
2481 }
2482 Ok(())
2483 }
2484}
2485
2486impl SecretEntry {
2487 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2489 validate_env_var(&self.env_var, secret_index)?;
2490
2491 if self.allowed_hosts.is_empty() {
2492 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2493 }
2494
2495 if !self.substitution.headers && !self.substitution.query && !self.substitution.body {
2496 return Err(SecretConfigError::MissingSubstitutionLocation { secret_index });
2497 }
2498
2499 validate_placeholder(&self.placeholder, secret_index)
2500 }
2501}
2502
2503impl fmt::Debug for SecretEntry {
2505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2506 f.debug_struct("SecretEntry")
2507 .field("env_var", &self.env_var)
2508 .field("value", &"[REDACTED]")
2509 .field("source", &self.source)
2510 .field("placeholder", &self.placeholder)
2511 .field("allowed_hosts", &self.allowed_hosts)
2512 .field("substitution", &self.substitution)
2513 .field("passthrough_hosts", &self.passthrough_hosts)
2514 .field("violation_action", &self.violation_action)
2515 .field("require_tls_identity", &self.require_tls_identity)
2516 .finish()
2517 }
2518}
2519
2520impl HostPattern {
2521 pub fn parse(host: &str) -> Self {
2524 if host == "*" {
2525 HostPattern::Any
2526 } else if host.starts_with("*.") {
2527 HostPattern::Wildcard(host.to_string())
2528 } else {
2529 HostPattern::Exact(host.to_string())
2530 }
2531 }
2532
2533 pub fn matches(&self, hostname: &str) -> bool {
2538 match self {
2539 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2540 HostPattern::Wildcard(pattern) => {
2541 if let Some(suffix) = pattern.strip_prefix("*.") {
2542 hostname.eq_ignore_ascii_case(suffix)
2543 || (hostname.len() > suffix.len() + 1
2544 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2545 && hostname[hostname.len() - suffix.len()..]
2546 .eq_ignore_ascii_case(suffix))
2547 } else {
2548 hostname.eq_ignore_ascii_case(pattern)
2549 }
2550 }
2551 HostPattern::Any => true,
2552 }
2553 }
2554}
2555
2556impl Default for SecretSubstitution {
2557 fn default() -> Self {
2558 Self {
2559 headers: true,
2560 query: false,
2561 body: false,
2562 }
2563 }
2564}
2565
2566fn default_true() -> bool {
2567 true
2568}
2569
2570fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2571 if env_var.is_empty() {
2572 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2573 }
2574 if env_var.contains('=') {
2575 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2576 }
2577 if env_var.contains('\0') {
2578 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2579 }
2580 Ok(())
2581}
2582
2583fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2584 if placeholder.is_empty() {
2585 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2586 }
2587
2588 let actual_bytes = placeholder.len();
2589 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2590 return Err(SecretConfigError::PlaceholderTooLong {
2591 secret_index,
2592 actual_bytes,
2593 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2594 });
2595 }
2596
2597 if placeholder.contains('\0') {
2598 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2599 }
2600 if placeholder.contains('\r') || placeholder.contains('\n') {
2601 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2602 }
2603
2604 Ok(())
2605}
2606
2607#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2617#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2618#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2619pub struct TlsConfig {
2620 #[serde(default)]
2622 pub enabled: bool,
2623
2624 #[serde(default = "default_intercepted_ports")]
2626 pub intercepted_ports: Vec<u16>,
2627
2628 #[serde(default)]
2630 pub bypass: Vec<String>,
2631
2632 #[serde(default = "default_true")]
2634 pub verify_upstream: bool,
2635
2636 #[serde(default = "default_true")]
2639 pub block_quic_on_intercept: bool,
2640
2641 #[serde(default)]
2643 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2644 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2645 pub upstream_ca_cert: Vec<PathBuf>,
2646
2647 #[serde(default, alias = "scoped_upstream_ca_certs")]
2649 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2650
2651 #[serde(default)]
2653 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2654
2655 #[serde(default, alias = "ca")]
2658 pub intercept_ca: InterceptCaConfig,
2659
2660 #[serde(default)]
2662 pub cache: CertCacheConfig,
2663}
2664
2665#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2667#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2668#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2669pub struct InterceptCaConfig {
2670 #[serde(default)]
2673 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2674 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2675 pub cert_path: Option<PathBuf>,
2676
2677 #[serde(default)]
2680 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2681 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2682 pub key_path: Option<PathBuf>,
2683}
2684
2685#[derive(Debug, Clone, Serialize, Deserialize)]
2687#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2688#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2689pub struct CertCacheConfig {
2690 #[serde(default = "default_cache_capacity")]
2692 pub capacity: usize,
2693
2694 #[serde(default = "default_cert_validity_hours")]
2696 pub validity_hours: u64,
2697}
2698
2699#[derive(Debug, Clone, Serialize, Deserialize)]
2701#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2702#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2703pub struct ScopedUpstreamCaCert {
2704 pub pattern: String,
2706
2707 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2709 #[cfg_attr(feature = "ts", ts(type = "string"))]
2710 pub path: PathBuf,
2711}
2712
2713#[derive(Debug, Clone, Serialize, Deserialize)]
2715#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2716#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2717pub struct ScopedVerifyUpstream {
2718 pub pattern: String,
2720
2721 pub verify: bool,
2723}
2724
2725impl Default for TlsConfig {
2726 fn default() -> Self {
2727 Self {
2728 enabled: false,
2729 intercepted_ports: default_intercepted_ports(),
2730 bypass: Vec::new(),
2731 verify_upstream: true,
2732 block_quic_on_intercept: true,
2733 upstream_ca_cert: Vec::new(),
2734 scoped_upstream_ca_cert: Vec::new(),
2735 scoped_verify_upstream: Vec::new(),
2736 intercept_ca: InterceptCaConfig::default(),
2737 cache: CertCacheConfig::default(),
2738 }
2739 }
2740}
2741
2742impl Default for CertCacheConfig {
2743 fn default() -> Self {
2744 Self {
2745 capacity: default_cache_capacity(),
2746 validity_hours: default_cert_validity_hours(),
2747 }
2748 }
2749}
2750
2751fn default_intercepted_ports() -> Vec<u16> {
2752 vec![443]
2753}
2754
2755fn default_cache_capacity() -> usize {
2756 1000
2757}
2758
2759fn default_cert_validity_hours() -> u64 {
2760 24
2761}
2762
2763#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2769#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2770#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2771#[serde(rename_all = "snake_case")]
2772pub enum Action {
2773 Allow,
2775 Deny,
2777}
2778
2779#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2781#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2782#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2783#[serde(rename_all = "snake_case")]
2784pub enum Direction {
2785 Egress,
2787 Ingress,
2789 Any,
2791}
2792
2793#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2795#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2796#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2797#[serde(rename_all = "snake_case")]
2798pub enum Protocol {
2799 Tcp,
2801 Udp,
2803 Icmpv4,
2805 Icmpv6,
2807}
2808
2809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2811#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2812#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2813#[serde(rename_all = "snake_case")]
2814pub enum DestinationGroup {
2815 Public,
2817 Loopback,
2819 Private,
2821 LinkLocal,
2823 Metadata,
2825 Multicast,
2827 Host,
2829}
2830
2831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2838#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2839#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2840#[serde(rename_all = "snake_case")]
2841pub enum Destination {
2842 Any,
2844 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2846 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2847 Domain(String),
2849 DomainSuffix(String),
2851 Group(DestinationGroup),
2853}
2854
2855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2857#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2858#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2859pub struct PortRange {
2860 pub start: u16,
2862 pub end: u16,
2864}
2865
2866#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2869#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2870#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2871pub struct Rule {
2872 pub direction: Direction,
2874 pub destination: Destination,
2876 #[serde(default)]
2878 pub protocols: Vec<Protocol>,
2879 #[serde(default)]
2881 pub ports: Vec<PortRange>,
2882 pub action: Action,
2884}
2885
2886#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2889#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2890#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2891pub struct NetworkPolicy {
2892 #[serde(default = "action_deny")]
2894 pub default_egress: Action,
2895 #[serde(default = "action_deny")]
2897 pub default_ingress: Action,
2898 #[serde(default)]
2900 pub rules: Vec<Rule>,
2901}
2902
2903fn action_deny() -> Action {
2906 Action::Deny
2907}
2908
2909#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2915#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2916#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2917#[serde(default)]
2918pub struct DnsConfig {
2919 pub rebind_protection: bool,
2921 pub nameservers: Vec<String>,
2924 pub query_timeout_ms: u64,
2926}
2927
2928impl Default for DnsConfig {
2929 fn default() -> Self {
2930 Self {
2931 rebind_protection: true,
2932 nameservers: Vec::new(),
2933 query_timeout_ms: 5000,
2934 }
2935 }
2936}
2937
2938#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2942#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2943#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2944#[serde(default)]
2945pub struct InterfaceOverrides {
2946 #[serde(skip_serializing_if = "Option::is_none")]
2948 pub mac: Option<[u8; 6]>,
2949 #[serde(skip_serializing_if = "Option::is_none")]
2951 pub mtu: Option<u16>,
2952 #[serde(skip_serializing_if = "Option::is_none")]
2954 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2955 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2956 pub ipv4_address: Option<Ipv4Addr>,
2957 #[serde(skip_serializing_if = "Option::is_none")]
2959 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2960 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2961 pub ipv4_pool: Option<Ipv4Network>,
2962 #[serde(skip_serializing_if = "Option::is_none")]
2964 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2965 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2966 pub ipv6_address: Option<Ipv6Addr>,
2967 #[serde(skip_serializing_if = "Option::is_none")]
2969 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2970 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2971 pub ipv6_pool: Option<Ipv6Network>,
2972}
2973
2974fn empty_secret_value() -> Zeroizing<String> {
2975 Zeroizing::new(String::new())
2976}
2977
2978#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2984pub enum NetworkRateLimitDirection {
2985 Egress,
2987 Ingress,
2989}
2990
2991#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2993#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2994#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2995#[serde(default)]
2996pub struct NetworkRateLimiterConfig {
2997 #[serde(skip_serializing_if = "Option::is_none")]
2999 pub egress: Option<RateLimiterConfig>,
3000
3001 #[serde(skip_serializing_if = "Option::is_none")]
3003 pub ingress: Option<RateLimiterConfig>,
3004}
3005
3006#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3012#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3013#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3014#[serde(default)]
3015pub struct RateLimiterConfig {
3016 #[serde(skip_serializing_if = "Option::is_none")]
3018 pub bandwidth: Option<TokenBucketConfig>,
3019
3020 #[serde(skip_serializing_if = "Option::is_none")]
3022 pub ops: Option<TokenBucketConfig>,
3023}
3024
3025#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3031#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3032#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3033pub struct TokenBucketConfig {
3034 pub size: u64,
3036
3037 pub refill_time_ms: u64,
3040
3041 #[serde(default)]
3043 pub one_time_burst: u64,
3044}
3045
3046#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3048pub enum RateLimitConfigError {
3049 #[error("rate limiter must configure at least one of bandwidth or ops")]
3051 EmptyLimiter,
3052
3053 #[error("{bucket} bucket: size must be greater than zero")]
3055 ZeroSize {
3056 bucket: &'static str,
3058 },
3059
3060 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
3062 ZeroRefillTime {
3063 bucket: &'static str,
3065 },
3066}
3067
3068impl RateLimiterConfig {
3069 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
3071 if self.bandwidth.is_none() && self.ops.is_none() {
3072 return Err(RateLimitConfigError::EmptyLimiter);
3073 }
3074 if let Some(bandwidth) = &self.bandwidth {
3075 bandwidth.validate("bandwidth")?;
3076 }
3077 if let Some(ops) = &self.ops {
3078 ops.validate("ops")?;
3079 }
3080 Ok(())
3081 }
3082}
3083
3084impl TokenBucketConfig {
3085 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
3087 if self.size == 0 {
3088 return Err(RateLimitConfigError::ZeroSize { bucket });
3089 }
3090 if self.refill_time_ms == 0 {
3091 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
3092 }
3093 Ok(())
3094 }
3095}
3096
3097impl fmt::Display for NetworkRateLimitDirection {
3098 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3099 match self {
3100 Self::Egress => f.write_str("egress"),
3101 Self::Ingress => f.write_str("ingress"),
3102 }
3103 }
3104}
3105
3106#[cfg(test)]
3111mod tests {
3112 use super::*;
3113
3114 fn tmpfs_mount(guest: &str) -> VolumeMount {
3115 VolumeMount::Tmpfs {
3116 guest: guest.to_owned(),
3117 size_mib: None,
3118 options: MountOptions::default(),
3119 }
3120 }
3121
3122 #[test]
3123 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
3124 let value = serde_json::to_value(MountOptions::default()).unwrap();
3125 assert!(value.get("override_uid").is_none());
3126 assert!(value.get("override_gid").is_none());
3127
3128 let decoded: MountOptions = serde_json::from_value(value).unwrap();
3129 assert_eq!(decoded.override_uid, None);
3130 assert_eq!(decoded.override_gid, None);
3131 }
3132
3133 #[test]
3134 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
3135 let mut mounts = vec![
3136 tmpfs_mount("/workspace//persist/./logs/"),
3137 tmpfs_mount("/alpha/z"),
3138 tmpfs_mount("/workspace"),
3139 ];
3140
3141 canonicalize_volume_mounts(&mut mounts).unwrap();
3142
3143 assert_eq!(
3144 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
3145 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
3146 );
3147 }
3148
3149 #[test]
3150 fn volume_mounts_reject_duplicate_canonical_paths() {
3151 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
3152
3153 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3154
3155 assert!(error.to_string().contains("same guest path: /data/cache"));
3156 }
3157
3158 #[test]
3159 fn volume_mounts_reject_parent_components_before_normalizing() {
3160 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3161
3162 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3163
3164 assert!(error.to_string().contains("must not contain '..'"));
3165 }
3166
3167 #[test]
3168 fn disk_image_format_from_extension() {
3169 assert_eq!(
3170 DiskImageFormat::from_extension("qcow2"),
3171 Some(DiskImageFormat::Qcow2)
3172 );
3173 assert_eq!(
3174 DiskImageFormat::from_extension("raw"),
3175 Some(DiskImageFormat::Raw)
3176 );
3177 assert_eq!(
3178 DiskImageFormat::from_extension("vmdk"),
3179 Some(DiskImageFormat::Vmdk)
3180 );
3181 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3182 assert_eq!(DiskImageFormat::from_extension(""), None);
3183 }
3184
3185 #[test]
3186 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3187 let resources: SandboxResources =
3188 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3189
3190 assert_eq!(resources.cpus, 4);
3191 assert_eq!(resources.max_cpus, 4);
3192 assert_eq!(resources.memory_mib, 2048);
3193 assert_eq!(resources.max_memory_mib, 2048);
3194 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3195 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3196 assert_eq!(
3197 serde_json::to_value(resources).unwrap(),
3198 serde_json::json!({
3199 "cpus": 4,
3200 "memory_mib": 2048,
3201 "max_cpus": 4,
3202 "max_memory_mib": 2048
3203 })
3204 );
3205 }
3206
3207 #[test]
3208 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3209 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3210 assert!(inherited.get("cpu_placement").is_none());
3211
3212 for policy in [
3213 CpuPlacement::Auto,
3214 CpuPlacement::Spread,
3215 CpuPlacement::Compact,
3216 ] {
3217 let resources = SandboxResources {
3218 cpu_placement: policy,
3219 ..Default::default()
3220 };
3221 let json = serde_json::to_string(&resources).unwrap();
3222 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3223
3224 assert_eq!(decoded.cpu_placement, policy);
3225 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3226 }
3227 }
3228
3229 #[test]
3230 fn transparent_huge_page_policy_roundtrips_non_default() {
3231 let resources: SandboxResources = serde_json::from_str(
3232 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3233 )
3234 .unwrap();
3235
3236 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3237 assert_eq!(
3238 serde_json::to_value(resources).unwrap()["thp"],
3239 serde_json::json!("always")
3240 );
3241 assert_eq!(
3242 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3243 TransparentHugePagePolicy::Never
3244 );
3245 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3246 }
3247
3248 #[test]
3249 fn disk_image_format_display_roundtrip() {
3250 for format in [
3251 DiskImageFormat::Qcow2,
3252 DiskImageFormat::Raw,
3253 DiskImageFormat::Vmdk,
3254 ] {
3255 let rendered = format.to_string();
3256 let parsed: DiskImageFormat = rendered.parse().unwrap();
3257 assert_eq!(parsed, format);
3258 }
3259 }
3260
3261 #[test]
3262 fn disk_image_format_from_str_unknown() {
3263 assert!("ext4".parse::<DiskImageFormat>().is_err());
3264 }
3265
3266 #[test]
3267 fn log_source_effective_uses_default_user_program_sources() {
3268 assert_eq!(
3269 LogSource::effective(&[]),
3270 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3271 );
3272 }
3273
3274 #[test]
3275 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3276 assert_eq!(
3277 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3278 vec![LogSource::Stdout, LogSource::System]
3279 );
3280 }
3281
3282 #[test]
3283 fn rlimit_resource_parses_case_insensitively() {
3284 assert_eq!(
3285 RlimitResource::try_from("NOFILE").unwrap(),
3286 RlimitResource::Nofile
3287 );
3288 assert!(RlimitResource::try_from("bogus").is_err());
3289 }
3290
3291 #[test]
3292 fn sandbox_policy_serde_roundtrip() {
3293 let policy = SandboxPolicy {
3294 ephemeral: true,
3295 max_duration_secs: Some(3600),
3296 idle_timeout_secs: Some(120),
3297 };
3298
3299 let json = serde_json::to_string(&policy).unwrap();
3300 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3301
3302 assert!(decoded.ephemeral);
3303 assert_eq!(decoded.max_duration_secs, Some(3600));
3304 assert_eq!(decoded.idle_timeout_secs, Some(120));
3305 }
3306
3307 #[test]
3308 fn sandbox_policy_defaults_to_persistent() {
3309 assert!(!SandboxPolicy::default().ephemeral);
3310 }
3311
3312 #[test]
3313 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3314 let decoded: SandboxPolicy =
3317 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3318 assert!(!decoded.ephemeral);
3319 assert_eq!(decoded.max_duration_secs, Some(60));
3320 }
3321
3322 #[test]
3323 fn sandbox_spec_default_uses_static_resource_defaults() {
3324 let spec = SandboxSpec::default();
3325
3326 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3327 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3328 assert_eq!(
3329 spec.runtime.metrics_sample_interval_ms,
3330 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3331 );
3332 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3333 }
3334
3335 #[test]
3336 fn deployment_profile_uses_stable_snake_case_wire_values() {
3337 assert_eq!(
3338 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3339 r#""multi_tenant""#
3340 );
3341 assert_eq!(
3342 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3343 DeploymentProfile::SingleTenant
3344 );
3345 }
3346
3347 #[test]
3348 fn sandbox_log_level_roundtrips_lowercase_values() {
3349 for (input, expected) in [
3350 ("error", SandboxLogLevel::Error),
3351 ("warn", SandboxLogLevel::Warn),
3352 ("info", SandboxLogLevel::Info),
3353 ("debug", SandboxLogLevel::Debug),
3354 ("trace", SandboxLogLevel::Trace),
3355 ] {
3356 let parsed: SandboxLogLevel = input.parse().unwrap();
3357 assert_eq!(parsed, expected);
3358 assert_eq!(parsed.as_str(), input);
3359 }
3360 }
3361}