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")]
638 #[config_patch(nullable)]
639 pub outbound_proxy: Option<OutboundProxy>,
640}
641
642#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
644#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
645#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
646#[serde(tag = "protocol", rename_all = "lowercase")]
647#[non_exhaustive]
648pub enum OutboundProxy {
649 Socks4 {
651 address: String,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 user_id: Option<String>,
656 },
657
658 Socks5 {
660 address: String,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
664 credentials: Option<Socks5Credentials>,
665 },
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
673#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
674#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
675pub struct Socks5Credentials {
676 pub username: String,
678
679 pub password: SecretSource,
681}
682
683#[derive(Debug, Clone, Serialize, Deserialize)]
685#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
686#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
687pub struct PublishedPortSpec {
688 pub host_port: u16,
690
691 pub guest_port: u16,
693
694 #[serde(default)]
696 pub protocol: PortProtocol,
697
698 pub host_bind: String,
700}
701
702#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
704#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
705#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
706pub enum PortProtocol {
707 #[default]
709 #[serde(rename = "tcp")]
710 Tcp,
711
712 #[serde(rename = "udp")]
714 Udp,
715}
716
717#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
723#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
724#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
725#[serde(default)]
726pub struct VsockSpec {
727 pub routes: Vec<VsockRouteSpec>,
729}
730
731impl VsockSpec {
732 pub fn is_empty(&self) -> bool {
734 self.routes.is_empty()
735 }
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
741#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
742pub struct VsockRouteSpec {
743 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
745 pub host_socket: PathBuf,
746
747 pub port: u32,
749
750 #[serde(default)]
752 pub socket_type: VsockSocketType,
753}
754
755#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
757#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
758#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
759#[serde(rename_all = "snake_case")]
760pub enum VsockSocketType {
761 #[default]
763 Stream,
764
765 Dgram,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize)]
775#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
776#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
777pub struct HandoffInit {
778 pub cmd: String,
782
783 #[serde(default)]
785 pub args: Vec<String>,
786
787 #[serde(default)]
789 pub env: Vec<(String, String)>,
790}
791
792#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
798#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
799#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
800pub struct SandboxPolicy {
801 #[serde(default)]
810 pub ephemeral: bool,
811
812 pub max_duration_secs: Option<u64>,
814
815 pub idle_timeout_secs: Option<u64>,
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize)]
829#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
830pub struct SnapshotSpec {
831 #[serde(default)]
833 pub guest_flush: crate::GuestFlush,
834 pub name: String,
836
837 #[serde(default)]
839 pub group: Option<String>,
840
841 #[serde(default)]
843 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
844 pub dest_dir: Option<PathBuf>,
845
846 pub source_sandbox: String,
848
849 pub labels: Vec<(String, String)>,
851
852 pub force: bool,
854
855 pub record_integrity: bool,
857
858 #[serde(default)]
860 pub full: bool,
861}
862
863#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
871#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
872#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
873#[serde(default)]
874pub struct SandboxSpec {
875 pub name: String,
877
878 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
880 pub image: RootfsSource,
881
882 #[config_patch(nested)]
884 pub resources: SandboxResources,
885
886 #[config_patch(nested)]
888 pub runtime: SandboxRuntimeOptions,
889
890 #[config_patch(merge_with = merge_env_vars)]
892 pub env: Vec<EnvVar>,
893
894 #[config_patch(merge)]
896 pub labels: BTreeMap<String, String>,
897
898 pub rlimits: Vec<Rlimit>,
900
901 pub mounts: Vec<VolumeMount>,
903
904 pub patches: Vec<Patch>,
906
907 #[config_patch(nested)]
909 pub network: NetworkSpec,
910
911 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
913 #[config_patch(nested)]
914 pub vsock: VsockSpec,
915
916 pub init: Option<HandoffInit>,
918
919 pub pull_policy: PullPolicy,
921
922 pub security_profile: SecurityProfile,
924
925 pub deployment_profile: DeploymentProfile,
931
932 #[config_patch(nested)]
934 pub lifecycle: SandboxPolicy,
935}
936
937#[derive(Debug, Clone, Serialize, ConfigPatch)]
939#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
940#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
941pub struct SandboxResources {
942 pub cpus: u8,
944
945 pub memory_mib: u32,
947
948 pub max_cpus: u8,
950
951 pub max_memory_mib: u32,
953
954 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
956 pub cpu_placement: CpuPlacement,
957
958 #[serde(default, skip_serializing_if = "Option::is_none")]
961 #[config_patch(nullable)]
962 pub placement_profile: Option<String>,
963
964 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
966 pub thp: TransparentHugePagePolicy,
967}
968
969#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
971#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
972#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
973#[serde(rename_all = "lowercase")]
974pub enum CpuPlacement {
975 #[default]
977 Inherit,
978
979 Auto,
981
982 Spread,
984
985 Compact,
987}
988
989#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
991#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
992#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
993#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
994pub enum NumaPlacement {
995 PreferSingle,
997 StrictSingle,
999 Inherit,
1001}
1002
1003#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1005#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1006#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1007#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1008pub enum MemoryPlacement {
1009 FollowCpu,
1011 Inherit,
1013}
1014
1015#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1017#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1018#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1019#[serde(deny_unknown_fields)]
1020pub struct PlacementProfile {
1021 pub numa: NumaPlacement,
1023 pub memory: MemoryPlacement,
1025}
1026
1027#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1029#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1030#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1031#[serde(rename_all = "lowercase")]
1032pub enum TransparentHugePagePolicy {
1033 Always,
1035
1036 #[default]
1038 Madvise,
1039
1040 Never,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1046#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1047#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1048#[serde(default)]
1049pub struct SandboxRuntimeOptions {
1050 #[config_patch(nullable)]
1053 pub workdir: Option<String>,
1054
1055 #[config_patch(nullable)]
1058 pub shell: Option<String>,
1059
1060 #[config_patch(merge)]
1062 pub scripts: BTreeMap<String, String>,
1063
1064 pub entrypoint: Option<Vec<String>>,
1066
1067 pub cmd: Option<Vec<String>>,
1069
1070 pub hostname: Option<String>,
1072
1073 pub user: Option<String>,
1075
1076 #[config_patch(nullable)]
1079 pub log_level: Option<SandboxLogLevel>,
1080
1081 #[config_patch(nullable)]
1084 pub metrics_sample_interval_ms: Option<u64>,
1085
1086 pub disable_metrics_sample: bool,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1092#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1093#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1094pub struct EnvVar {
1095 pub key: String,
1097
1098 pub value: String,
1100}
1101
1102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1104#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1105#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1106#[serde(rename_all = "lowercase")]
1107pub enum SandboxLogLevel {
1108 Error,
1110
1111 Warn,
1113
1114 Info,
1116
1117 Debug,
1119
1120 Trace,
1122}
1123
1124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1130#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1131#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1132pub enum RlimitResource {
1133 Cpu,
1135 Fsize,
1137 Data,
1139 Stack,
1141 Core,
1143 Rss,
1145 Nproc,
1147 Nofile,
1149 Memlock,
1151 As,
1153 Locks,
1155 Sigpending,
1157 Msgqueue,
1159 Nice,
1161 Rtprio,
1163 Rttime,
1165}
1166
1167#[derive(Debug, Clone, Serialize, Deserialize)]
1169#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1170#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1171pub struct Rlimit {
1172 pub resource: RlimitResource,
1174
1175 pub soft: u64,
1177
1178 pub hard: u64,
1180}
1181
1182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1188#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1189#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1190#[serde(rename_all = "lowercase")]
1191pub enum LogSource {
1192 Stdout,
1194
1195 Stderr,
1197
1198 Output,
1200
1201 System,
1203}
1204
1205impl SandboxResourcesPatch {
1210 pub fn has_cpus(&self) -> bool {
1212 self.cpus.is_some()
1213 }
1214
1215 pub fn has_memory_mib(&self) -> bool {
1217 self.memory_mib.is_some()
1218 }
1219
1220 pub fn has_max_cpus(&self) -> bool {
1222 self.max_cpus.is_some()
1223 }
1224
1225 pub fn has_max_memory_mib(&self) -> bool {
1227 self.max_memory_mib.is_some()
1228 }
1229}
1230
1231impl DiskImageFormat {
1232 pub fn as_str(&self) -> &'static str {
1234 match self {
1235 Self::Qcow2 => "qcow2",
1236 Self::Raw => "raw",
1237 Self::Vmdk => "vmdk",
1238 }
1239 }
1240
1241 pub fn from_extension(ext: &str) -> Option<Self> {
1245 match ext {
1246 "qcow2" => Some(Self::Qcow2),
1247 "raw" => Some(Self::Raw),
1248 "vmdk" => Some(Self::Vmdk),
1249 _ => None,
1250 }
1251 }
1252}
1253
1254impl OciRootfsSource {
1255 pub fn new(reference: impl Into<String>) -> Self {
1257 Self {
1258 reference: reference.into(),
1259 root_disk: None,
1260 }
1261 }
1262}
1263
1264impl TransparentHugePagePolicy {
1265 pub fn is_madvise(&self) -> bool {
1267 matches!(self, Self::Madvise)
1268 }
1269
1270 pub fn as_str(self) -> &'static str {
1272 match self {
1273 Self::Always => "always",
1274 Self::Madvise => "madvise",
1275 Self::Never => "never",
1276 }
1277 }
1278}
1279
1280impl RootDisk {
1281 pub fn managed(size_mib: u32) -> Self {
1283 Self::Managed {
1284 size_mib: Some(size_mib),
1285 }
1286 }
1287
1288 pub fn tmpfs(size_mib: u32) -> Self {
1290 Self::Tmpfs {
1291 size_mib: Some(size_mib),
1292 }
1293 }
1294
1295 pub fn flat(size_mib: u32) -> Self {
1297 Self::Flat {
1298 size_mib: Some(size_mib),
1299 fstype: None,
1300 clone: FlatClone::Auto,
1301 }
1302 }
1303
1304 pub fn size_mib(&self) -> Option<u32> {
1306 match self {
1307 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1308 *size_mib
1309 }
1310 Self::DiskImage { .. } => None,
1311 }
1312 }
1313
1314 pub fn kind_str(&self) -> &'static str {
1316 match self {
1317 Self::Managed { .. } => "managed",
1318 Self::Tmpfs { .. } => "tmpfs",
1319 Self::DiskImage { .. } => "disk-image",
1320 Self::Flat { .. } => "flat",
1321 }
1322 }
1323
1324 pub fn is_managed(&self) -> bool {
1326 matches!(self, Self::Managed { .. })
1327 }
1328}
1329
1330impl FlatClone {
1331 pub const fn as_str(self) -> &'static str {
1333 match self {
1334 Self::Auto => "auto",
1335 Self::Copy => "copy",
1336 Self::Reflink => "reflink",
1337 }
1338 }
1339
1340 pub const fn is_auto(&self) -> bool {
1342 matches!(self, Self::Auto)
1343 }
1344}
1345
1346impl RootfsSource {
1347 pub fn oci(reference: impl Into<String>) -> Self {
1349 Self::Oci(OciRootfsSource::new(reference))
1350 }
1351
1352 pub fn oci_reference(&self) -> Option<&str> {
1354 match self {
1355 Self::Oci(oci) => Some(&oci.reference),
1356 _ => None,
1357 }
1358 }
1359
1360 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1362 match self {
1363 Self::Oci(oci) => oci.root_disk.as_ref(),
1364 _ => None,
1365 }
1366 }
1367
1368 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1371 match self {
1372 Self::Oci(oci) => match &oci.root_disk {
1373 Some(RootDisk::Managed { size_mib }) => *size_mib,
1374 Some(_) => None,
1375 None => None,
1376 },
1377 _ => None,
1378 }
1379 }
1380}
1381
1382impl EnvVar {
1383 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1385 Self {
1386 key: key.into(),
1387 value: value.into(),
1388 }
1389 }
1390
1391 pub fn as_pair(&self) -> (&str, &str) {
1393 (&self.key, &self.value)
1394 }
1395}
1396
1397impl VolumeKind {
1398 pub fn as_str(self) -> &'static str {
1400 match self {
1401 Self::Directory => "dir",
1402 Self::Disk => "disk",
1403 }
1404 }
1405
1406 pub fn from_db_value(value: &str) -> Self {
1408 match value {
1409 "disk" => Self::Disk,
1410 _ => Self::Directory,
1411 }
1412 }
1413}
1414
1415impl VolumeSpec {
1416 pub fn new(name: impl Into<String>) -> Self {
1418 Self {
1419 name: name.into(),
1420 kind: VolumeKind::Directory,
1421 quota_mib: None,
1422 capacity_mib: None,
1423 labels: Vec::new(),
1424 }
1425 }
1426}
1427
1428impl NamedVolumeCreate {
1429 pub fn mode(&self) -> NamedVolumeMode {
1431 self.mode
1432 }
1433
1434 pub fn name(&self) -> &str {
1436 &self.name
1437 }
1438
1439 pub fn kind(&self) -> VolumeKind {
1441 self.kind
1442 }
1443
1444 pub fn quota_mib(&self) -> Option<u32> {
1446 self.quota_mib
1447 }
1448
1449 pub fn capacity_mib(&self) -> Option<u32> {
1451 self.capacity_mib
1452 }
1453
1454 pub fn labels(&self) -> &[(String, String)] {
1456 &self.labels
1457 }
1458}
1459
1460impl VolumeMount {
1461 pub fn guest(&self) -> &str {
1463 match self {
1464 Self::Bind { guest, .. }
1465 | Self::Owned { guest, .. }
1466 | Self::Named { guest, .. }
1467 | Self::Tmpfs { guest, .. }
1468 | Self::DiskImage { guest, .. } => guest,
1469 }
1470 }
1471
1472 fn guest_mut(&mut self) -> &mut String {
1473 match self {
1474 Self::Bind { guest, .. }
1475 | Self::Owned { guest, .. }
1476 | Self::Named { guest, .. }
1477 | Self::Tmpfs { guest, .. }
1478 | Self::DiskImage { guest, .. } => guest,
1479 }
1480 }
1481
1482 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1484 match self {
1485 Self::Named { create, .. } => create.as_ref(),
1486 _ => None,
1487 }
1488 }
1489}
1490
1491pub fn owned_volume_mount_id(guest: &str) -> String {
1498 use std::fmt::Write as _;
1499 let slug: String = guest
1500 .trim_start_matches('/')
1501 .chars()
1502 .take(11)
1503 .map(|character| {
1504 if character.is_ascii_alphanumeric() || character == '-' {
1505 character
1506 } else {
1507 '_'
1508 }
1509 })
1510 .collect();
1511 let mut id = if slug.is_empty() {
1512 String::new()
1513 } else {
1514 format!("{slug}_")
1515 };
1516 for byte in Sha256::digest(guest.as_bytes()).iter().take(4) {
1517 let _ = write!(id, "{byte:02x}");
1518 }
1519 id
1520}
1521
1522pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1529 for mount in mounts.iter_mut() {
1530 let canonical = canonical_guest_mount_path(mount.guest())?;
1531 *mount.guest_mut() = canonical;
1532 }
1533
1534 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1535
1536 for pair in mounts.windows(2) {
1537 if pair[0].guest() == pair[1].guest() {
1538 return Err(TypesError::invalid_config(format!(
1539 "multiple volumes cannot mount the same guest path: {}",
1540 pair[0].guest()
1541 )));
1542 }
1543 }
1544
1545 Ok(())
1546}
1547
1548fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1549 let path = Utf8UnixPath::new(guest);
1550
1551 if !path.is_valid() {
1552 return Err(TypesError::invalid_config(format!(
1553 "guest mount path must be a valid Unix path: {guest}"
1554 )));
1555 }
1556 if !path.is_absolute() {
1557 return Err(TypesError::invalid_config(format!(
1558 "guest mount path must be absolute: {guest}"
1559 )));
1560 }
1561 if path
1562 .components()
1563 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1564 {
1565 return Err(TypesError::invalid_config(format!(
1566 "guest mount path must not contain '..': {guest}"
1567 )));
1568 }
1569 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1570 return Err(TypesError::invalid_config(format!(
1571 "guest mount path must not contain ':', ';', or ',': {guest}"
1572 )));
1573 }
1574
1575 let canonical = path.normalize().to_string();
1576 if canonical == "/" {
1577 return Err(TypesError::invalid_config(
1578 "cannot mount a volume at guest root /",
1579 ));
1580 }
1581
1582 Ok(canonical)
1583}
1584
1585fn guest_mount_order_key(guest: &str) -> (usize, String) {
1586 let path = Utf8UnixPath::new(guest);
1587 let depth = path.components().filter(Utf8Component::is_normal).count();
1588 (depth, guest.to_owned())
1589}
1590
1591impl RlimitResource {
1592 pub fn as_str(&self) -> &'static str {
1594 match self {
1595 Self::Cpu => "cpu",
1596 Self::Fsize => "fsize",
1597 Self::Data => "data",
1598 Self::Stack => "stack",
1599 Self::Core => "core",
1600 Self::Rss => "rss",
1601 Self::Nproc => "nproc",
1602 Self::Nofile => "nofile",
1603 Self::Memlock => "memlock",
1604 Self::As => "as",
1605 Self::Locks => "locks",
1606 Self::Sigpending => "sigpending",
1607 Self::Msgqueue => "msgqueue",
1608 Self::Nice => "nice",
1609 Self::Rtprio => "rtprio",
1610 Self::Rttime => "rttime",
1611 }
1612 }
1613}
1614
1615impl LogSource {
1616 pub fn effective(requested: &[Self]) -> Vec<Self> {
1618 if requested.is_empty() {
1619 vec![Self::Stdout, Self::Stderr, Self::Output]
1620 } else {
1621 let mut sources = requested.to_vec();
1622 sources.sort_by_key(|src| match src {
1623 Self::Stdout => 0,
1624 Self::Stderr => 1,
1625 Self::Output => 2,
1626 Self::System => 3,
1627 });
1628 sources.dedup();
1629 sources
1630 }
1631 }
1632}
1633
1634impl SandboxLogLevel {
1635 pub const fn as_str(self) -> &'static str {
1637 match self {
1638 Self::Error => "error",
1639 Self::Warn => "warn",
1640 Self::Info => "info",
1641 Self::Debug => "debug",
1642 Self::Trace => "trace",
1643 }
1644 }
1645}
1646
1647impl std::fmt::Display for DiskImageFormat {
1652 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1653 f.write_str(self.as_str())
1654 }
1655}
1656
1657impl FromStr for DiskImageFormat {
1658 type Err = String;
1659
1660 fn from_str(s: &str) -> Result<Self, Self::Err> {
1661 match s {
1662 "qcow2" => Ok(Self::Qcow2),
1663 "raw" => Ok(Self::Raw),
1664 "vmdk" => Ok(Self::Vmdk),
1665 _ => Err(format!("unknown disk image format: {s}")),
1666 }
1667 }
1668}
1669
1670impl fmt::Display for TransparentHugePagePolicy {
1671 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1672 f.write_str(self.as_str())
1673 }
1674}
1675
1676impl FromStr for TransparentHugePagePolicy {
1677 type Err = String;
1678
1679 fn from_str(value: &str) -> Result<Self, Self::Err> {
1680 match value {
1681 "always" => Ok(Self::Always),
1682 "madvise" => Ok(Self::Madvise),
1683 "never" => Ok(Self::Never),
1684 _ => Err(format!(
1685 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1686 )),
1687 }
1688 }
1689}
1690
1691impl Default for RootfsSource {
1692 fn default() -> Self {
1693 Self::oci(String::new())
1694 }
1695}
1696
1697impl Default for SandboxResources {
1698 fn default() -> Self {
1699 Self {
1700 cpus: DEFAULT_SANDBOX_CPUS,
1701 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1702 max_cpus: DEFAULT_SANDBOX_CPUS,
1703 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1704 cpu_placement: CpuPlacement::Inherit,
1705 placement_profile: None,
1706 thp: TransparentHugePagePolicy::Madvise,
1707 }
1708 }
1709}
1710
1711impl<'de> Deserialize<'de> for SandboxResources {
1712 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1713 where
1714 D: serde::Deserializer<'de>,
1715 {
1716 #[derive(Deserialize)]
1717 struct RawResources {
1718 #[serde(default = "default_sandbox_cpus")]
1719 cpus: u8,
1720 #[serde(default = "default_sandbox_memory_mib")]
1721 memory_mib: u32,
1722 max_cpus: Option<u8>,
1723 max_memory_mib: Option<u32>,
1724 #[serde(default)]
1725 cpu_placement: CpuPlacement,
1726 #[serde(default)]
1727 placement_profile: Option<String>,
1728 #[serde(default)]
1729 thp: TransparentHugePagePolicy,
1730 }
1731
1732 let raw = RawResources::deserialize(deserializer)?;
1733 Ok(Self {
1734 cpus: raw.cpus,
1735 memory_mib: raw.memory_mib,
1736 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1740 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1741 cpu_placement: raw.cpu_placement,
1742 placement_profile: raw.placement_profile,
1743 thp: raw.thp,
1744 })
1745 }
1746}
1747
1748impl CpuPlacement {
1749 pub const fn is_inherit(&self) -> bool {
1751 matches!(self, Self::Inherit)
1752 }
1753}
1754
1755impl std::fmt::Display for CpuPlacement {
1756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1757 f.write_str(match self {
1758 Self::Inherit => "inherit",
1759 Self::Auto => "auto",
1760 Self::Spread => "spread",
1761 Self::Compact => "compact",
1762 })
1763 }
1764}
1765
1766impl FromStr for CpuPlacement {
1767 type Err = String;
1768
1769 fn from_str(value: &str) -> Result<Self, Self::Err> {
1770 match value {
1771 "inherit" => Ok(Self::Inherit),
1772 "auto" => Ok(Self::Auto),
1773 "spread" => Ok(Self::Spread),
1774 "compact" => Ok(Self::Compact),
1775 _ => Err(format!(
1776 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1777 )),
1778 }
1779 }
1780}
1781
1782impl Default for SandboxRuntimeOptions {
1783 fn default() -> Self {
1784 Self {
1785 workdir: None,
1786 shell: None,
1787 scripts: BTreeMap::new(),
1788 entrypoint: None,
1789 cmd: None,
1790 hostname: None,
1791 user: None,
1792 log_level: None,
1793 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1794 disable_metrics_sample: false,
1795 }
1796 }
1797}
1798
1799impl Default for NetworkSpec {
1800 fn default() -> Self {
1801 Self {
1802 enabled: true,
1803 interface: None,
1804 ports: Vec::new(),
1805 policy: None,
1806 dns: None,
1807 tls: None,
1808 strict: true,
1809 secrets: None,
1810 max_tcp_connections: None,
1811 max_udp_connections: None,
1812 rate_limiter: None,
1813 trust_host_cas: false,
1814 outbound_proxy: None,
1815 }
1816 }
1817}
1818
1819impl Default for PublishedPortSpec {
1820 fn default() -> Self {
1821 Self {
1822 host_port: 0,
1823 guest_port: 0,
1824 protocol: PortProtocol::Tcp,
1825 host_bind: "127.0.0.1".into(),
1826 }
1827 }
1828}
1829
1830impl From<(String, String)> for EnvVar {
1831 fn from((key, value): (String, String)) -> Self {
1832 Self { key, value }
1833 }
1834}
1835
1836impl From<EnvVar> for (String, String) {
1837 fn from(var: EnvVar) -> Self {
1838 (var.key, var.value)
1839 }
1840}
1841
1842impl FromStr for SandboxLogLevel {
1843 type Err = String;
1844
1845 fn from_str(s: &str) -> Result<Self, Self::Err> {
1846 match s {
1847 "error" => Ok(Self::Error),
1848 "warn" => Ok(Self::Warn),
1849 "info" => Ok(Self::Info),
1850 "debug" => Ok(Self::Debug),
1851 "trace" => Ok(Self::Trace),
1852 _ => Err(format!("unknown sandbox log level: {s}")),
1853 }
1854 }
1855}
1856
1857impl std::fmt::Display for SandboxLogLevel {
1858 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1859 formatter.write_str(self.as_str())
1860 }
1861}
1862
1863impl Serialize for VolumeMount {
1864 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1865 use serde::ser::SerializeMap;
1866
1867 match self {
1868 Self::Owned {
1869 guest,
1870 storage,
1871 options,
1872 stat_virtualization,
1873 host_permissions,
1874 } => {
1875 let mut map = serializer.serialize_map(Some(6))?;
1878 map.serialize_entry("type", "Owned")?;
1879 map.serialize_entry("guest", guest)?;
1880 map.serialize_entry("storage", storage)?;
1881 map.serialize_entry("options", options)?;
1882 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1883 map.serialize_entry("host_permissions", host_permissions)?;
1884 map.end()
1885 }
1886 Self::Bind {
1887 host,
1888 guest,
1889 options,
1890 stat_virtualization,
1891 host_permissions,
1892 follow_root_symlinks,
1893 quota_mib,
1894 } => {
1895 let mut map = serializer.serialize_map(Some(8))?;
1896 map.serialize_entry("type", "Bind")?;
1897 map.serialize_entry("host", host)?;
1898 map.serialize_entry("guest", guest)?;
1899 map.serialize_entry("options", options)?;
1900 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1901 map.serialize_entry("host_permissions", host_permissions)?;
1902 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1903 map.serialize_entry("quota_mib", quota_mib)?;
1904 map.end()
1905 }
1906 Self::Named {
1907 name,
1908 guest,
1909 create: _,
1910 options,
1911 stat_virtualization,
1912 host_permissions,
1913 follow_root_symlinks,
1914 } => {
1915 let mut map = serializer.serialize_map(Some(7))?;
1916 map.serialize_entry("type", "Named")?;
1917 map.serialize_entry("name", name)?;
1918 map.serialize_entry("guest", guest)?;
1919 map.serialize_entry("options", options)?;
1920 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1921 map.serialize_entry("host_permissions", host_permissions)?;
1922 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1923 map.end()
1924 }
1925 Self::Tmpfs {
1926 guest,
1927 size_mib,
1928 options,
1929 } => {
1930 let mut map = serializer.serialize_map(Some(4))?;
1931 map.serialize_entry("type", "Tmpfs")?;
1932 map.serialize_entry("guest", guest)?;
1933 map.serialize_entry("size_mib", size_mib)?;
1934 map.serialize_entry("options", options)?;
1935 map.end()
1936 }
1937 Self::DiskImage {
1938 host,
1939 guest,
1940 format,
1941 fstype,
1942 options,
1943 } => {
1944 let mut map = serializer.serialize_map(Some(6))?;
1945 map.serialize_entry("type", "DiskImage")?;
1946 map.serialize_entry("host", host)?;
1947 map.serialize_entry("guest", guest)?;
1948 map.serialize_entry("format", format)?;
1949 map.serialize_entry("fstype", fstype)?;
1950 map.serialize_entry("options", options)?;
1951 map.end()
1952 }
1953 }
1954 }
1955}
1956
1957impl<'de> Deserialize<'de> for VolumeMount {
1958 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1959 fn default_strict() -> StatVirtualization {
1960 StatVirtualization::Strict
1961 }
1962
1963 fn default_private() -> HostPermissions {
1964 HostPermissions::Private
1965 }
1966
1967 #[derive(Deserialize)]
1968 #[serde(tag = "type")]
1969 enum VolumeMountHelper {
1970 Owned {
1971 guest: String,
1972 storage: OwnedVolumeStorage,
1973 #[serde(default)]
1974 options: MountOptions,
1975 #[serde(default = "default_strict")]
1976 stat_virtualization: StatVirtualization,
1977 #[serde(default = "default_private")]
1978 host_permissions: HostPermissions,
1979 },
1980 Bind {
1981 host: PathBuf,
1982 guest: String,
1983 #[serde(default)]
1984 options: Option<MountOptions>,
1985 #[serde(default)]
1986 readonly: bool,
1987 #[serde(default = "default_strict")]
1988 stat_virtualization: StatVirtualization,
1989 #[serde(default = "default_private")]
1990 host_permissions: HostPermissions,
1991 #[serde(default)]
1992 follow_root_symlinks: bool,
1993 #[serde(default)]
1994 quota_mib: Option<u32>,
1995 },
1996 Named {
1997 name: String,
1998 guest: String,
1999 #[serde(default)]
2000 options: Option<MountOptions>,
2001 #[serde(default)]
2002 readonly: bool,
2003 #[serde(default = "default_strict")]
2004 stat_virtualization: StatVirtualization,
2005 #[serde(default = "default_private")]
2006 host_permissions: HostPermissions,
2007 #[serde(default)]
2008 follow_root_symlinks: bool,
2009 },
2010 Tmpfs {
2011 guest: String,
2012 #[serde(default)]
2013 size_mib: Option<u32>,
2014 #[serde(default)]
2015 options: Option<MountOptions>,
2016 #[serde(default)]
2017 readonly: bool,
2018 },
2019 DiskImage {
2020 host: PathBuf,
2021 guest: String,
2022 format: DiskImageFormat,
2023 #[serde(default)]
2024 fstype: Option<String>,
2025 #[serde(default)]
2026 options: Option<MountOptions>,
2027 #[serde(default)]
2028 readonly: bool,
2029 },
2030 }
2031
2032 let helper = VolumeMountHelper::deserialize(deserializer)?;
2033 Ok(match helper {
2034 VolumeMountHelper::Owned {
2035 guest,
2036 storage,
2037 options,
2038 stat_virtualization,
2039 host_permissions,
2040 } => Self::Owned {
2041 guest,
2042 storage,
2043 options,
2044 stat_virtualization,
2045 host_permissions,
2046 },
2047 VolumeMountHelper::Bind {
2048 host,
2049 guest,
2050 options,
2051 readonly,
2052 stat_virtualization,
2053 host_permissions,
2054 follow_root_symlinks,
2055 quota_mib,
2056 } => Self::Bind {
2057 host,
2058 guest,
2059 options: decode_mount_options(options, readonly),
2060 stat_virtualization,
2061 host_permissions,
2062 follow_root_symlinks,
2063 quota_mib,
2064 },
2065 VolumeMountHelper::Named {
2066 name,
2067 guest,
2068 options,
2069 readonly,
2070 stat_virtualization,
2071 host_permissions,
2072 follow_root_symlinks,
2073 } => Self::Named {
2074 name,
2075 guest,
2076 create: None,
2077 options: decode_mount_options(options, readonly),
2078 stat_virtualization,
2079 host_permissions,
2080 follow_root_symlinks,
2081 },
2082 VolumeMountHelper::Tmpfs {
2083 guest,
2084 size_mib,
2085 options,
2086 readonly,
2087 } => Self::Tmpfs {
2088 guest,
2089 size_mib,
2090 options: decode_mount_options(options, readonly),
2091 },
2092 VolumeMountHelper::DiskImage {
2093 host,
2094 guest,
2095 format,
2096 fstype,
2097 options,
2098 readonly,
2099 } => Self::DiskImage {
2100 host,
2101 guest,
2102 format,
2103 fstype,
2104 options: decode_mount_options(options, readonly),
2105 },
2106 })
2107 }
2108}
2109
2110impl fmt::Debug for VolumeMount {
2111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2112 match self {
2113 Self::Owned {
2114 guest,
2115 storage,
2116 options,
2117 stat_virtualization,
2118 host_permissions,
2119 } => f
2120 .debug_struct("Owned")
2121 .field("guest", guest)
2122 .field("storage", storage)
2123 .field("options", options)
2124 .field("stat_virtualization", stat_virtualization)
2125 .field("host_permissions", host_permissions)
2126 .finish(),
2127 Self::Bind {
2128 host,
2129 guest,
2130 options,
2131 stat_virtualization,
2132 host_permissions,
2133 follow_root_symlinks,
2134 quota_mib,
2135 } => f
2136 .debug_struct("Bind")
2137 .field("host", host)
2138 .field("guest", guest)
2139 .field("options", options)
2140 .field("stat_virtualization", stat_virtualization)
2141 .field("host_permissions", host_permissions)
2142 .field("follow_root_symlinks", follow_root_symlinks)
2143 .field("quota_mib", quota_mib)
2144 .finish(),
2145 Self::Named {
2146 name,
2147 guest,
2148 create,
2149 options,
2150 stat_virtualization,
2151 host_permissions,
2152 follow_root_symlinks,
2153 } => f
2154 .debug_struct("Named")
2155 .field("name", name)
2156 .field("guest", guest)
2157 .field("create", create)
2158 .field("options", options)
2159 .field("stat_virtualization", stat_virtualization)
2160 .field("host_permissions", host_permissions)
2161 .field("follow_root_symlinks", follow_root_symlinks)
2162 .finish(),
2163 Self::Tmpfs {
2164 guest,
2165 size_mib,
2166 options,
2167 } => f
2168 .debug_struct("Tmpfs")
2169 .field("guest", guest)
2170 .field("size_mib", size_mib)
2171 .field("options", options)
2172 .finish(),
2173 Self::DiskImage {
2174 host,
2175 guest,
2176 format,
2177 fstype,
2178 options,
2179 } => f
2180 .debug_struct("DiskImage")
2181 .field("host", host)
2182 .field("guest", guest)
2183 .field("format", format)
2184 .field("fstype", fstype)
2185 .field("options", options)
2186 .finish(),
2187 }
2188 }
2189}
2190
2191impl TryFrom<&str> for RlimitResource {
2193 type Error = String;
2194
2195 fn try_from(s: &str) -> Result<Self, Self::Error> {
2196 match s.to_ascii_lowercase().as_str() {
2197 "cpu" => Ok(Self::Cpu),
2198 "fsize" => Ok(Self::Fsize),
2199 "data" => Ok(Self::Data),
2200 "stack" => Ok(Self::Stack),
2201 "core" => Ok(Self::Core),
2202 "rss" => Ok(Self::Rss),
2203 "nproc" => Ok(Self::Nproc),
2204 "nofile" => Ok(Self::Nofile),
2205 "memlock" => Ok(Self::Memlock),
2206 "as" => Ok(Self::As),
2207 "locks" => Ok(Self::Locks),
2208 "sigpending" => Ok(Self::Sigpending),
2209 "msgqueue" => Ok(Self::Msgqueue),
2210 "nice" => Ok(Self::Nice),
2211 "rtprio" => Ok(Self::Rtprio),
2212 "rttime" => Ok(Self::Rttime),
2213 _ => Err(format!("unknown rlimit resource: {s}")),
2214 }
2215 }
2216}
2217
2218fn default_sandbox_cpus() -> u8 {
2223 DEFAULT_SANDBOX_CPUS
2224}
2225
2226fn default_sandbox_memory_mib() -> u32 {
2227 DEFAULT_SANDBOX_MEMORY_MIB
2228}
2229
2230fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2231 options.unwrap_or(MountOptions {
2232 readonly,
2233 ..MountOptions::default()
2234 })
2235}
2236
2237fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2238 for value in higher {
2239 match base.iter_mut().find(|current| current.key == value.key) {
2240 Some(current) => *current = value,
2241 None => base.push(value),
2242 }
2243 }
2244}
2245
2246fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2247 for value in higher {
2248 match base
2249 .iter_mut()
2250 .find(|current| current.env_var == value.env_var)
2251 {
2252 Some(current) => *current = value,
2253 None => base.push(value),
2254 }
2255 }
2256}
2257
2258pub(crate) fn default_strict() -> StatVirtualization {
2260 StatVirtualization::Strict
2261}
2262
2263pub(crate) fn default_private() -> HostPermissions {
2265 HostPermissions::Private
2266}
2267
2268pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2270
2271#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2282#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2283#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2284pub struct SecretsConfig {
2285 #[doc(hidden)]
2288 #[serde(default, skip_serializing_if = "Option::is_none")]
2289 #[cfg_attr(feature = "ts", ts(skip))]
2290 #[cfg_attr(feature = "utoipa", schema(ignore))]
2291 pub passthrough_hosts: Option<Vec<HostPattern>>,
2292
2293 #[serde(default)]
2295 #[config_patch(merge_with = merge_secret_entries)]
2296 pub secrets: Vec<SecretEntry>,
2297
2298 #[serde(default)]
2300 pub violation_action: SecretViolationAction,
2301}
2302
2303#[derive(Clone, Serialize, Deserialize)]
2308#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2309#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2310pub struct SecretEntry {
2311 pub env_var: String,
2317
2318 #[serde(default = "empty_secret_value")]
2327 #[cfg_attr(feature = "ts", ts(type = "string"))]
2328 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2329 pub value: Zeroizing<String>,
2330
2331 #[serde(default, skip_serializing_if = "Option::is_none")]
2335 pub source: Option<SecretSource>,
2336
2337 pub placeholder: String,
2342
2343 #[serde(default)]
2345 pub allowed_hosts: Vec<HostPattern>,
2346
2347 #[serde(default)]
2349 pub substitution: SecretSubstitution,
2350
2351 #[serde(default)]
2353 pub passthrough_hosts: Vec<HostPattern>,
2354
2355 #[serde(default, skip_serializing_if = "Option::is_none")]
2357 pub violation_action: Option<SecretViolationAction>,
2358
2359 #[serde(default = "default_true")]
2364 pub require_tls_identity: bool,
2365}
2366
2367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2369#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2370#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2371#[serde(rename_all = "kebab-case")]
2372pub enum HostPattern {
2373 #[serde(alias = "Exact")]
2375 Exact(String),
2376 #[serde(alias = "Wildcard")]
2378 Wildcard(String),
2379 #[serde(alias = "Any")]
2381 Any,
2382}
2383
2384#[derive(Debug, Clone, Serialize, Deserialize)]
2386#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2387#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2388pub struct SecretSubstitution {
2389 #[serde(default = "default_true")]
2391 pub headers: bool,
2392
2393 #[serde(default)]
2395 pub query: bool,
2396
2397 #[serde(default)]
2405 pub body: bool,
2406}
2407
2408#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2410#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2411#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2412#[serde(rename_all = "kebab-case")]
2413pub enum SecretViolationAction {
2414 #[serde(alias = "Block")]
2416 Block,
2417 #[default]
2419 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2420 BlockAndLog,
2421 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2423 BlockAndTerminate,
2424}
2425
2426#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2428pub enum SecretConfigError {
2429 #[error("secret #{secret_index}: env_var must not be empty")]
2431 EmptyEnvVar {
2432 secret_index: usize,
2434 },
2435
2436 #[error("secret #{secret_index}: env_var must not contain `=`")]
2438 EnvVarContainsEquals {
2439 secret_index: usize,
2441 },
2442
2443 #[error("secret #{secret_index}: env_var must not contain NUL")]
2445 EnvVarContainsNul {
2446 secret_index: usize,
2448 },
2449
2450 #[error("secret #{secret_index}: at least one allowed host is required")]
2452 MissingAllowedHosts {
2453 secret_index: usize,
2455 },
2456
2457 #[error("secret #{secret_index}: at least one substitution location is required")]
2459 MissingSubstitutionLocation {
2460 secret_index: usize,
2462 },
2463
2464 #[error("secret #{secret_index}: placeholder must not be empty")]
2466 EmptyPlaceholder {
2467 secret_index: usize,
2469 },
2470
2471 #[error(
2473 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2474 )]
2475 PlaceholderTooLong {
2476 secret_index: usize,
2478 actual_bytes: usize,
2480 max_bytes: usize,
2482 },
2483
2484 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2486 PlaceholderContainsNul {
2487 secret_index: usize,
2489 },
2490
2491 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2493 PlaceholderContainsLineBreak {
2494 secret_index: usize,
2496 },
2497}
2498
2499impl SecretsConfig {
2500 pub fn has_tls_identity_secrets(&self) -> bool {
2502 self.secrets
2503 .iter()
2504 .any(|secret| secret.require_tls_identity)
2505 }
2506
2507 pub fn contains_env_var(&self, env_var: &str) -> bool {
2509 self.secrets.iter().any(|secret| secret.env_var == env_var)
2510 }
2511
2512 pub fn validate(&self) -> Result<(), SecretConfigError> {
2514 for (index, secret) in self.secrets.iter().enumerate() {
2515 secret.validate(index)?;
2516 }
2517 Ok(())
2518 }
2519}
2520
2521impl SecretEntry {
2522 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2524 validate_env_var(&self.env_var, secret_index)?;
2525
2526 if self.allowed_hosts.is_empty() {
2527 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2528 }
2529
2530 if !self.substitution.headers && !self.substitution.query && !self.substitution.body {
2531 return Err(SecretConfigError::MissingSubstitutionLocation { secret_index });
2532 }
2533
2534 validate_placeholder(&self.placeholder, secret_index)
2535 }
2536}
2537
2538impl fmt::Debug for SecretEntry {
2540 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2541 f.debug_struct("SecretEntry")
2542 .field("env_var", &self.env_var)
2543 .field("value", &"[REDACTED]")
2544 .field("source", &self.source)
2545 .field("placeholder", &self.placeholder)
2546 .field("allowed_hosts", &self.allowed_hosts)
2547 .field("substitution", &self.substitution)
2548 .field("passthrough_hosts", &self.passthrough_hosts)
2549 .field("violation_action", &self.violation_action)
2550 .field("require_tls_identity", &self.require_tls_identity)
2551 .finish()
2552 }
2553}
2554
2555impl HostPattern {
2556 pub fn parse(host: &str) -> Self {
2559 if host == "*" {
2560 HostPattern::Any
2561 } else if host.starts_with("*.") {
2562 HostPattern::Wildcard(host.to_string())
2563 } else {
2564 HostPattern::Exact(host.to_string())
2565 }
2566 }
2567
2568 pub fn matches(&self, hostname: &str) -> bool {
2573 match self {
2574 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2575 HostPattern::Wildcard(pattern) => {
2576 if let Some(suffix) = pattern.strip_prefix("*.") {
2577 hostname.eq_ignore_ascii_case(suffix)
2578 || (hostname.len() > suffix.len() + 1
2579 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2580 && hostname[hostname.len() - suffix.len()..]
2581 .eq_ignore_ascii_case(suffix))
2582 } else {
2583 hostname.eq_ignore_ascii_case(pattern)
2584 }
2585 }
2586 HostPattern::Any => true,
2587 }
2588 }
2589}
2590
2591impl Default for SecretSubstitution {
2592 fn default() -> Self {
2593 Self {
2594 headers: true,
2595 query: false,
2596 body: false,
2597 }
2598 }
2599}
2600
2601fn default_true() -> bool {
2602 true
2603}
2604
2605fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2606 if env_var.is_empty() {
2607 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2608 }
2609 if env_var.contains('=') {
2610 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2611 }
2612 if env_var.contains('\0') {
2613 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2614 }
2615 Ok(())
2616}
2617
2618fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2619 if placeholder.is_empty() {
2620 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2621 }
2622
2623 let actual_bytes = placeholder.len();
2624 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2625 return Err(SecretConfigError::PlaceholderTooLong {
2626 secret_index,
2627 actual_bytes,
2628 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2629 });
2630 }
2631
2632 if placeholder.contains('\0') {
2633 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2634 }
2635 if placeholder.contains('\r') || placeholder.contains('\n') {
2636 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2637 }
2638
2639 Ok(())
2640}
2641
2642#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2652#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2653#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2654pub struct TlsConfig {
2655 #[serde(default)]
2657 pub enabled: bool,
2658
2659 #[serde(default = "default_intercepted_ports")]
2661 pub intercepted_ports: Vec<u16>,
2662
2663 #[serde(default)]
2665 pub bypass: Vec<String>,
2666
2667 #[serde(default = "default_true")]
2669 pub verify_upstream: bool,
2670
2671 #[serde(default = "default_true")]
2674 pub block_quic_on_intercept: bool,
2675
2676 #[serde(default)]
2678 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2679 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2680 pub upstream_ca_cert: Vec<PathBuf>,
2681
2682 #[serde(default, alias = "scoped_upstream_ca_certs")]
2684 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2685
2686 #[serde(default)]
2688 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2689
2690 #[serde(default, alias = "ca")]
2693 pub intercept_ca: InterceptCaConfig,
2694
2695 #[serde(default)]
2697 pub cache: CertCacheConfig,
2698}
2699
2700#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2702#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2703#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2704pub struct InterceptCaConfig {
2705 #[serde(default)]
2708 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2709 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2710 pub cert_path: Option<PathBuf>,
2711
2712 #[serde(default)]
2715 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2716 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2717 pub key_path: Option<PathBuf>,
2718}
2719
2720#[derive(Debug, Clone, Serialize, Deserialize)]
2722#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2723#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2724pub struct CertCacheConfig {
2725 #[serde(default = "default_cache_capacity")]
2727 pub capacity: usize,
2728
2729 #[serde(default = "default_cert_validity_hours")]
2731 pub validity_hours: u64,
2732}
2733
2734#[derive(Debug, Clone, Serialize, Deserialize)]
2736#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2737#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2738pub struct ScopedUpstreamCaCert {
2739 pub pattern: String,
2741
2742 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2744 #[cfg_attr(feature = "ts", ts(type = "string"))]
2745 pub path: PathBuf,
2746}
2747
2748#[derive(Debug, Clone, Serialize, Deserialize)]
2750#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2751#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2752pub struct ScopedVerifyUpstream {
2753 pub pattern: String,
2755
2756 pub verify: bool,
2758}
2759
2760impl Default for TlsConfig {
2761 fn default() -> Self {
2762 Self {
2763 enabled: false,
2764 intercepted_ports: default_intercepted_ports(),
2765 bypass: Vec::new(),
2766 verify_upstream: true,
2767 block_quic_on_intercept: true,
2768 upstream_ca_cert: Vec::new(),
2769 scoped_upstream_ca_cert: Vec::new(),
2770 scoped_verify_upstream: Vec::new(),
2771 intercept_ca: InterceptCaConfig::default(),
2772 cache: CertCacheConfig::default(),
2773 }
2774 }
2775}
2776
2777impl Default for CertCacheConfig {
2778 fn default() -> Self {
2779 Self {
2780 capacity: default_cache_capacity(),
2781 validity_hours: default_cert_validity_hours(),
2782 }
2783 }
2784}
2785
2786fn default_intercepted_ports() -> Vec<u16> {
2787 vec![443]
2788}
2789
2790fn default_cache_capacity() -> usize {
2791 1000
2792}
2793
2794fn default_cert_validity_hours() -> u64 {
2795 24
2796}
2797
2798#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2804#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2805#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2806#[serde(rename_all = "snake_case")]
2807pub enum Action {
2808 Allow,
2810 Deny,
2812}
2813
2814#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2816#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2817#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2818#[serde(rename_all = "snake_case")]
2819pub enum Direction {
2820 Egress,
2822 Ingress,
2824 Any,
2826}
2827
2828#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2830#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2831#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2832#[serde(rename_all = "snake_case")]
2833pub enum Protocol {
2834 Tcp,
2836 Udp,
2838 Icmpv4,
2840 Icmpv6,
2842}
2843
2844#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2846#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2847#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2848#[serde(rename_all = "snake_case")]
2849pub enum DestinationGroup {
2850 Public,
2852 Loopback,
2854 Private,
2856 LinkLocal,
2858 Metadata,
2860 Multicast,
2862 Host,
2864}
2865
2866#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2873#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2874#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2875#[serde(rename_all = "snake_case")]
2876pub enum Destination {
2877 Any,
2879 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2881 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2882 Domain(String),
2884 DomainSuffix(String),
2886 Group(DestinationGroup),
2888}
2889
2890#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2892#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2893#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2894pub struct PortRange {
2895 pub start: u16,
2897 pub end: u16,
2899}
2900
2901#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2904#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2905#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2906pub struct Rule {
2907 pub direction: Direction,
2909 pub destination: Destination,
2911 #[serde(default)]
2913 pub protocols: Vec<Protocol>,
2914 #[serde(default)]
2916 pub ports: Vec<PortRange>,
2917 pub action: Action,
2919}
2920
2921#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2924#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2925#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2926pub struct NetworkPolicy {
2927 #[serde(default = "action_deny")]
2929 pub default_egress: Action,
2930 #[serde(default = "action_deny")]
2932 pub default_ingress: Action,
2933 #[serde(default)]
2935 pub rules: Vec<Rule>,
2936}
2937
2938fn action_deny() -> Action {
2941 Action::Deny
2942}
2943
2944#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2950#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2951#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2952#[serde(default)]
2953pub struct DnsConfig {
2954 pub rebind_protection: bool,
2956 pub nameservers: Vec<String>,
2959 pub query_timeout_ms: u64,
2961}
2962
2963impl Default for DnsConfig {
2964 fn default() -> Self {
2965 Self {
2966 rebind_protection: true,
2967 nameservers: Vec::new(),
2968 query_timeout_ms: 5000,
2969 }
2970 }
2971}
2972
2973#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2977#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2978#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2979#[serde(default)]
2980pub struct InterfaceOverrides {
2981 #[serde(skip_serializing_if = "Option::is_none")]
2983 pub mac: Option<[u8; 6]>,
2984 #[serde(skip_serializing_if = "Option::is_none")]
2986 pub mtu: Option<u16>,
2987 #[serde(skip_serializing_if = "Option::is_none")]
2989 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2990 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2991 pub ipv4_address: Option<Ipv4Addr>,
2992 #[serde(skip_serializing_if = "Option::is_none")]
2994 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2995 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2996 pub ipv4_pool: Option<Ipv4Network>,
2997 #[serde(skip_serializing_if = "Option::is_none")]
2999 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3000 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3001 pub ipv6_address: Option<Ipv6Addr>,
3002 #[serde(skip_serializing_if = "Option::is_none")]
3004 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3005 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3006 pub ipv6_pool: Option<Ipv6Network>,
3007}
3008
3009fn empty_secret_value() -> Zeroizing<String> {
3010 Zeroizing::new(String::new())
3011}
3012
3013#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3019pub enum NetworkRateLimitDirection {
3020 Egress,
3022 Ingress,
3024}
3025
3026#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3028#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3029#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3030#[serde(default)]
3031pub struct NetworkRateLimiterConfig {
3032 #[serde(skip_serializing_if = "Option::is_none")]
3034 pub egress: Option<RateLimiterConfig>,
3035
3036 #[serde(skip_serializing_if = "Option::is_none")]
3038 pub ingress: Option<RateLimiterConfig>,
3039}
3040
3041#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3047#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3048#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3049#[serde(default)]
3050pub struct RateLimiterConfig {
3051 #[serde(skip_serializing_if = "Option::is_none")]
3053 pub bandwidth: Option<TokenBucketConfig>,
3054
3055 #[serde(skip_serializing_if = "Option::is_none")]
3057 pub ops: Option<TokenBucketConfig>,
3058}
3059
3060#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3066#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3067#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3068pub struct TokenBucketConfig {
3069 pub size: u64,
3071
3072 pub refill_time_ms: u64,
3075
3076 #[serde(default)]
3078 pub one_time_burst: u64,
3079}
3080
3081#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3083pub enum RateLimitConfigError {
3084 #[error("rate limiter must configure at least one of bandwidth or ops")]
3086 EmptyLimiter,
3087
3088 #[error("{bucket} bucket: size must be greater than zero")]
3090 ZeroSize {
3091 bucket: &'static str,
3093 },
3094
3095 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
3097 ZeroRefillTime {
3098 bucket: &'static str,
3100 },
3101}
3102
3103impl RateLimiterConfig {
3104 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
3106 if self.bandwidth.is_none() && self.ops.is_none() {
3107 return Err(RateLimitConfigError::EmptyLimiter);
3108 }
3109 if let Some(bandwidth) = &self.bandwidth {
3110 bandwidth.validate("bandwidth")?;
3111 }
3112 if let Some(ops) = &self.ops {
3113 ops.validate("ops")?;
3114 }
3115 Ok(())
3116 }
3117}
3118
3119impl TokenBucketConfig {
3120 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
3122 if self.size == 0 {
3123 return Err(RateLimitConfigError::ZeroSize { bucket });
3124 }
3125 if self.refill_time_ms == 0 {
3126 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
3127 }
3128 Ok(())
3129 }
3130}
3131
3132impl fmt::Display for NetworkRateLimitDirection {
3133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3134 match self {
3135 Self::Egress => f.write_str("egress"),
3136 Self::Ingress => f.write_str("ingress"),
3137 }
3138 }
3139}
3140
3141#[cfg(test)]
3146mod tests {
3147 use super::*;
3148
3149 fn secret_entry(env_var: &str, require_tls_identity: bool) -> SecretEntry {
3150 SecretEntry {
3151 env_var: env_var.to_owned(),
3152 value: Zeroizing::new("secret".to_owned()),
3153 source: None,
3154 placeholder: format!("$MSB_{env_var}"),
3155 allowed_hosts: vec![HostPattern::Any],
3156 substitution: SecretSubstitution::default(),
3157 passthrough_hosts: Vec::new(),
3158 violation_action: None,
3159 require_tls_identity,
3160 }
3161 }
3162
3163 fn tmpfs_mount(guest: &str) -> VolumeMount {
3164 VolumeMount::Tmpfs {
3165 guest: guest.to_owned(),
3166 size_mib: None,
3167 options: MountOptions::default(),
3168 }
3169 }
3170
3171 #[test]
3172 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
3173 let value = serde_json::to_value(MountOptions::default()).unwrap();
3174 assert!(value.get("override_uid").is_none());
3175 assert!(value.get("override_gid").is_none());
3176
3177 let decoded: MountOptions = serde_json::from_value(value).unwrap();
3178 assert_eq!(decoded.override_uid, None);
3179 assert_eq!(decoded.override_gid, None);
3180 }
3181
3182 #[test]
3183 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
3184 let mut mounts = vec![
3185 tmpfs_mount("/workspace//persist/./logs/"),
3186 tmpfs_mount("/alpha/z"),
3187 tmpfs_mount("/workspace"),
3188 ];
3189
3190 canonicalize_volume_mounts(&mut mounts).unwrap();
3191
3192 assert_eq!(
3193 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
3194 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
3195 );
3196 }
3197
3198 #[test]
3199 fn secrets_config_queries_entries() {
3200 let mut config = SecretsConfig {
3201 secrets: vec![secret_entry("HTTP_TOKEN", false)],
3202 ..Default::default()
3203 };
3204
3205 assert!(!config.has_tls_identity_secrets());
3206 assert!(config.contains_env_var("HTTP_TOKEN"));
3207 assert!(!config.contains_env_var("MISSING"));
3208
3209 config.secrets.push(secret_entry("API_KEY", true));
3210 assert!(config.has_tls_identity_secrets());
3211 }
3212
3213 #[test]
3214 fn volume_mounts_reject_duplicate_canonical_paths() {
3215 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
3216
3217 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3218
3219 assert!(error.to_string().contains("same guest path: /data/cache"));
3220 }
3221
3222 #[test]
3223 fn volume_mounts_reject_parent_components_before_normalizing() {
3224 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3225
3226 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3227
3228 assert!(error.to_string().contains("must not contain '..'"));
3229 }
3230
3231 #[test]
3232 fn disk_image_format_from_extension() {
3233 assert_eq!(
3234 DiskImageFormat::from_extension("qcow2"),
3235 Some(DiskImageFormat::Qcow2)
3236 );
3237 assert_eq!(
3238 DiskImageFormat::from_extension("raw"),
3239 Some(DiskImageFormat::Raw)
3240 );
3241 assert_eq!(
3242 DiskImageFormat::from_extension("vmdk"),
3243 Some(DiskImageFormat::Vmdk)
3244 );
3245 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3246 assert_eq!(DiskImageFormat::from_extension(""), None);
3247 }
3248
3249 #[test]
3250 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3251 let resources: SandboxResources =
3252 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3253
3254 assert_eq!(resources.cpus, 4);
3255 assert_eq!(resources.max_cpus, 4);
3256 assert_eq!(resources.memory_mib, 2048);
3257 assert_eq!(resources.max_memory_mib, 2048);
3258 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3259 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3260 assert_eq!(
3261 serde_json::to_value(resources).unwrap(),
3262 serde_json::json!({
3263 "cpus": 4,
3264 "memory_mib": 2048,
3265 "max_cpus": 4,
3266 "max_memory_mib": 2048
3267 })
3268 );
3269 }
3270
3271 #[test]
3272 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3273 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3274 assert!(inherited.get("cpu_placement").is_none());
3275
3276 for policy in [
3277 CpuPlacement::Auto,
3278 CpuPlacement::Spread,
3279 CpuPlacement::Compact,
3280 ] {
3281 let resources = SandboxResources {
3282 cpu_placement: policy,
3283 ..Default::default()
3284 };
3285 let json = serde_json::to_string(&resources).unwrap();
3286 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3287
3288 assert_eq!(decoded.cpu_placement, policy);
3289 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3290 }
3291 }
3292
3293 #[test]
3294 fn transparent_huge_page_policy_roundtrips_non_default() {
3295 let resources: SandboxResources = serde_json::from_str(
3296 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3297 )
3298 .unwrap();
3299
3300 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3301 assert_eq!(
3302 serde_json::to_value(resources).unwrap()["thp"],
3303 serde_json::json!("always")
3304 );
3305 assert_eq!(
3306 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3307 TransparentHugePagePolicy::Never
3308 );
3309 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3310 }
3311
3312 #[test]
3313 fn disk_image_format_display_roundtrip() {
3314 for format in [
3315 DiskImageFormat::Qcow2,
3316 DiskImageFormat::Raw,
3317 DiskImageFormat::Vmdk,
3318 ] {
3319 let rendered = format.to_string();
3320 let parsed: DiskImageFormat = rendered.parse().unwrap();
3321 assert_eq!(parsed, format);
3322 }
3323 }
3324
3325 #[test]
3326 fn disk_image_format_from_str_unknown() {
3327 assert!("ext4".parse::<DiskImageFormat>().is_err());
3328 }
3329
3330 #[test]
3331 fn log_source_effective_uses_default_user_program_sources() {
3332 assert_eq!(
3333 LogSource::effective(&[]),
3334 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3335 );
3336 }
3337
3338 #[test]
3339 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3340 assert_eq!(
3341 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3342 vec![LogSource::Stdout, LogSource::System]
3343 );
3344 }
3345
3346 #[test]
3347 fn rlimit_resource_parses_case_insensitively() {
3348 assert_eq!(
3349 RlimitResource::try_from("NOFILE").unwrap(),
3350 RlimitResource::Nofile
3351 );
3352 assert!(RlimitResource::try_from("bogus").is_err());
3353 }
3354
3355 #[test]
3356 fn sandbox_policy_serde_roundtrip() {
3357 let policy = SandboxPolicy {
3358 ephemeral: true,
3359 max_duration_secs: Some(3600),
3360 idle_timeout_secs: Some(120),
3361 };
3362
3363 let json = serde_json::to_string(&policy).unwrap();
3364 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3365
3366 assert!(decoded.ephemeral);
3367 assert_eq!(decoded.max_duration_secs, Some(3600));
3368 assert_eq!(decoded.idle_timeout_secs, Some(120));
3369 }
3370
3371 #[test]
3372 fn sandbox_policy_defaults_to_persistent() {
3373 assert!(!SandboxPolicy::default().ephemeral);
3374 }
3375
3376 #[test]
3377 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3378 let decoded: SandboxPolicy =
3381 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3382 assert!(!decoded.ephemeral);
3383 assert_eq!(decoded.max_duration_secs, Some(60));
3384 }
3385
3386 #[test]
3387 fn sandbox_spec_default_uses_static_resource_defaults() {
3388 let spec = SandboxSpec::default();
3389
3390 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3391 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3392 assert_eq!(
3393 spec.runtime.metrics_sample_interval_ms,
3394 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3395 );
3396 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3397 }
3398
3399 #[test]
3400 fn deployment_profile_uses_stable_snake_case_wire_values() {
3401 assert_eq!(
3402 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3403 r#""multi_tenant""#
3404 );
3405 assert_eq!(
3406 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3407 DeploymentProfile::SingleTenant
3408 );
3409 }
3410
3411 #[test]
3412 fn sandbox_log_level_roundtrips_lowercase_values() {
3413 for (input, expected) in [
3414 ("error", SandboxLogLevel::Error),
3415 ("warn", SandboxLogLevel::Warn),
3416 ("info", SandboxLogLevel::Info),
3417 ("debug", SandboxLogLevel::Debug),
3418 ("trace", SandboxLogLevel::Trace),
3419 ] {
3420 let parsed: SandboxLogLevel = input.parse().unwrap();
3421 assert_eq!(parsed, expected);
3422 assert_eq!(parsed.as_str(), input);
3423 }
3424 }
3425}