1use std::collections::BTreeMap;
4use std::fmt;
5use std::net::{Ipv4Addr, Ipv6Addr};
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
10use microsandbox_types_macros::ConfigPatch;
11use serde::{Deserialize, Serialize};
12use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath};
13use zeroize::Zeroizing;
14
15use crate::modify::SecretSource;
16use crate::{TypesError, TypesResult};
17
18pub const DEFAULT_SANDBOX_CPUS: u8 = 1;
24
25pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
27
28pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
38#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
39pub enum DiskImageFormat {
40 Qcow2,
42 Raw,
44 Vmdk,
46}
47
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
51#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
52#[serde(rename_all = "kebab-case")]
53pub enum FlatClone {
54 #[default]
56 Auto,
57
58 Copy,
60
61 Reflink,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
68pub enum RootfsSource {
69 Bind {
71 #[cfg_attr(feature = "ts", ts(type = "string"))]
73 path: PathBuf,
74 #[serde(default)]
81 follow_root_symlinks: bool,
82 },
83
84 Oci(OciRootfsSource),
86
87 DiskImage {
89 #[cfg_attr(feature = "ts", ts(type = "string"))]
91 path: PathBuf,
92 format: DiskImageFormat,
94 fstype: Option<String>,
96 },
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
102#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
103pub struct OciRootfsSource {
104 pub reference: String,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub root_disk: Option<RootDisk>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
119#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
120#[serde(tag = "kind", rename_all = "kebab-case")]
121pub enum RootDisk {
122 Managed {
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 size_mib: Option<u32>,
128 },
129
130 Tmpfs {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 size_mib: Option<u32>,
136 },
137
138 DiskImage {
141 #[cfg_attr(feature = "ts", ts(type = "string"))]
143 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
144 path: PathBuf,
145 format: DiskImageFormat,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 fstype: Option<String>,
150 },
151
152 Flat {
157 #[serde(default, skip_serializing_if = "Option::is_none")]
160 size_mib: Option<u32>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 fstype: Option<String>,
164 #[serde(default, skip_serializing_if = "FlatClone::is_auto")]
166 clone: FlatClone,
167 },
168}
169
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
172#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
173#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
174pub enum PullPolicy {
175 #[default]
177 IfMissing,
178
179 Always,
181
182 Never,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
195#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
196#[serde(rename_all = "lowercase")]
197pub enum StatVirtualization {
198 Strict,
200 Relaxed,
202 Off,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
211#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
212#[serde(rename_all = "lowercase")]
213pub enum HostPermissions {
214 Private,
216 Mirror,
218}
219
220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
222#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
223#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
224#[serde(rename_all = "lowercase")]
225pub enum SecurityProfile {
226 #[default]
230 Default,
231
232 Restricted,
236}
237
238#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
244#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
245#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
246#[serde(rename_all = "snake_case")]
247pub enum DeploymentProfile {
248 #[default]
250 SingleTenant,
251
252 MultiTenant,
254}
255
256#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
258#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
259#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
260#[serde(default)]
261pub struct MountOptions {
262 pub readonly: bool,
266
267 pub noexec: bool,
271
272 pub nosuid: bool,
274
275 pub nodev: bool,
277
278 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub override_uid: Option<u32>,
287
288 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub override_gid: Option<u32>,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
298#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
299pub enum VolumeKind {
300 Directory,
302
303 Disk,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
310#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
311pub struct VolumeSpec {
312 pub name: String,
314
315 pub kind: VolumeKind,
317
318 pub quota_mib: Option<u32>,
320
321 pub capacity_mib: Option<u32>,
323
324 pub labels: Vec<(String, String)>,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
331#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
332pub enum NamedVolumeMode {
333 Existing,
335
336 Create,
338
339 EnsureExists,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
346#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
347pub struct NamedVolumeCreate {
348 pub mode: NamedVolumeMode,
350
351 pub name: String,
353
354 pub kind: VolumeKind,
356
357 pub quota_mib: Option<u32>,
359
360 pub capacity_mib: Option<u32>,
362
363 pub labels: Vec<(String, String)>,
365}
366
367#[derive(Clone)]
369#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
370#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
371#[cfg_attr(feature = "ts", ts(tag = "type"))]
372pub enum VolumeMount {
373 Bind {
375 #[cfg_attr(feature = "ts", ts(type = "string"))]
377 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
378 host: PathBuf,
379 guest: String,
381 options: MountOptions,
383 stat_virtualization: StatVirtualization,
385 host_permissions: HostPermissions,
387 follow_root_symlinks: bool,
394 quota_mib: Option<u32>,
400 },
401
402 Named {
404 name: String,
406 guest: String,
408 create: Option<NamedVolumeCreate>,
412 options: MountOptions,
414 stat_virtualization: StatVirtualization,
416 host_permissions: HostPermissions,
418 follow_root_symlinks: bool,
423 },
424
425 Tmpfs {
427 guest: String,
429 size_mib: Option<u32>,
431 options: MountOptions,
433 },
434
435 DiskImage {
437 #[cfg_attr(feature = "ts", ts(type = "string"))]
439 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
440 host: PathBuf,
441 guest: String,
443 format: DiskImageFormat,
445 fstype: Option<String>,
447 options: MountOptions,
449 },
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
454#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
455#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
456pub enum Patch {
457 Text {
459 path: String,
461 content: String,
463 mode: Option<u32>,
465 replace: bool,
467 },
468
469 File {
471 path: String,
473 content: Vec<u8>,
475 mode: Option<u32>,
477 replace: bool,
479 },
480
481 CopyFile {
483 #[cfg_attr(feature = "ts", ts(type = "string"))]
485 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
486 src: PathBuf,
487 dst: String,
489 mode: Option<u32>,
491 replace: bool,
493 },
494
495 CopyDir {
497 #[cfg_attr(feature = "ts", ts(type = "string"))]
499 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
500 src: PathBuf,
501 dst: String,
503 replace: bool,
505 },
506
507 Symlink {
509 target: String,
511 link: String,
513 replace: bool,
515 },
516
517 Mkdir {
519 path: String,
521 mode: Option<u32>,
523 },
524
525 Remove {
527 path: String,
529 },
530
531 Append {
533 path: String,
535 content: String,
537 },
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
548#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
549#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
550#[serde(default)]
551pub struct NetworkSpec {
552 pub enabled: bool,
554
555 #[serde(skip_serializing_if = "Option::is_none")]
557 #[config_patch(nested)]
558 pub interface: Option<InterfaceOverrides>,
559
560 pub ports: Vec<PublishedPortSpec>,
562
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub policy: Option<NetworkPolicy>,
566
567 #[serde(skip_serializing_if = "Option::is_none")]
569 #[config_patch(nested)]
570 pub dns: Option<DnsConfig>,
571
572 #[serde(skip_serializing_if = "Option::is_none")]
574 #[config_patch(nested)]
575 pub tls: Option<TlsConfig>,
576
577 pub strict: bool,
579
580 #[serde(skip_serializing_if = "Option::is_none")]
582 #[config_patch(nested)]
583 pub secrets: Option<SecretsConfig>,
584
585 pub max_connections: Option<usize>,
587
588 #[serde(skip_serializing_if = "Option::is_none")]
590 #[config_patch(nested)]
591 pub rate_limiter: Option<NetworkRateLimiterConfig>,
592
593 pub trust_host_cas: bool,
595
596 #[serde(skip_serializing_if = "Option::is_none")]
599 pub outbound_proxy: Option<OutboundProxy>,
600}
601
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
604#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
605#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
606#[serde(tag = "protocol", rename_all = "lowercase")]
607#[non_exhaustive]
608pub enum OutboundProxy {
609 Socks4 {
611 address: String,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
615 user_id: Option<String>,
616 },
617
618 Socks5 {
620 address: String,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 credentials: Option<Socks5Credentials>,
625 },
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
633#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
634#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
635pub struct Socks5Credentials {
636 pub username: String,
638
639 pub password: SecretSource,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
645#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
646#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
647pub struct PublishedPortSpec {
648 pub host_port: u16,
650
651 pub guest_port: u16,
653
654 #[serde(default)]
656 pub protocol: PortProtocol,
657
658 pub host_bind: String,
660}
661
662#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
664#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
665#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
666pub enum PortProtocol {
667 #[default]
669 #[serde(rename = "tcp")]
670 Tcp,
671
672 #[serde(rename = "udp")]
674 Udp,
675}
676
677#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
683#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
684#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
685#[serde(default)]
686pub struct VsockSpec {
687 pub routes: Vec<VsockRouteSpec>,
689}
690
691impl VsockSpec {
692 pub fn is_empty(&self) -> bool {
694 self.routes.is_empty()
695 }
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
701#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
702pub struct VsockRouteSpec {
703 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
705 pub host_socket: PathBuf,
706
707 pub port: u32,
709
710 #[serde(default)]
712 pub socket_type: VsockSocketType,
713}
714
715#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
717#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
718#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
719#[serde(rename_all = "snake_case")]
720pub enum VsockSocketType {
721 #[default]
723 Stream,
724
725 Dgram,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
735#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
736#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
737pub struct HandoffInit {
738 pub cmd: String,
742
743 #[serde(default)]
745 pub args: Vec<String>,
746
747 #[serde(default)]
749 pub env: Vec<(String, String)>,
750}
751
752#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
758#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
759#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
760pub struct SandboxPolicy {
761 #[serde(default)]
770 pub ephemeral: bool,
771
772 pub max_duration_secs: Option<u64>,
774
775 pub idle_timeout_secs: Option<u64>,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize)]
790#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
791pub struct SnapshotSpec {
792 pub name: String,
794
795 #[serde(default)]
798 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
799 pub dest_dir: Option<PathBuf>,
800
801 pub source_sandbox: String,
803
804 pub labels: Vec<(String, String)>,
806
807 pub force: bool,
809
810 pub record_integrity: bool,
812
813 #[serde(default)]
819 pub resumable: bool,
820}
821
822#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
830#[config_patch(name = SandboxConfigPatch)]
831#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
832#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
833#[serde(default)]
834pub struct SandboxSpec {
835 pub name: String,
837
838 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
840 pub image: RootfsSource,
841
842 #[config_patch(nested)]
844 pub resources: SandboxResources,
845
846 #[config_patch(nested)]
848 pub runtime: SandboxRuntimeOptions,
849
850 #[config_patch(merge_with = merge_env_vars)]
852 pub env: Vec<EnvVar>,
853
854 #[config_patch(merge)]
856 pub labels: BTreeMap<String, String>,
857
858 pub rlimits: Vec<Rlimit>,
860
861 pub mounts: Vec<VolumeMount>,
863
864 pub patches: Vec<Patch>,
866
867 #[config_patch(nested)]
869 pub network: NetworkSpec,
870
871 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
873 #[config_patch(nested)]
874 pub vsock: VsockSpec,
875
876 pub init: Option<HandoffInit>,
878
879 pub pull_policy: PullPolicy,
881
882 pub security_profile: SecurityProfile,
884
885 pub deployment_profile: DeploymentProfile,
891
892 #[config_patch(nested)]
894 pub lifecycle: SandboxPolicy,
895}
896
897#[derive(Debug, Clone, Serialize, ConfigPatch)]
899#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
900#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
901pub struct SandboxResources {
902 pub cpus: u8,
904
905 pub memory_mib: u32,
907
908 pub max_cpus: u8,
910
911 pub max_memory_mib: u32,
913
914 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
916 pub cpu_placement: CpuPlacement,
917
918 #[serde(default, skip_serializing_if = "Option::is_none")]
920 pub placement_profile: Option<String>,
921
922 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
924 pub thp: TransparentHugePagePolicy,
925}
926
927#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
929#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
930#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
931#[serde(rename_all = "lowercase")]
932pub enum CpuPlacement {
933 #[default]
935 Inherit,
936
937 Auto,
939
940 Spread,
942
943 Compact,
945}
946
947#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
949#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
950#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
951#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
952pub enum NumaPlacement {
953 PreferSingle,
955 StrictSingle,
957 Inherit,
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
963#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
964#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
965#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
966pub enum MemoryPlacement {
967 FollowCpu,
969 Inherit,
971}
972
973#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
975#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
976#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
977#[serde(deny_unknown_fields)]
978pub struct PlacementProfile {
979 pub numa: NumaPlacement,
981 pub memory: MemoryPlacement,
983}
984
985#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
987#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
988#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
989#[serde(rename_all = "lowercase")]
990pub enum TransparentHugePagePolicy {
991 Always,
993
994 #[default]
996 Madvise,
997
998 Never,
1000}
1001
1002#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1004#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1005#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1006#[serde(default)]
1007pub struct SandboxRuntimeOptions {
1008 pub workdir: Option<String>,
1010
1011 pub shell: Option<String>,
1013
1014 #[config_patch(merge)]
1016 pub scripts: BTreeMap<String, String>,
1017
1018 pub entrypoint: Option<Vec<String>>,
1020
1021 pub cmd: Option<Vec<String>>,
1023
1024 pub hostname: Option<String>,
1026
1027 pub user: Option<String>,
1029
1030 pub log_level: Option<SandboxLogLevel>,
1032
1033 pub metrics_sample_interval_ms: Option<u64>,
1035
1036 pub disable_metrics_sample: bool,
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1042#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1043#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1044pub struct EnvVar {
1045 pub key: String,
1047
1048 pub value: String,
1050}
1051
1052#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1054#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1055#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1056#[serde(rename_all = "lowercase")]
1057pub enum SandboxLogLevel {
1058 Error,
1060
1061 Warn,
1063
1064 Info,
1066
1067 Debug,
1069
1070 Trace,
1072}
1073
1074#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1080#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1081#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1082pub enum RlimitResource {
1083 Cpu,
1085 Fsize,
1087 Data,
1089 Stack,
1091 Core,
1093 Rss,
1095 Nproc,
1097 Nofile,
1099 Memlock,
1101 As,
1103 Locks,
1105 Sigpending,
1107 Msgqueue,
1109 Nice,
1111 Rtprio,
1113 Rttime,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1119#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1120#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1121pub struct Rlimit {
1122 pub resource: RlimitResource,
1124
1125 pub soft: u64,
1127
1128 pub hard: u64,
1130}
1131
1132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1138#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1139#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1140#[serde(rename_all = "lowercase")]
1141pub enum LogSource {
1142 Stdout,
1144
1145 Stderr,
1147
1148 Output,
1150
1151 System,
1153}
1154
1155impl DiskImageFormat {
1160 pub fn as_str(&self) -> &'static str {
1162 match self {
1163 Self::Qcow2 => "qcow2",
1164 Self::Raw => "raw",
1165 Self::Vmdk => "vmdk",
1166 }
1167 }
1168
1169 pub fn from_extension(ext: &str) -> Option<Self> {
1173 match ext {
1174 "qcow2" => Some(Self::Qcow2),
1175 "raw" => Some(Self::Raw),
1176 "vmdk" => Some(Self::Vmdk),
1177 _ => None,
1178 }
1179 }
1180}
1181
1182impl OciRootfsSource {
1183 pub fn new(reference: impl Into<String>) -> Self {
1185 Self {
1186 reference: reference.into(),
1187 root_disk: None,
1188 }
1189 }
1190}
1191
1192impl TransparentHugePagePolicy {
1193 pub fn is_madvise(&self) -> bool {
1195 matches!(self, Self::Madvise)
1196 }
1197
1198 pub fn as_str(self) -> &'static str {
1200 match self {
1201 Self::Always => "always",
1202 Self::Madvise => "madvise",
1203 Self::Never => "never",
1204 }
1205 }
1206}
1207
1208impl RootDisk {
1209 pub fn managed(size_mib: u32) -> Self {
1211 Self::Managed {
1212 size_mib: Some(size_mib),
1213 }
1214 }
1215
1216 pub fn tmpfs(size_mib: u32) -> Self {
1218 Self::Tmpfs {
1219 size_mib: Some(size_mib),
1220 }
1221 }
1222
1223 pub fn flat(size_mib: u32) -> Self {
1225 Self::Flat {
1226 size_mib: Some(size_mib),
1227 fstype: None,
1228 clone: FlatClone::Auto,
1229 }
1230 }
1231
1232 pub fn size_mib(&self) -> Option<u32> {
1234 match self {
1235 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1236 *size_mib
1237 }
1238 Self::DiskImage { .. } => None,
1239 }
1240 }
1241
1242 pub fn kind_str(&self) -> &'static str {
1244 match self {
1245 Self::Managed { .. } => "managed",
1246 Self::Tmpfs { .. } => "tmpfs",
1247 Self::DiskImage { .. } => "disk-image",
1248 Self::Flat { .. } => "flat",
1249 }
1250 }
1251
1252 pub fn is_managed(&self) -> bool {
1254 matches!(self, Self::Managed { .. })
1255 }
1256}
1257
1258impl FlatClone {
1259 pub const fn as_str(self) -> &'static str {
1261 match self {
1262 Self::Auto => "auto",
1263 Self::Copy => "copy",
1264 Self::Reflink => "reflink",
1265 }
1266 }
1267
1268 pub const fn is_auto(&self) -> bool {
1270 matches!(self, Self::Auto)
1271 }
1272}
1273
1274impl RootfsSource {
1275 pub fn oci(reference: impl Into<String>) -> Self {
1277 Self::Oci(OciRootfsSource::new(reference))
1278 }
1279
1280 pub fn oci_reference(&self) -> Option<&str> {
1282 match self {
1283 Self::Oci(oci) => Some(&oci.reference),
1284 _ => None,
1285 }
1286 }
1287
1288 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1290 match self {
1291 Self::Oci(oci) => oci.root_disk.as_ref(),
1292 _ => None,
1293 }
1294 }
1295
1296 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1299 match self {
1300 Self::Oci(oci) => match &oci.root_disk {
1301 Some(RootDisk::Managed { size_mib }) => *size_mib,
1302 Some(_) => None,
1303 None => None,
1304 },
1305 _ => None,
1306 }
1307 }
1308}
1309
1310impl EnvVar {
1311 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1313 Self {
1314 key: key.into(),
1315 value: value.into(),
1316 }
1317 }
1318
1319 pub fn as_pair(&self) -> (&str, &str) {
1321 (&self.key, &self.value)
1322 }
1323}
1324
1325impl VolumeKind {
1326 pub fn as_str(self) -> &'static str {
1328 match self {
1329 Self::Directory => "dir",
1330 Self::Disk => "disk",
1331 }
1332 }
1333
1334 pub fn from_db_value(value: &str) -> Self {
1336 match value {
1337 "disk" => Self::Disk,
1338 _ => Self::Directory,
1339 }
1340 }
1341}
1342
1343impl VolumeSpec {
1344 pub fn new(name: impl Into<String>) -> Self {
1346 Self {
1347 name: name.into(),
1348 kind: VolumeKind::Directory,
1349 quota_mib: None,
1350 capacity_mib: None,
1351 labels: Vec::new(),
1352 }
1353 }
1354}
1355
1356impl NamedVolumeCreate {
1357 pub fn mode(&self) -> NamedVolumeMode {
1359 self.mode
1360 }
1361
1362 pub fn name(&self) -> &str {
1364 &self.name
1365 }
1366
1367 pub fn kind(&self) -> VolumeKind {
1369 self.kind
1370 }
1371
1372 pub fn quota_mib(&self) -> Option<u32> {
1374 self.quota_mib
1375 }
1376
1377 pub fn capacity_mib(&self) -> Option<u32> {
1379 self.capacity_mib
1380 }
1381
1382 pub fn labels(&self) -> &[(String, String)] {
1384 &self.labels
1385 }
1386}
1387
1388impl VolumeMount {
1389 pub fn guest(&self) -> &str {
1391 match self {
1392 Self::Bind { guest, .. }
1393 | Self::Named { guest, .. }
1394 | Self::Tmpfs { guest, .. }
1395 | Self::DiskImage { guest, .. } => guest,
1396 }
1397 }
1398
1399 fn guest_mut(&mut self) -> &mut String {
1400 match self {
1401 Self::Bind { guest, .. }
1402 | Self::Named { guest, .. }
1403 | Self::Tmpfs { guest, .. }
1404 | Self::DiskImage { guest, .. } => guest,
1405 }
1406 }
1407
1408 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1410 match self {
1411 Self::Named { create, .. } => create.as_ref(),
1412 _ => None,
1413 }
1414 }
1415}
1416
1417pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1428 for mount in mounts.iter_mut() {
1429 let canonical = canonical_guest_mount_path(mount.guest())?;
1430 *mount.guest_mut() = canonical;
1431 }
1432
1433 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1434
1435 for pair in mounts.windows(2) {
1436 if pair[0].guest() == pair[1].guest() {
1437 return Err(TypesError::invalid_config(format!(
1438 "multiple volumes cannot mount the same guest path: {}",
1439 pair[0].guest()
1440 )));
1441 }
1442 }
1443
1444 Ok(())
1445}
1446
1447fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1448 let path = Utf8UnixPath::new(guest);
1449
1450 if !path.is_valid() {
1451 return Err(TypesError::invalid_config(format!(
1452 "guest mount path must be a valid Unix path: {guest}"
1453 )));
1454 }
1455 if !path.is_absolute() {
1456 return Err(TypesError::invalid_config(format!(
1457 "guest mount path must be absolute: {guest}"
1458 )));
1459 }
1460 if path
1461 .components()
1462 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1463 {
1464 return Err(TypesError::invalid_config(format!(
1465 "guest mount path must not contain '..': {guest}"
1466 )));
1467 }
1468 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1469 return Err(TypesError::invalid_config(format!(
1470 "guest mount path must not contain ':', ';', or ',': {guest}"
1471 )));
1472 }
1473
1474 let canonical = path.normalize().to_string();
1475 if canonical == "/" {
1476 return Err(TypesError::invalid_config(
1477 "cannot mount a volume at guest root /",
1478 ));
1479 }
1480
1481 Ok(canonical)
1482}
1483
1484fn guest_mount_order_key(guest: &str) -> (usize, String) {
1485 let path = Utf8UnixPath::new(guest);
1486 let depth = path.components().filter(Utf8Component::is_normal).count();
1487 (depth, guest.to_owned())
1488}
1489
1490impl RlimitResource {
1491 pub fn as_str(&self) -> &'static str {
1493 match self {
1494 Self::Cpu => "cpu",
1495 Self::Fsize => "fsize",
1496 Self::Data => "data",
1497 Self::Stack => "stack",
1498 Self::Core => "core",
1499 Self::Rss => "rss",
1500 Self::Nproc => "nproc",
1501 Self::Nofile => "nofile",
1502 Self::Memlock => "memlock",
1503 Self::As => "as",
1504 Self::Locks => "locks",
1505 Self::Sigpending => "sigpending",
1506 Self::Msgqueue => "msgqueue",
1507 Self::Nice => "nice",
1508 Self::Rtprio => "rtprio",
1509 Self::Rttime => "rttime",
1510 }
1511 }
1512}
1513
1514impl LogSource {
1515 pub fn effective(requested: &[Self]) -> Vec<Self> {
1517 if requested.is_empty() {
1518 vec![Self::Stdout, Self::Stderr, Self::Output]
1519 } else {
1520 let mut sources = requested.to_vec();
1521 sources.sort_by_key(|src| match src {
1522 Self::Stdout => 0,
1523 Self::Stderr => 1,
1524 Self::Output => 2,
1525 Self::System => 3,
1526 });
1527 sources.dedup();
1528 sources
1529 }
1530 }
1531}
1532
1533impl SandboxLogLevel {
1534 pub const fn as_str(self) -> &'static str {
1536 match self {
1537 Self::Error => "error",
1538 Self::Warn => "warn",
1539 Self::Info => "info",
1540 Self::Debug => "debug",
1541 Self::Trace => "trace",
1542 }
1543 }
1544}
1545
1546impl std::fmt::Display for DiskImageFormat {
1551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1552 f.write_str(self.as_str())
1553 }
1554}
1555
1556impl FromStr for DiskImageFormat {
1557 type Err = String;
1558
1559 fn from_str(s: &str) -> Result<Self, Self::Err> {
1560 match s {
1561 "qcow2" => Ok(Self::Qcow2),
1562 "raw" => Ok(Self::Raw),
1563 "vmdk" => Ok(Self::Vmdk),
1564 _ => Err(format!("unknown disk image format: {s}")),
1565 }
1566 }
1567}
1568
1569impl fmt::Display for TransparentHugePagePolicy {
1570 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1571 f.write_str(self.as_str())
1572 }
1573}
1574
1575impl FromStr for TransparentHugePagePolicy {
1576 type Err = String;
1577
1578 fn from_str(value: &str) -> Result<Self, Self::Err> {
1579 match value {
1580 "always" => Ok(Self::Always),
1581 "madvise" => Ok(Self::Madvise),
1582 "never" => Ok(Self::Never),
1583 _ => Err(format!(
1584 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1585 )),
1586 }
1587 }
1588}
1589
1590impl Default for RootfsSource {
1591 fn default() -> Self {
1592 Self::oci(String::new())
1593 }
1594}
1595
1596impl Default for SandboxResources {
1597 fn default() -> Self {
1598 Self {
1599 cpus: DEFAULT_SANDBOX_CPUS,
1600 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1601 max_cpus: DEFAULT_SANDBOX_CPUS,
1602 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1603 cpu_placement: CpuPlacement::Inherit,
1604 placement_profile: None,
1605 thp: TransparentHugePagePolicy::Madvise,
1606 }
1607 }
1608}
1609
1610impl<'de> Deserialize<'de> for SandboxResources {
1611 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1612 where
1613 D: serde::Deserializer<'de>,
1614 {
1615 #[derive(Deserialize)]
1616 struct RawResources {
1617 #[serde(default = "default_sandbox_cpus")]
1618 cpus: u8,
1619 #[serde(default = "default_sandbox_memory_mib")]
1620 memory_mib: u32,
1621 max_cpus: Option<u8>,
1622 max_memory_mib: Option<u32>,
1623 #[serde(default)]
1624 cpu_placement: CpuPlacement,
1625 #[serde(default)]
1626 placement_profile: Option<String>,
1627 #[serde(default)]
1628 thp: TransparentHugePagePolicy,
1629 }
1630
1631 let raw = RawResources::deserialize(deserializer)?;
1632 Ok(Self {
1633 cpus: raw.cpus,
1634 memory_mib: raw.memory_mib,
1635 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1639 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1640 cpu_placement: raw.cpu_placement,
1641 placement_profile: raw.placement_profile,
1642 thp: raw.thp,
1643 })
1644 }
1645}
1646
1647impl CpuPlacement {
1648 pub const fn is_inherit(&self) -> bool {
1650 matches!(self, Self::Inherit)
1651 }
1652}
1653
1654impl std::fmt::Display for CpuPlacement {
1655 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1656 f.write_str(match self {
1657 Self::Inherit => "inherit",
1658 Self::Auto => "auto",
1659 Self::Spread => "spread",
1660 Self::Compact => "compact",
1661 })
1662 }
1663}
1664
1665impl FromStr for CpuPlacement {
1666 type Err = String;
1667
1668 fn from_str(value: &str) -> Result<Self, Self::Err> {
1669 match value {
1670 "inherit" => Ok(Self::Inherit),
1671 "auto" => Ok(Self::Auto),
1672 "spread" => Ok(Self::Spread),
1673 "compact" => Ok(Self::Compact),
1674 _ => Err(format!(
1675 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1676 )),
1677 }
1678 }
1679}
1680
1681impl Default for SandboxRuntimeOptions {
1682 fn default() -> Self {
1683 Self {
1684 workdir: None,
1685 shell: None,
1686 scripts: BTreeMap::new(),
1687 entrypoint: None,
1688 cmd: None,
1689 hostname: None,
1690 user: None,
1691 log_level: None,
1692 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1693 disable_metrics_sample: false,
1694 }
1695 }
1696}
1697
1698impl Default for NetworkSpec {
1699 fn default() -> Self {
1700 Self {
1701 enabled: true,
1702 interface: None,
1703 ports: Vec::new(),
1704 policy: None,
1705 dns: None,
1706 tls: None,
1707 strict: false,
1708 secrets: None,
1709 max_connections: None,
1710 rate_limiter: None,
1711 trust_host_cas: false,
1712 outbound_proxy: None,
1713 }
1714 }
1715}
1716
1717impl Default for PublishedPortSpec {
1718 fn default() -> Self {
1719 Self {
1720 host_port: 0,
1721 guest_port: 0,
1722 protocol: PortProtocol::Tcp,
1723 host_bind: "127.0.0.1".into(),
1724 }
1725 }
1726}
1727
1728impl From<(String, String)> for EnvVar {
1729 fn from((key, value): (String, String)) -> Self {
1730 Self { key, value }
1731 }
1732}
1733
1734impl From<EnvVar> for (String, String) {
1735 fn from(var: EnvVar) -> Self {
1736 (var.key, var.value)
1737 }
1738}
1739
1740impl FromStr for SandboxLogLevel {
1741 type Err = String;
1742
1743 fn from_str(s: &str) -> Result<Self, Self::Err> {
1744 match s {
1745 "error" => Ok(Self::Error),
1746 "warn" => Ok(Self::Warn),
1747 "info" => Ok(Self::Info),
1748 "debug" => Ok(Self::Debug),
1749 "trace" => Ok(Self::Trace),
1750 _ => Err(format!("unknown sandbox log level: {s}")),
1751 }
1752 }
1753}
1754
1755impl Serialize for VolumeMount {
1756 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1757 use serde::ser::SerializeMap;
1758
1759 match self {
1760 Self::Bind {
1761 host,
1762 guest,
1763 options,
1764 stat_virtualization,
1765 host_permissions,
1766 follow_root_symlinks,
1767 quota_mib,
1768 } => {
1769 let mut map = serializer.serialize_map(Some(8))?;
1770 map.serialize_entry("type", "Bind")?;
1771 map.serialize_entry("host", host)?;
1772 map.serialize_entry("guest", guest)?;
1773 map.serialize_entry("options", options)?;
1774 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1775 map.serialize_entry("host_permissions", host_permissions)?;
1776 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1777 map.serialize_entry("quota_mib", quota_mib)?;
1778 map.end()
1779 }
1780 Self::Named {
1781 name,
1782 guest,
1783 create: _,
1784 options,
1785 stat_virtualization,
1786 host_permissions,
1787 follow_root_symlinks,
1788 } => {
1789 let mut map = serializer.serialize_map(Some(7))?;
1790 map.serialize_entry("type", "Named")?;
1791 map.serialize_entry("name", name)?;
1792 map.serialize_entry("guest", guest)?;
1793 map.serialize_entry("options", options)?;
1794 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1795 map.serialize_entry("host_permissions", host_permissions)?;
1796 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1797 map.end()
1798 }
1799 Self::Tmpfs {
1800 guest,
1801 size_mib,
1802 options,
1803 } => {
1804 let mut map = serializer.serialize_map(Some(4))?;
1805 map.serialize_entry("type", "Tmpfs")?;
1806 map.serialize_entry("guest", guest)?;
1807 map.serialize_entry("size_mib", size_mib)?;
1808 map.serialize_entry("options", options)?;
1809 map.end()
1810 }
1811 Self::DiskImage {
1812 host,
1813 guest,
1814 format,
1815 fstype,
1816 options,
1817 } => {
1818 let mut map = serializer.serialize_map(Some(6))?;
1819 map.serialize_entry("type", "DiskImage")?;
1820 map.serialize_entry("host", host)?;
1821 map.serialize_entry("guest", guest)?;
1822 map.serialize_entry("format", format)?;
1823 map.serialize_entry("fstype", fstype)?;
1824 map.serialize_entry("options", options)?;
1825 map.end()
1826 }
1827 }
1828 }
1829}
1830
1831impl<'de> Deserialize<'de> for VolumeMount {
1832 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1833 fn default_strict() -> StatVirtualization {
1834 StatVirtualization::Strict
1835 }
1836
1837 fn default_private() -> HostPermissions {
1838 HostPermissions::Private
1839 }
1840
1841 #[derive(Deserialize)]
1842 #[serde(tag = "type")]
1843 enum VolumeMountHelper {
1844 Bind {
1845 host: PathBuf,
1846 guest: String,
1847 #[serde(default)]
1848 options: Option<MountOptions>,
1849 #[serde(default)]
1850 readonly: bool,
1851 #[serde(default = "default_strict")]
1852 stat_virtualization: StatVirtualization,
1853 #[serde(default = "default_private")]
1854 host_permissions: HostPermissions,
1855 #[serde(default)]
1856 follow_root_symlinks: bool,
1857 #[serde(default)]
1858 quota_mib: Option<u32>,
1859 },
1860 Named {
1861 name: String,
1862 guest: String,
1863 #[serde(default)]
1864 options: Option<MountOptions>,
1865 #[serde(default)]
1866 readonly: bool,
1867 #[serde(default = "default_strict")]
1868 stat_virtualization: StatVirtualization,
1869 #[serde(default = "default_private")]
1870 host_permissions: HostPermissions,
1871 #[serde(default)]
1872 follow_root_symlinks: bool,
1873 },
1874 Tmpfs {
1875 guest: String,
1876 #[serde(default)]
1877 size_mib: Option<u32>,
1878 #[serde(default)]
1879 options: Option<MountOptions>,
1880 #[serde(default)]
1881 readonly: bool,
1882 },
1883 DiskImage {
1884 host: PathBuf,
1885 guest: String,
1886 format: DiskImageFormat,
1887 #[serde(default)]
1888 fstype: Option<String>,
1889 #[serde(default)]
1890 options: Option<MountOptions>,
1891 #[serde(default)]
1892 readonly: bool,
1893 },
1894 }
1895
1896 let helper = VolumeMountHelper::deserialize(deserializer)?;
1897 Ok(match helper {
1898 VolumeMountHelper::Bind {
1899 host,
1900 guest,
1901 options,
1902 readonly,
1903 stat_virtualization,
1904 host_permissions,
1905 follow_root_symlinks,
1906 quota_mib,
1907 } => Self::Bind {
1908 host,
1909 guest,
1910 options: decode_mount_options(options, readonly),
1911 stat_virtualization,
1912 host_permissions,
1913 follow_root_symlinks,
1914 quota_mib,
1915 },
1916 VolumeMountHelper::Named {
1917 name,
1918 guest,
1919 options,
1920 readonly,
1921 stat_virtualization,
1922 host_permissions,
1923 follow_root_symlinks,
1924 } => Self::Named {
1925 name,
1926 guest,
1927 create: None,
1928 options: decode_mount_options(options, readonly),
1929 stat_virtualization,
1930 host_permissions,
1931 follow_root_symlinks,
1932 },
1933 VolumeMountHelper::Tmpfs {
1934 guest,
1935 size_mib,
1936 options,
1937 readonly,
1938 } => Self::Tmpfs {
1939 guest,
1940 size_mib,
1941 options: decode_mount_options(options, readonly),
1942 },
1943 VolumeMountHelper::DiskImage {
1944 host,
1945 guest,
1946 format,
1947 fstype,
1948 options,
1949 readonly,
1950 } => Self::DiskImage {
1951 host,
1952 guest,
1953 format,
1954 fstype,
1955 options: decode_mount_options(options, readonly),
1956 },
1957 })
1958 }
1959}
1960
1961impl fmt::Debug for VolumeMount {
1962 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1963 match self {
1964 Self::Bind {
1965 host,
1966 guest,
1967 options,
1968 stat_virtualization,
1969 host_permissions,
1970 follow_root_symlinks,
1971 quota_mib,
1972 } => f
1973 .debug_struct("Bind")
1974 .field("host", host)
1975 .field("guest", guest)
1976 .field("options", options)
1977 .field("stat_virtualization", stat_virtualization)
1978 .field("host_permissions", host_permissions)
1979 .field("follow_root_symlinks", follow_root_symlinks)
1980 .field("quota_mib", quota_mib)
1981 .finish(),
1982 Self::Named {
1983 name,
1984 guest,
1985 create,
1986 options,
1987 stat_virtualization,
1988 host_permissions,
1989 follow_root_symlinks,
1990 } => f
1991 .debug_struct("Named")
1992 .field("name", name)
1993 .field("guest", guest)
1994 .field("create", create)
1995 .field("options", options)
1996 .field("stat_virtualization", stat_virtualization)
1997 .field("host_permissions", host_permissions)
1998 .field("follow_root_symlinks", follow_root_symlinks)
1999 .finish(),
2000 Self::Tmpfs {
2001 guest,
2002 size_mib,
2003 options,
2004 } => f
2005 .debug_struct("Tmpfs")
2006 .field("guest", guest)
2007 .field("size_mib", size_mib)
2008 .field("options", options)
2009 .finish(),
2010 Self::DiskImage {
2011 host,
2012 guest,
2013 format,
2014 fstype,
2015 options,
2016 } => f
2017 .debug_struct("DiskImage")
2018 .field("host", host)
2019 .field("guest", guest)
2020 .field("format", format)
2021 .field("fstype", fstype)
2022 .field("options", options)
2023 .finish(),
2024 }
2025 }
2026}
2027
2028impl TryFrom<&str> for RlimitResource {
2030 type Error = String;
2031
2032 fn try_from(s: &str) -> Result<Self, Self::Error> {
2033 match s.to_ascii_lowercase().as_str() {
2034 "cpu" => Ok(Self::Cpu),
2035 "fsize" => Ok(Self::Fsize),
2036 "data" => Ok(Self::Data),
2037 "stack" => Ok(Self::Stack),
2038 "core" => Ok(Self::Core),
2039 "rss" => Ok(Self::Rss),
2040 "nproc" => Ok(Self::Nproc),
2041 "nofile" => Ok(Self::Nofile),
2042 "memlock" => Ok(Self::Memlock),
2043 "as" => Ok(Self::As),
2044 "locks" => Ok(Self::Locks),
2045 "sigpending" => Ok(Self::Sigpending),
2046 "msgqueue" => Ok(Self::Msgqueue),
2047 "nice" => Ok(Self::Nice),
2048 "rtprio" => Ok(Self::Rtprio),
2049 "rttime" => Ok(Self::Rttime),
2050 _ => Err(format!("unknown rlimit resource: {s}")),
2051 }
2052 }
2053}
2054
2055fn default_sandbox_cpus() -> u8 {
2060 DEFAULT_SANDBOX_CPUS
2061}
2062
2063fn default_sandbox_memory_mib() -> u32 {
2064 DEFAULT_SANDBOX_MEMORY_MIB
2065}
2066
2067fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2068 options.unwrap_or(MountOptions {
2069 readonly,
2070 ..MountOptions::default()
2071 })
2072}
2073
2074fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2075 for value in higher {
2076 match base.iter_mut().find(|current| current.key == value.key) {
2077 Some(current) => *current = value,
2078 None => base.push(value),
2079 }
2080 }
2081}
2082
2083fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2084 for value in higher {
2085 match base
2086 .iter_mut()
2087 .find(|current| current.env_var == value.env_var)
2088 {
2089 Some(current) => *current = value,
2090 None => base.push(value),
2091 }
2092 }
2093}
2094
2095pub(crate) fn default_strict() -> StatVirtualization {
2097 StatVirtualization::Strict
2098}
2099
2100pub(crate) fn default_private() -> HostPermissions {
2102 HostPermissions::Private
2103}
2104
2105pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2107
2108#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2115#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2116#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2117pub struct SecretsConfig {
2118 #[serde(default)]
2120 #[config_patch(merge_with = merge_secret_entries)]
2121 pub secrets: Vec<SecretEntry>,
2122
2123 #[serde(default)]
2125 pub on_violation: ViolationAction,
2126}
2127
2128#[derive(Clone, Serialize, Deserialize)]
2133#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2134#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2135pub struct SecretEntry {
2136 pub env_var: String,
2142
2143 #[serde(default = "empty_secret_value")]
2152 #[cfg_attr(feature = "ts", ts(type = "string"))]
2153 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2154 pub value: Zeroizing<String>,
2155
2156 #[serde(default, skip_serializing_if = "Option::is_none")]
2160 pub source: Option<SecretSource>,
2161
2162 pub placeholder: String,
2167
2168 #[serde(default)]
2170 pub allowed_hosts: Vec<HostPattern>,
2171
2172 #[serde(default)]
2174 pub injection: SecretInjection,
2175
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2178 pub on_violation: Option<ViolationAction>,
2179
2180 #[serde(default = "default_true")]
2185 pub require_tls_identity: bool,
2186}
2187
2188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2190#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2191#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2192#[serde(rename_all = "kebab-case")]
2193pub enum HostPattern {
2194 #[serde(alias = "Exact")]
2196 Exact(String),
2197 #[serde(alias = "Wildcard")]
2199 Wildcard(String),
2200 #[serde(alias = "Any")]
2202 Any,
2203}
2204
2205#[derive(Debug, Clone, Serialize, Deserialize)]
2207#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2208#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2209pub struct SecretInjection {
2210 #[serde(default = "default_true")]
2212 pub headers: bool,
2213
2214 #[serde(default = "default_true")]
2216 pub basic_auth: bool,
2217
2218 #[serde(default)]
2220 pub query_params: bool,
2221
2222 #[serde(default)]
2230 pub body: bool,
2231}
2232
2233#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2235#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2236#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2237#[serde(rename_all = "kebab-case")]
2238pub enum ViolationAction {
2239 #[serde(alias = "Block")]
2241 Block,
2242 #[default]
2244 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2245 BlockAndLog,
2246 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2248 BlockAndTerminate,
2249 #[serde(alias = "Passthrough")]
2251 Passthrough(Vec<HostPattern>),
2252}
2253
2254#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2256pub enum SecretConfigError {
2257 #[error("secret #{secret_index}: env_var must not be empty")]
2259 EmptyEnvVar {
2260 secret_index: usize,
2262 },
2263
2264 #[error("secret #{secret_index}: env_var must not contain `=`")]
2266 EnvVarContainsEquals {
2267 secret_index: usize,
2269 },
2270
2271 #[error("secret #{secret_index}: env_var must not contain NUL")]
2273 EnvVarContainsNul {
2274 secret_index: usize,
2276 },
2277
2278 #[error("secret #{secret_index}: at least one allowed host is required")]
2280 MissingAllowedHosts {
2281 secret_index: usize,
2283 },
2284
2285 #[error("secret #{secret_index}: placeholder must not be empty")]
2287 EmptyPlaceholder {
2288 secret_index: usize,
2290 },
2291
2292 #[error(
2294 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2295 )]
2296 PlaceholderTooLong {
2297 secret_index: usize,
2299 actual_bytes: usize,
2301 max_bytes: usize,
2303 },
2304
2305 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2307 PlaceholderContainsNul {
2308 secret_index: usize,
2310 },
2311
2312 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2314 PlaceholderContainsLineBreak {
2315 secret_index: usize,
2317 },
2318}
2319
2320impl SecretsConfig {
2321 pub fn validate(&self) -> Result<(), SecretConfigError> {
2323 for (index, secret) in self.secrets.iter().enumerate() {
2324 secret.validate(index)?;
2325 }
2326 Ok(())
2327 }
2328}
2329
2330impl SecretEntry {
2331 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2333 validate_env_var(&self.env_var, secret_index)?;
2334
2335 if self.allowed_hosts.is_empty() {
2336 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2337 }
2338
2339 validate_placeholder(&self.placeholder, secret_index)
2340 }
2341}
2342
2343impl fmt::Debug for SecretEntry {
2345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2346 f.debug_struct("SecretEntry")
2347 .field("env_var", &self.env_var)
2348 .field("value", &"[REDACTED]")
2349 .field("source", &self.source)
2350 .field("placeholder", &self.placeholder)
2351 .field("allowed_hosts", &self.allowed_hosts)
2352 .field("injection", &self.injection)
2353 .field("on_violation", &self.on_violation)
2354 .field("require_tls_identity", &self.require_tls_identity)
2355 .finish()
2356 }
2357}
2358
2359impl HostPattern {
2360 pub fn parse(host: &str) -> Self {
2363 if host == "*" {
2364 HostPattern::Any
2365 } else if host.starts_with("*.") {
2366 HostPattern::Wildcard(host.to_string())
2367 } else {
2368 HostPattern::Exact(host.to_string())
2369 }
2370 }
2371
2372 pub fn matches(&self, hostname: &str) -> bool {
2377 match self {
2378 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2379 HostPattern::Wildcard(pattern) => {
2380 if let Some(suffix) = pattern.strip_prefix("*.") {
2381 hostname.eq_ignore_ascii_case(suffix)
2382 || (hostname.len() > suffix.len() + 1
2383 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2384 && hostname[hostname.len() - suffix.len()..]
2385 .eq_ignore_ascii_case(suffix))
2386 } else {
2387 hostname.eq_ignore_ascii_case(pattern)
2388 }
2389 }
2390 HostPattern::Any => true,
2391 }
2392 }
2393}
2394
2395impl Default for SecretInjection {
2396 fn default() -> Self {
2397 Self {
2398 headers: true,
2399 basic_auth: true,
2400 query_params: false,
2401 body: false,
2402 }
2403 }
2404}
2405
2406fn default_true() -> bool {
2407 true
2408}
2409
2410fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2411 if env_var.is_empty() {
2412 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2413 }
2414 if env_var.contains('=') {
2415 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2416 }
2417 if env_var.contains('\0') {
2418 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2419 }
2420 Ok(())
2421}
2422
2423fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2424 if placeholder.is_empty() {
2425 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2426 }
2427
2428 let actual_bytes = placeholder.len();
2429 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2430 return Err(SecretConfigError::PlaceholderTooLong {
2431 secret_index,
2432 actual_bytes,
2433 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2434 });
2435 }
2436
2437 if placeholder.contains('\0') {
2438 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2439 }
2440 if placeholder.contains('\r') || placeholder.contains('\n') {
2441 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2442 }
2443
2444 Ok(())
2445}
2446
2447#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2457#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2458#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2459pub struct TlsConfig {
2460 #[serde(default)]
2462 pub enabled: bool,
2463
2464 #[serde(default = "default_intercepted_ports")]
2466 pub intercepted_ports: Vec<u16>,
2467
2468 #[serde(default)]
2470 pub bypass: Vec<String>,
2471
2472 #[serde(default = "default_true")]
2474 pub verify_upstream: bool,
2475
2476 #[serde(default = "default_true")]
2479 pub block_quic_on_intercept: bool,
2480
2481 #[serde(default)]
2483 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2484 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2485 pub upstream_ca_cert: Vec<PathBuf>,
2486
2487 #[serde(default, alias = "scoped_upstream_ca_certs")]
2489 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2490
2491 #[serde(default)]
2493 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2494
2495 #[serde(default, alias = "ca")]
2498 pub intercept_ca: InterceptCaConfig,
2499
2500 #[serde(default)]
2502 pub cache: CertCacheConfig,
2503}
2504
2505#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2507#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2508#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2509pub struct InterceptCaConfig {
2510 #[serde(default)]
2513 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2514 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2515 pub cert_path: Option<PathBuf>,
2516
2517 #[serde(default)]
2520 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2521 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2522 pub key_path: Option<PathBuf>,
2523}
2524
2525#[derive(Debug, Clone, Serialize, Deserialize)]
2527#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2528#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2529pub struct CertCacheConfig {
2530 #[serde(default = "default_cache_capacity")]
2532 pub capacity: usize,
2533
2534 #[serde(default = "default_cert_validity_hours")]
2536 pub validity_hours: u64,
2537}
2538
2539#[derive(Debug, Clone, Serialize, Deserialize)]
2541#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2542#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2543pub struct ScopedUpstreamCaCert {
2544 pub pattern: String,
2546
2547 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2549 #[cfg_attr(feature = "ts", ts(type = "string"))]
2550 pub path: PathBuf,
2551}
2552
2553#[derive(Debug, Clone, Serialize, Deserialize)]
2555#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2556#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2557pub struct ScopedVerifyUpstream {
2558 pub pattern: String,
2560
2561 pub verify: bool,
2563}
2564
2565impl Default for TlsConfig {
2566 fn default() -> Self {
2567 Self {
2568 enabled: false,
2569 intercepted_ports: default_intercepted_ports(),
2570 bypass: Vec::new(),
2571 verify_upstream: true,
2572 block_quic_on_intercept: true,
2573 upstream_ca_cert: Vec::new(),
2574 scoped_upstream_ca_cert: Vec::new(),
2575 scoped_verify_upstream: Vec::new(),
2576 intercept_ca: InterceptCaConfig::default(),
2577 cache: CertCacheConfig::default(),
2578 }
2579 }
2580}
2581
2582impl Default for CertCacheConfig {
2583 fn default() -> Self {
2584 Self {
2585 capacity: default_cache_capacity(),
2586 validity_hours: default_cert_validity_hours(),
2587 }
2588 }
2589}
2590
2591fn default_intercepted_ports() -> Vec<u16> {
2592 vec![443]
2593}
2594
2595fn default_cache_capacity() -> usize {
2596 1000
2597}
2598
2599fn default_cert_validity_hours() -> u64 {
2600 24
2601}
2602
2603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2609#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2610#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2611#[serde(rename_all = "snake_case")]
2612pub enum Action {
2613 Allow,
2615 Deny,
2617}
2618
2619#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2621#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2622#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2623#[serde(rename_all = "snake_case")]
2624pub enum Direction {
2625 Egress,
2627 Ingress,
2629 Any,
2631}
2632
2633#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2635#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2636#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2637#[serde(rename_all = "snake_case")]
2638pub enum Protocol {
2639 Tcp,
2641 Udp,
2643 Icmpv4,
2645 Icmpv6,
2647}
2648
2649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2651#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2652#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2653#[serde(rename_all = "snake_case")]
2654pub enum DestinationGroup {
2655 Public,
2657 Loopback,
2659 Private,
2661 LinkLocal,
2663 Metadata,
2665 Multicast,
2667 Host,
2669}
2670
2671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2678#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2679#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2680#[serde(rename_all = "snake_case")]
2681pub enum Destination {
2682 Any,
2684 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2686 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2687 Domain(String),
2689 DomainSuffix(String),
2691 Group(DestinationGroup),
2693}
2694
2695#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2697#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2698#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2699pub struct PortRange {
2700 pub start: u16,
2702 pub end: u16,
2704}
2705
2706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2709#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2710#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2711pub struct Rule {
2712 pub direction: Direction,
2714 pub destination: Destination,
2716 #[serde(default)]
2718 pub protocols: Vec<Protocol>,
2719 #[serde(default)]
2721 pub ports: Vec<PortRange>,
2722 pub action: Action,
2724}
2725
2726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2729#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2730#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2731pub struct NetworkPolicy {
2732 #[serde(default = "action_deny")]
2734 pub default_egress: Action,
2735 #[serde(default = "action_deny")]
2737 pub default_ingress: Action,
2738 #[serde(default)]
2740 pub rules: Vec<Rule>,
2741}
2742
2743fn action_deny() -> Action {
2746 Action::Deny
2747}
2748
2749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2755#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2756#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2757#[serde(default)]
2758pub struct DnsConfig {
2759 pub rebind_protection: bool,
2761 pub nameservers: Vec<String>,
2764 pub query_timeout_ms: u64,
2766}
2767
2768impl Default for DnsConfig {
2769 fn default() -> Self {
2770 Self {
2771 rebind_protection: true,
2772 nameservers: Vec::new(),
2773 query_timeout_ms: 5000,
2774 }
2775 }
2776}
2777
2778#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2782#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2783#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2784#[serde(default)]
2785pub struct InterfaceOverrides {
2786 #[serde(skip_serializing_if = "Option::is_none")]
2788 pub mac: Option<[u8; 6]>,
2789 #[serde(skip_serializing_if = "Option::is_none")]
2791 pub mtu: Option<u16>,
2792 #[serde(skip_serializing_if = "Option::is_none")]
2794 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2795 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2796 pub ipv4_address: Option<Ipv4Addr>,
2797 #[serde(skip_serializing_if = "Option::is_none")]
2799 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2800 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2801 pub ipv4_pool: Option<Ipv4Network>,
2802 #[serde(skip_serializing_if = "Option::is_none")]
2804 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2805 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2806 pub ipv6_address: Option<Ipv6Addr>,
2807 #[serde(skip_serializing_if = "Option::is_none")]
2809 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2810 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2811 pub ipv6_pool: Option<Ipv6Network>,
2812}
2813
2814fn empty_secret_value() -> Zeroizing<String> {
2815 Zeroizing::new(String::new())
2816}
2817
2818#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2824pub enum NetworkRateLimitDirection {
2825 Egress,
2827 Ingress,
2829}
2830
2831#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2833#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2834#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2835#[serde(default)]
2836pub struct NetworkRateLimiterConfig {
2837 #[serde(skip_serializing_if = "Option::is_none")]
2839 pub egress: Option<RateLimiterConfig>,
2840
2841 #[serde(skip_serializing_if = "Option::is_none")]
2843 pub ingress: Option<RateLimiterConfig>,
2844}
2845
2846#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2852#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2853#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2854#[serde(default)]
2855pub struct RateLimiterConfig {
2856 #[serde(skip_serializing_if = "Option::is_none")]
2858 pub bandwidth: Option<TokenBucketConfig>,
2859
2860 #[serde(skip_serializing_if = "Option::is_none")]
2862 pub ops: Option<TokenBucketConfig>,
2863}
2864
2865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2871#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2872#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2873pub struct TokenBucketConfig {
2874 pub size: u64,
2876
2877 pub refill_time_ms: u64,
2880
2881 #[serde(default)]
2883 pub one_time_burst: u64,
2884}
2885
2886#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2888pub enum RateLimitConfigError {
2889 #[error("rate limiter must configure at least one of bandwidth or ops")]
2891 EmptyLimiter,
2892
2893 #[error("{bucket} bucket: size must be greater than zero")]
2895 ZeroSize {
2896 bucket: &'static str,
2898 },
2899
2900 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
2902 ZeroRefillTime {
2903 bucket: &'static str,
2905 },
2906}
2907
2908impl RateLimiterConfig {
2909 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
2911 if self.bandwidth.is_none() && self.ops.is_none() {
2912 return Err(RateLimitConfigError::EmptyLimiter);
2913 }
2914 if let Some(bandwidth) = &self.bandwidth {
2915 bandwidth.validate("bandwidth")?;
2916 }
2917 if let Some(ops) = &self.ops {
2918 ops.validate("ops")?;
2919 }
2920 Ok(())
2921 }
2922}
2923
2924impl TokenBucketConfig {
2925 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
2927 if self.size == 0 {
2928 return Err(RateLimitConfigError::ZeroSize { bucket });
2929 }
2930 if self.refill_time_ms == 0 {
2931 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
2932 }
2933 Ok(())
2934 }
2935}
2936
2937impl fmt::Display for NetworkRateLimitDirection {
2938 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2939 match self {
2940 Self::Egress => f.write_str("egress"),
2941 Self::Ingress => f.write_str("ingress"),
2942 }
2943 }
2944}
2945
2946#[cfg(test)]
2951mod tests {
2952 use super::*;
2953
2954 fn tmpfs_mount(guest: &str) -> VolumeMount {
2955 VolumeMount::Tmpfs {
2956 guest: guest.to_owned(),
2957 size_mib: None,
2958 options: MountOptions::default(),
2959 }
2960 }
2961
2962 #[test]
2963 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
2964 let value = serde_json::to_value(MountOptions::default()).unwrap();
2965 assert!(value.get("override_uid").is_none());
2966 assert!(value.get("override_gid").is_none());
2967
2968 let decoded: MountOptions = serde_json::from_value(value).unwrap();
2969 assert_eq!(decoded.override_uid, None);
2970 assert_eq!(decoded.override_gid, None);
2971 }
2972
2973 #[test]
2974 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
2975 let mut mounts = vec![
2976 tmpfs_mount("/workspace//persist/./logs/"),
2977 tmpfs_mount("/alpha/z"),
2978 tmpfs_mount("/workspace"),
2979 ];
2980
2981 canonicalize_volume_mounts(&mut mounts).unwrap();
2982
2983 assert_eq!(
2984 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
2985 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
2986 );
2987 }
2988
2989 #[test]
2990 fn volume_mounts_reject_duplicate_canonical_paths() {
2991 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
2992
2993 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
2994
2995 assert!(error.to_string().contains("same guest path: /data/cache"));
2996 }
2997
2998 #[test]
2999 fn volume_mounts_reject_parent_components_before_normalizing() {
3000 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3001
3002 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3003
3004 assert!(error.to_string().contains("must not contain '..'"));
3005 }
3006
3007 #[test]
3008 fn disk_image_format_from_extension() {
3009 assert_eq!(
3010 DiskImageFormat::from_extension("qcow2"),
3011 Some(DiskImageFormat::Qcow2)
3012 );
3013 assert_eq!(
3014 DiskImageFormat::from_extension("raw"),
3015 Some(DiskImageFormat::Raw)
3016 );
3017 assert_eq!(
3018 DiskImageFormat::from_extension("vmdk"),
3019 Some(DiskImageFormat::Vmdk)
3020 );
3021 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3022 assert_eq!(DiskImageFormat::from_extension(""), None);
3023 }
3024
3025 #[test]
3026 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3027 let resources: SandboxResources =
3028 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3029
3030 assert_eq!(resources.cpus, 4);
3031 assert_eq!(resources.max_cpus, 4);
3032 assert_eq!(resources.memory_mib, 2048);
3033 assert_eq!(resources.max_memory_mib, 2048);
3034 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3035 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3036 assert_eq!(
3037 serde_json::to_value(resources).unwrap(),
3038 serde_json::json!({
3039 "cpus": 4,
3040 "memory_mib": 2048,
3041 "max_cpus": 4,
3042 "max_memory_mib": 2048
3043 })
3044 );
3045 }
3046
3047 #[test]
3048 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3049 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3050 assert!(inherited.get("cpu_placement").is_none());
3051
3052 for policy in [
3053 CpuPlacement::Auto,
3054 CpuPlacement::Spread,
3055 CpuPlacement::Compact,
3056 ] {
3057 let resources = SandboxResources {
3058 cpu_placement: policy,
3059 ..Default::default()
3060 };
3061 let json = serde_json::to_string(&resources).unwrap();
3062 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3063
3064 assert_eq!(decoded.cpu_placement, policy);
3065 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3066 }
3067 }
3068
3069 #[test]
3070 fn transparent_huge_page_policy_roundtrips_non_default() {
3071 let resources: SandboxResources = serde_json::from_str(
3072 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3073 )
3074 .unwrap();
3075
3076 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3077 assert_eq!(
3078 serde_json::to_value(resources).unwrap()["thp"],
3079 serde_json::json!("always")
3080 );
3081 assert_eq!(
3082 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3083 TransparentHugePagePolicy::Never
3084 );
3085 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3086 }
3087
3088 #[test]
3089 fn disk_image_format_display_roundtrip() {
3090 for format in [
3091 DiskImageFormat::Qcow2,
3092 DiskImageFormat::Raw,
3093 DiskImageFormat::Vmdk,
3094 ] {
3095 let rendered = format.to_string();
3096 let parsed: DiskImageFormat = rendered.parse().unwrap();
3097 assert_eq!(parsed, format);
3098 }
3099 }
3100
3101 #[test]
3102 fn disk_image_format_from_str_unknown() {
3103 assert!("ext4".parse::<DiskImageFormat>().is_err());
3104 }
3105
3106 #[test]
3107 fn log_source_effective_uses_default_user_program_sources() {
3108 assert_eq!(
3109 LogSource::effective(&[]),
3110 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3111 );
3112 }
3113
3114 #[test]
3115 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3116 assert_eq!(
3117 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3118 vec![LogSource::Stdout, LogSource::System]
3119 );
3120 }
3121
3122 #[test]
3123 fn rlimit_resource_parses_case_insensitively() {
3124 assert_eq!(
3125 RlimitResource::try_from("NOFILE").unwrap(),
3126 RlimitResource::Nofile
3127 );
3128 assert!(RlimitResource::try_from("bogus").is_err());
3129 }
3130
3131 #[test]
3132 fn sandbox_policy_serde_roundtrip() {
3133 let policy = SandboxPolicy {
3134 ephemeral: true,
3135 max_duration_secs: Some(3600),
3136 idle_timeout_secs: Some(120),
3137 };
3138
3139 let json = serde_json::to_string(&policy).unwrap();
3140 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3141
3142 assert!(decoded.ephemeral);
3143 assert_eq!(decoded.max_duration_secs, Some(3600));
3144 assert_eq!(decoded.idle_timeout_secs, Some(120));
3145 }
3146
3147 #[test]
3148 fn sandbox_policy_defaults_to_persistent() {
3149 assert!(!SandboxPolicy::default().ephemeral);
3150 }
3151
3152 #[test]
3153 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3154 let decoded: SandboxPolicy =
3157 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3158 assert!(!decoded.ephemeral);
3159 assert_eq!(decoded.max_duration_secs, Some(60));
3160 }
3161
3162 #[test]
3163 fn sandbox_spec_default_uses_static_resource_defaults() {
3164 let spec = SandboxSpec::default();
3165
3166 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3167 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3168 assert_eq!(
3169 spec.runtime.metrics_sample_interval_ms,
3170 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3171 );
3172 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3173 }
3174
3175 #[test]
3176 fn deployment_profile_uses_stable_snake_case_wire_values() {
3177 assert_eq!(
3178 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3179 r#""multi_tenant""#
3180 );
3181 assert_eq!(
3182 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3183 DeploymentProfile::SingleTenant
3184 );
3185 }
3186
3187 #[test]
3188 fn sandbox_log_level_roundtrips_lowercase_values() {
3189 for (input, expected) in [
3190 ("error", SandboxLogLevel::Error),
3191 ("warn", SandboxLogLevel::Warn),
3192 ("info", SandboxLogLevel::Info),
3193 ("debug", SandboxLogLevel::Debug),
3194 ("trace", SandboxLogLevel::Trace),
3195 ] {
3196 let parsed: SandboxLogLevel = input.parse().unwrap();
3197 assert_eq!(parsed, expected);
3198 assert_eq!(parsed.as_str(), input);
3199 }
3200 }
3201}