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
32pub const WELL_KNOWN_NAT64_PREFIX: &str = "64:ff9b::/96";
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
42#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
43pub enum DiskImageFormat {
44 Qcow2,
46 Raw,
48 Vmdk,
50}
51
52#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
55#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
56#[serde(rename_all = "kebab-case")]
57pub enum FlatClone {
58 #[default]
60 Auto,
61
62 Copy,
64
65 Reflink,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
72pub enum RootfsSource {
73 Bind {
75 #[cfg_attr(feature = "ts", ts(type = "string"))]
77 path: PathBuf,
78 #[serde(default)]
85 follow_root_symlinks: bool,
86 },
87
88 Oci(OciRootfsSource),
90
91 DiskImage {
93 #[cfg_attr(feature = "ts", ts(type = "string"))]
95 path: PathBuf,
96 format: DiskImageFormat,
98 fstype: Option<String>,
100 },
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
106#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
107pub struct OciRootfsSource {
108 pub reference: String,
110
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub root_disk: Option<RootDisk>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
123#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
124#[serde(tag = "kind", rename_all = "kebab-case")]
125pub enum RootDisk {
126 Managed {
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 size_mib: Option<u32>,
132 },
133
134 Tmpfs {
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 size_mib: Option<u32>,
140 },
141
142 DiskImage {
145 #[cfg_attr(feature = "ts", ts(type = "string"))]
147 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
148 path: PathBuf,
149 format: DiskImageFormat,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 fstype: Option<String>,
154 },
155
156 Flat {
161 #[serde(default, skip_serializing_if = "Option::is_none")]
164 size_mib: Option<u32>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 fstype: Option<String>,
168 #[serde(default, skip_serializing_if = "FlatClone::is_auto")]
170 clone: FlatClone,
171 },
172}
173
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
176#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
177#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
178pub enum PullPolicy {
179 #[default]
181 IfMissing,
182
183 Always,
185
186 Never,
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
198#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
199#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
200#[serde(rename_all = "lowercase")]
201pub enum StatVirtualization {
202 Strict,
204 Relaxed,
206 Off,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
215#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
216#[serde(rename_all = "lowercase")]
217pub enum HostPermissions {
218 Private,
220 Mirror,
222}
223
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
226#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
228#[serde(rename_all = "lowercase")]
229pub enum SecurityProfile {
230 #[default]
234 Default,
235
236 Restricted,
240}
241
242#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
248#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
249#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
250#[serde(rename_all = "snake_case")]
251pub enum DeploymentProfile {
252 #[default]
254 SingleTenant,
255
256 MultiTenant,
258}
259
260#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
262#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
263#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
264#[serde(default)]
265pub struct MountOptions {
266 pub readonly: bool,
270
271 pub noexec: bool,
275
276 pub nosuid: bool,
278
279 pub nodev: bool,
281
282 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub override_uid: Option<u32>,
291
292 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub override_gid: Option<u32>,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
301#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
302#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
303pub enum VolumeKind {
304 Directory,
306
307 Disk,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
314#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
315pub struct VolumeSpec {
316 pub name: String,
318
319 pub kind: VolumeKind,
321
322 pub quota_mib: Option<u32>,
324
325 pub capacity_mib: Option<u32>,
327
328 pub labels: Vec<(String, String)>,
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
335#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
336pub enum NamedVolumeMode {
337 Existing,
339
340 Create,
342
343 EnsureExists,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
350#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
351pub struct NamedVolumeCreate {
352 pub mode: NamedVolumeMode,
354
355 pub name: String,
357
358 pub kind: VolumeKind,
360
361 pub quota_mib: Option<u32>,
363
364 pub capacity_mib: Option<u32>,
366
367 pub labels: Vec<(String, String)>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
374#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
375#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
376pub enum OwnedVolumeStorage {
377 Directory {
379 quota_mib: Option<u32>,
381 },
382 Disk {
384 capacity_mib: u32,
386 },
387}
388
389#[derive(Clone)]
391#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
392#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
393#[cfg_attr(feature = "ts", ts(tag = "type"))]
394pub enum VolumeMount {
395 Owned {
397 guest: String,
399 storage: OwnedVolumeStorage,
401 options: MountOptions,
403 stat_virtualization: StatVirtualization,
405 host_permissions: HostPermissions,
407 },
408 Bind {
410 #[cfg_attr(feature = "ts", ts(type = "string"))]
412 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
413 host: PathBuf,
414 guest: String,
416 options: MountOptions,
418 stat_virtualization: StatVirtualization,
420 host_permissions: HostPermissions,
422 follow_root_symlinks: bool,
429 quota_mib: Option<u32>,
435 },
436
437 Named {
439 name: String,
441 guest: String,
443 create: Option<NamedVolumeCreate>,
447 options: MountOptions,
449 stat_virtualization: StatVirtualization,
451 host_permissions: HostPermissions,
453 follow_root_symlinks: bool,
458 },
459
460 Tmpfs {
462 guest: String,
464 size_mib: Option<u32>,
466 options: MountOptions,
468 },
469
470 DiskImage {
472 #[cfg_attr(feature = "ts", ts(type = "string"))]
474 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
475 host: PathBuf,
476 guest: String,
478 format: DiskImageFormat,
480 fstype: Option<String>,
482 options: MountOptions,
484 },
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
489#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
490#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
491pub enum Patch {
492 Text {
494 path: String,
496 content: String,
498 mode: Option<u32>,
500 replace: bool,
502 },
503
504 File {
506 path: String,
508 content: Vec<u8>,
510 mode: Option<u32>,
512 replace: bool,
514 },
515
516 CopyFile {
518 #[cfg_attr(feature = "ts", ts(type = "string"))]
520 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
521 src: PathBuf,
522 dst: String,
524 mode: Option<u32>,
526 replace: bool,
528 },
529
530 CopyDir {
532 #[cfg_attr(feature = "ts", ts(type = "string"))]
534 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
535 src: PathBuf,
536 dst: String,
538 replace: bool,
540 },
541
542 Symlink {
544 target: String,
546 link: String,
548 replace: bool,
550 },
551
552 Mkdir {
554 path: String,
556 mode: Option<u32>,
558 },
559
560 Remove {
562 path: String,
564 },
565
566 Append {
568 path: String,
570 content: String,
572 },
573}
574
575#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
581#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
582#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
583#[serde(default)]
584pub struct HttpConfig {
585 pub deny_response: bool,
587
588 #[serde(skip_serializing_if = "Option::is_none")]
592 pub deny_message: Option<String>,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
599#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
600#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
601#[serde(default)]
602pub struct NetworkSpec {
603 pub enabled: bool,
605
606 #[serde(skip_serializing_if = "Option::is_none")]
608 #[config_patch(nested)]
609 pub interface: Option<InterfaceOverrides>,
610
611 pub ports: Vec<PublishedPortSpec>,
613
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub policy: Option<NetworkPolicy>,
617
618 #[serde(skip_serializing_if = "Option::is_none")]
620 #[config_patch(nested)]
621 pub dns: Option<DnsConfig>,
622
623 #[serde(skip_serializing_if = "Option::is_none")]
625 #[config_patch(nested)]
626 pub tls: Option<TlsConfig>,
627
628 pub strict: bool,
630
631 #[serde(skip_serializing_if = "Option::is_none")]
633 #[config_patch(nested)]
634 pub secrets: Option<SecretsConfig>,
635
636 #[serde(rename = "max_connections", alias = "max_tcp_connections")]
639 pub max_tcp_connections: Option<usize>,
640
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub max_udp_connections: Option<usize>,
644
645 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub tcp_accept_queue_size: Option<u32>,
649
650 #[serde(skip_serializing_if = "Option::is_none")]
652 #[config_patch(nested)]
653 pub rate_limiter: Option<NetworkRateLimiterConfig>,
654
655 #[serde(default = "default_nat64_prefixes")]
657 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
658 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
659 pub nat64_prefixes: Vec<Ipv6Network>,
660
661 pub trust_host_cas: bool,
663
664 #[config_patch(nested)]
666 pub http: HttpConfig,
667
668 #[serde(skip_serializing_if = "Option::is_none")]
672 #[config_patch(nullable)]
673 pub outbound_proxy: Option<OutboundProxy>,
674}
675
676#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
679#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
680#[serde(tag = "protocol", rename_all = "lowercase")]
681#[non_exhaustive]
682pub enum OutboundProxy {
683 #[serde(rename = "http_connect")]
685 HttpConnect {
686 address: String,
688 },
689
690 Socks4 {
692 address: String,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
696 user_id: Option<String>,
697 },
698
699 Socks5 {
701 address: String,
703 #[serde(default, skip_serializing_if = "Option::is_none")]
705 credentials: Option<Socks5Credentials>,
706 },
707}
708
709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
715#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
716pub struct Socks5Credentials {
717 pub username: String,
719
720 pub password: SecretSource,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
726#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
727#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
728pub struct PublishedPortSpec {
729 pub host_port: u16,
731
732 pub guest_port: u16,
734
735 #[serde(default)]
737 pub protocol: PortProtocol,
738
739 pub host_bind: String,
741}
742
743#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
745#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
746#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
747pub enum PortProtocol {
748 #[default]
750 #[serde(rename = "tcp")]
751 Tcp,
752
753 #[serde(rename = "udp")]
755 Udp,
756}
757
758#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
764#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
765#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
766#[serde(default)]
767pub struct VsockSpec {
768 pub routes: Vec<VsockRouteSpec>,
770}
771
772impl VsockSpec {
773 pub fn is_empty(&self) -> bool {
775 self.routes.is_empty()
776 }
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
781#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
782#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
783pub struct VsockRouteSpec {
784 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
786 pub host_socket: PathBuf,
787
788 pub port: u32,
790
791 #[serde(default)]
793 pub socket_type: VsockSocketType,
794}
795
796#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
798#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
799#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
800#[serde(rename_all = "snake_case")]
801pub enum VsockSocketType {
802 #[default]
804 Stream,
805
806 Dgram,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
816#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
817#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
818pub struct HandoffInit {
819 pub cmd: String,
823
824 #[serde(default)]
826 pub args: Vec<String>,
827
828 #[serde(default)]
830 pub env: Vec<(String, String)>,
831}
832
833#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
839#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
840#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
841pub struct SandboxPolicy {
842 #[serde(default)]
851 pub ephemeral: bool,
852
853 pub max_duration_secs: Option<u64>,
855
856 pub idle_timeout_secs: Option<u64>,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
870#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
871pub struct SnapshotSpec {
872 #[serde(default)]
874 pub guest_flush: crate::GuestFlush,
875 pub name: String,
877
878 #[serde(default)]
880 pub group: Option<String>,
881
882 #[serde(default)]
884 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
885 pub dest_dir: Option<PathBuf>,
886
887 pub source_sandbox: String,
889
890 pub labels: Vec<(String, String)>,
892
893 pub force: bool,
895
896 pub record_integrity: bool,
898
899 #[serde(default)]
901 pub full: bool,
902}
903
904#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
912#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
913#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
914#[serde(default)]
915pub struct SandboxSpec {
916 pub name: String,
918
919 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
921 pub image: RootfsSource,
922
923 #[config_patch(nested)]
925 pub resources: SandboxResources,
926
927 #[config_patch(nested)]
929 pub runtime: SandboxRuntimeOptions,
930
931 #[config_patch(merge_with = merge_env_vars)]
933 pub env: Vec<EnvVar>,
934
935 #[config_patch(merge)]
937 pub labels: BTreeMap<String, String>,
938
939 pub rlimits: Vec<Rlimit>,
941
942 pub mounts: Vec<VolumeMount>,
944
945 pub patches: Vec<Patch>,
947
948 #[config_patch(nested)]
950 pub network: NetworkSpec,
951
952 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
954 #[config_patch(nested)]
955 pub vsock: VsockSpec,
956
957 pub init: Option<HandoffInit>,
959
960 pub pull_policy: PullPolicy,
962
963 pub security_profile: SecurityProfile,
965
966 pub deployment_profile: DeploymentProfile,
972
973 #[config_patch(nested)]
975 pub lifecycle: SandboxPolicy,
976}
977
978#[derive(Debug, Clone, Serialize, ConfigPatch)]
980#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
981#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
982pub struct SandboxResources {
983 pub cpus: u8,
985
986 pub memory_mib: u32,
988
989 pub max_cpus: u8,
991
992 pub max_memory_mib: u32,
994
995 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
997 pub cpu_placement: CpuPlacement,
998
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 #[config_patch(nullable)]
1003 pub placement_profile: Option<String>,
1004
1005 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
1007 pub thp: TransparentHugePagePolicy,
1008}
1009
1010#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1012#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1013#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1014#[serde(rename_all = "lowercase")]
1015pub enum CpuPlacement {
1016 #[default]
1018 Inherit,
1019
1020 Auto,
1022
1023 Spread,
1025
1026 Compact,
1028}
1029
1030#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1032#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1033#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1034#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1035pub enum NumaPlacement {
1036 PreferSingle,
1038 StrictSingle,
1040 Inherit,
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1046#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1047#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1048#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1049pub enum MemoryPlacement {
1050 FollowCpu,
1052 Inherit,
1054}
1055
1056#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1058#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1059#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1060#[serde(deny_unknown_fields)]
1061pub struct PlacementProfile {
1062 pub numa: NumaPlacement,
1064 pub memory: MemoryPlacement,
1066}
1067
1068#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1070#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1071#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1072#[serde(rename_all = "lowercase")]
1073pub enum TransparentHugePagePolicy {
1074 Always,
1076
1077 #[default]
1079 Madvise,
1080
1081 Never,
1083}
1084
1085#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1090#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1091#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1092#[serde(rename_all = "lowercase")]
1093pub enum GuestClockPolicy {
1094 #[default]
1099 Sync,
1100
1101 Off,
1106}
1107
1108#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1110#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1111#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1112#[serde(default)]
1113pub struct SandboxRuntimeOptions {
1114 #[config_patch(nullable)]
1117 pub workdir: Option<String>,
1118
1119 #[config_patch(nullable)]
1122 pub shell: Option<String>,
1123
1124 #[config_patch(merge)]
1126 pub scripts: BTreeMap<String, String>,
1127
1128 pub entrypoint: Option<Vec<String>>,
1130
1131 pub cmd: Option<Vec<String>>,
1133
1134 pub hostname: Option<String>,
1136
1137 pub user: Option<String>,
1139
1140 #[config_patch(nullable)]
1143 pub log_level: Option<SandboxLogLevel>,
1144
1145 #[config_patch(nullable)]
1148 pub metrics_sample_interval_ms: Option<u64>,
1149
1150 pub disable_metrics_sample: bool,
1152
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub guest_clock: Option<GuestClockPolicy>,
1157}
1158
1159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1162#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1163pub struct EnvVar {
1164 pub key: String,
1166
1167 pub value: String,
1169}
1170
1171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1173#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1174#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1175#[serde(rename_all = "lowercase")]
1176pub enum SandboxLogLevel {
1177 Error,
1179
1180 Warn,
1182
1183 Info,
1185
1186 Debug,
1188
1189 Trace,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1199#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1200#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1201pub enum RlimitResource {
1202 Cpu,
1204 Fsize,
1206 Data,
1208 Stack,
1210 Core,
1212 Rss,
1214 Nproc,
1216 Nofile,
1218 Memlock,
1220 As,
1222 Locks,
1224 Sigpending,
1226 Msgqueue,
1228 Nice,
1230 Rtprio,
1232 Rttime,
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize)]
1238#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1239#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1240pub struct Rlimit {
1241 pub resource: RlimitResource,
1243
1244 pub soft: u64,
1246
1247 pub hard: u64,
1249}
1250
1251#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1257#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1258#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1259#[serde(rename_all = "lowercase")]
1260pub enum LogSource {
1261 Stdout,
1263
1264 Stderr,
1266
1267 Output,
1269
1270 System,
1272}
1273
1274impl SandboxResourcesPatch {
1279 pub fn has_cpus(&self) -> bool {
1281 self.cpus.is_some()
1282 }
1283
1284 pub fn has_memory_mib(&self) -> bool {
1286 self.memory_mib.is_some()
1287 }
1288
1289 pub fn has_max_cpus(&self) -> bool {
1291 self.max_cpus.is_some()
1292 }
1293
1294 pub fn has_max_memory_mib(&self) -> bool {
1296 self.max_memory_mib.is_some()
1297 }
1298}
1299
1300impl DiskImageFormat {
1301 pub fn as_str(&self) -> &'static str {
1303 match self {
1304 Self::Qcow2 => "qcow2",
1305 Self::Raw => "raw",
1306 Self::Vmdk => "vmdk",
1307 }
1308 }
1309
1310 pub fn from_extension(ext: &str) -> Option<Self> {
1314 match ext {
1315 "qcow2" => Some(Self::Qcow2),
1316 "raw" => Some(Self::Raw),
1317 "vmdk" => Some(Self::Vmdk),
1318 _ => None,
1319 }
1320 }
1321}
1322
1323impl OciRootfsSource {
1324 pub fn new(reference: impl Into<String>) -> Self {
1326 Self {
1327 reference: reference.into(),
1328 root_disk: None,
1329 }
1330 }
1331}
1332
1333impl TransparentHugePagePolicy {
1334 pub fn is_madvise(&self) -> bool {
1336 matches!(self, Self::Madvise)
1337 }
1338
1339 pub fn as_str(self) -> &'static str {
1341 match self {
1342 Self::Always => "always",
1343 Self::Madvise => "madvise",
1344 Self::Never => "never",
1345 }
1346 }
1347}
1348
1349impl GuestClockPolicy {
1350 pub fn is_sync(&self) -> bool {
1352 matches!(self, Self::Sync)
1353 }
1354
1355 pub fn as_str(self) -> &'static str {
1357 match self {
1358 Self::Sync => "sync",
1359 Self::Off => "off",
1360 }
1361 }
1362}
1363
1364impl RootDisk {
1365 pub fn managed(size_mib: u32) -> Self {
1367 Self::Managed {
1368 size_mib: Some(size_mib),
1369 }
1370 }
1371
1372 pub fn tmpfs(size_mib: u32) -> Self {
1374 Self::Tmpfs {
1375 size_mib: Some(size_mib),
1376 }
1377 }
1378
1379 pub fn flat(size_mib: u32) -> Self {
1381 Self::Flat {
1382 size_mib: Some(size_mib),
1383 fstype: None,
1384 clone: FlatClone::Auto,
1385 }
1386 }
1387
1388 pub fn size_mib(&self) -> Option<u32> {
1390 match self {
1391 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1392 *size_mib
1393 }
1394 Self::DiskImage { .. } => None,
1395 }
1396 }
1397
1398 pub fn kind_str(&self) -> &'static str {
1400 match self {
1401 Self::Managed { .. } => "managed",
1402 Self::Tmpfs { .. } => "tmpfs",
1403 Self::DiskImage { .. } => "disk-image",
1404 Self::Flat { .. } => "flat",
1405 }
1406 }
1407
1408 pub fn is_managed(&self) -> bool {
1410 matches!(self, Self::Managed { .. })
1411 }
1412}
1413
1414impl FlatClone {
1415 pub const fn as_str(self) -> &'static str {
1417 match self {
1418 Self::Auto => "auto",
1419 Self::Copy => "copy",
1420 Self::Reflink => "reflink",
1421 }
1422 }
1423
1424 pub const fn is_auto(&self) -> bool {
1426 matches!(self, Self::Auto)
1427 }
1428}
1429
1430impl RootfsSource {
1431 pub fn oci(reference: impl Into<String>) -> Self {
1433 Self::Oci(OciRootfsSource::new(reference))
1434 }
1435
1436 pub fn oci_reference(&self) -> Option<&str> {
1438 match self {
1439 Self::Oci(oci) => Some(&oci.reference),
1440 _ => None,
1441 }
1442 }
1443
1444 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1446 match self {
1447 Self::Oci(oci) => oci.root_disk.as_ref(),
1448 _ => None,
1449 }
1450 }
1451
1452 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1455 match self {
1456 Self::Oci(oci) => match &oci.root_disk {
1457 Some(RootDisk::Managed { size_mib }) => *size_mib,
1458 Some(_) => None,
1459 None => None,
1460 },
1461 _ => None,
1462 }
1463 }
1464}
1465
1466impl EnvVar {
1467 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1469 Self {
1470 key: key.into(),
1471 value: value.into(),
1472 }
1473 }
1474
1475 pub fn as_pair(&self) -> (&str, &str) {
1477 (&self.key, &self.value)
1478 }
1479}
1480
1481impl VolumeKind {
1482 pub fn as_str(self) -> &'static str {
1484 match self {
1485 Self::Directory => "dir",
1486 Self::Disk => "disk",
1487 }
1488 }
1489
1490 pub fn from_db_value(value: &str) -> Self {
1492 match value {
1493 "disk" => Self::Disk,
1494 _ => Self::Directory,
1495 }
1496 }
1497}
1498
1499impl VolumeSpec {
1500 pub fn new(name: impl Into<String>) -> Self {
1502 Self {
1503 name: name.into(),
1504 kind: VolumeKind::Directory,
1505 quota_mib: None,
1506 capacity_mib: None,
1507 labels: Vec::new(),
1508 }
1509 }
1510}
1511
1512impl NamedVolumeCreate {
1513 pub fn mode(&self) -> NamedVolumeMode {
1515 self.mode
1516 }
1517
1518 pub fn name(&self) -> &str {
1520 &self.name
1521 }
1522
1523 pub fn kind(&self) -> VolumeKind {
1525 self.kind
1526 }
1527
1528 pub fn quota_mib(&self) -> Option<u32> {
1530 self.quota_mib
1531 }
1532
1533 pub fn capacity_mib(&self) -> Option<u32> {
1535 self.capacity_mib
1536 }
1537
1538 pub fn labels(&self) -> &[(String, String)] {
1540 &self.labels
1541 }
1542}
1543
1544impl VolumeMount {
1545 pub fn guest(&self) -> &str {
1547 match self {
1548 Self::Bind { guest, .. }
1549 | Self::Owned { guest, .. }
1550 | Self::Named { guest, .. }
1551 | Self::Tmpfs { guest, .. }
1552 | Self::DiskImage { guest, .. } => guest,
1553 }
1554 }
1555
1556 fn guest_mut(&mut self) -> &mut String {
1557 match self {
1558 Self::Bind { guest, .. }
1559 | Self::Owned { guest, .. }
1560 | Self::Named { guest, .. }
1561 | Self::Tmpfs { guest, .. }
1562 | Self::DiskImage { guest, .. } => guest,
1563 }
1564 }
1565
1566 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1568 match self {
1569 Self::Named { create, .. } => create.as_ref(),
1570 _ => None,
1571 }
1572 }
1573}
1574
1575pub fn owned_volume_mount_id(guest: &str) -> String {
1582 use std::fmt::Write as _;
1583 let slug: String = guest
1584 .trim_start_matches('/')
1585 .chars()
1586 .take(11)
1587 .map(|character| {
1588 if character.is_ascii_alphanumeric() || character == '-' {
1589 character
1590 } else {
1591 '_'
1592 }
1593 })
1594 .collect();
1595 let mut id = if slug.is_empty() {
1596 String::new()
1597 } else {
1598 format!("{slug}_")
1599 };
1600 for byte in Sha256::digest(guest.as_bytes()).iter().take(4) {
1601 let _ = write!(id, "{byte:02x}");
1602 }
1603 id
1604}
1605
1606pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1613 for mount in mounts.iter_mut() {
1614 let canonical = canonical_guest_mount_path(mount.guest())?;
1615 *mount.guest_mut() = canonical;
1616 }
1617
1618 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1619
1620 for pair in mounts.windows(2) {
1621 if pair[0].guest() == pair[1].guest() {
1622 return Err(TypesError::invalid_config(format!(
1623 "multiple volumes cannot mount the same guest path: {}",
1624 pair[0].guest()
1625 )));
1626 }
1627 }
1628
1629 Ok(())
1630}
1631
1632fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1633 let path = Utf8UnixPath::new(guest);
1634
1635 if !path.is_valid() {
1636 return Err(TypesError::invalid_config(format!(
1637 "guest mount path must be a valid Unix path: {guest}"
1638 )));
1639 }
1640 if !path.is_absolute() {
1641 return Err(TypesError::invalid_config(format!(
1642 "guest mount path must be absolute: {guest}"
1643 )));
1644 }
1645 if path
1646 .components()
1647 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1648 {
1649 return Err(TypesError::invalid_config(format!(
1650 "guest mount path must not contain '..': {guest}"
1651 )));
1652 }
1653 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1654 return Err(TypesError::invalid_config(format!(
1655 "guest mount path must not contain ':', ';', or ',': {guest}"
1656 )));
1657 }
1658
1659 let canonical = path.normalize().to_string();
1660 if canonical == "/" {
1661 return Err(TypesError::invalid_config(
1662 "cannot mount a volume at guest root /",
1663 ));
1664 }
1665
1666 Ok(canonical)
1667}
1668
1669fn guest_mount_order_key(guest: &str) -> (usize, String) {
1670 let path = Utf8UnixPath::new(guest);
1671 let depth = path.components().filter(Utf8Component::is_normal).count();
1672 (depth, guest.to_owned())
1673}
1674
1675impl RlimitResource {
1676 pub fn as_str(&self) -> &'static str {
1678 match self {
1679 Self::Cpu => "cpu",
1680 Self::Fsize => "fsize",
1681 Self::Data => "data",
1682 Self::Stack => "stack",
1683 Self::Core => "core",
1684 Self::Rss => "rss",
1685 Self::Nproc => "nproc",
1686 Self::Nofile => "nofile",
1687 Self::Memlock => "memlock",
1688 Self::As => "as",
1689 Self::Locks => "locks",
1690 Self::Sigpending => "sigpending",
1691 Self::Msgqueue => "msgqueue",
1692 Self::Nice => "nice",
1693 Self::Rtprio => "rtprio",
1694 Self::Rttime => "rttime",
1695 }
1696 }
1697}
1698
1699impl LogSource {
1700 pub fn effective(requested: &[Self]) -> Vec<Self> {
1702 if requested.is_empty() {
1703 vec![Self::Stdout, Self::Stderr, Self::Output]
1704 } else {
1705 let mut sources = requested.to_vec();
1706 sources.sort_by_key(|src| match src {
1707 Self::Stdout => 0,
1708 Self::Stderr => 1,
1709 Self::Output => 2,
1710 Self::System => 3,
1711 });
1712 sources.dedup();
1713 sources
1714 }
1715 }
1716}
1717
1718impl SandboxLogLevel {
1719 pub const fn as_str(self) -> &'static str {
1721 match self {
1722 Self::Error => "error",
1723 Self::Warn => "warn",
1724 Self::Info => "info",
1725 Self::Debug => "debug",
1726 Self::Trace => "trace",
1727 }
1728 }
1729}
1730
1731impl std::fmt::Display for DiskImageFormat {
1736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1737 f.write_str(self.as_str())
1738 }
1739}
1740
1741impl FromStr for DiskImageFormat {
1742 type Err = String;
1743
1744 fn from_str(s: &str) -> Result<Self, Self::Err> {
1745 match s {
1746 "qcow2" => Ok(Self::Qcow2),
1747 "raw" => Ok(Self::Raw),
1748 "vmdk" => Ok(Self::Vmdk),
1749 _ => Err(format!("unknown disk image format: {s}")),
1750 }
1751 }
1752}
1753
1754impl fmt::Display for TransparentHugePagePolicy {
1755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1756 f.write_str(self.as_str())
1757 }
1758}
1759
1760impl FromStr for TransparentHugePagePolicy {
1761 type Err = String;
1762
1763 fn from_str(value: &str) -> Result<Self, Self::Err> {
1764 match value {
1765 "always" => Ok(Self::Always),
1766 "madvise" => Ok(Self::Madvise),
1767 "never" => Ok(Self::Never),
1768 _ => Err(format!(
1769 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1770 )),
1771 }
1772 }
1773}
1774
1775impl fmt::Display for GuestClockPolicy {
1776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1777 f.write_str(self.as_str())
1778 }
1779}
1780
1781impl FromStr for GuestClockPolicy {
1782 type Err = String;
1783
1784 fn from_str(value: &str) -> Result<Self, Self::Err> {
1785 match value {
1786 "sync" => Ok(Self::Sync),
1787 "off" => Ok(Self::Off),
1788 _ => Err(format!(
1789 "unknown guest clock policy: {value}; expected sync or off"
1790 )),
1791 }
1792 }
1793}
1794
1795impl Default for RootfsSource {
1796 fn default() -> Self {
1797 Self::oci(String::new())
1798 }
1799}
1800
1801impl Default for SandboxResources {
1802 fn default() -> Self {
1803 Self {
1804 cpus: DEFAULT_SANDBOX_CPUS,
1805 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1806 max_cpus: DEFAULT_SANDBOX_CPUS,
1807 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1808 cpu_placement: CpuPlacement::Inherit,
1809 placement_profile: None,
1810 thp: TransparentHugePagePolicy::Madvise,
1811 }
1812 }
1813}
1814
1815impl<'de> Deserialize<'de> for SandboxResources {
1816 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1817 where
1818 D: serde::Deserializer<'de>,
1819 {
1820 #[derive(Deserialize)]
1821 struct RawResources {
1822 #[serde(default = "default_sandbox_cpus")]
1823 cpus: u8,
1824 #[serde(default = "default_sandbox_memory_mib")]
1825 memory_mib: u32,
1826 max_cpus: Option<u8>,
1827 max_memory_mib: Option<u32>,
1828 #[serde(default)]
1829 cpu_placement: CpuPlacement,
1830 #[serde(default)]
1831 placement_profile: Option<String>,
1832 #[serde(default)]
1833 thp: TransparentHugePagePolicy,
1834 }
1835
1836 let raw = RawResources::deserialize(deserializer)?;
1837 Ok(Self {
1838 cpus: raw.cpus,
1839 memory_mib: raw.memory_mib,
1840 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1844 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1845 cpu_placement: raw.cpu_placement,
1846 placement_profile: raw.placement_profile,
1847 thp: raw.thp,
1848 })
1849 }
1850}
1851
1852impl CpuPlacement {
1853 pub const fn is_inherit(&self) -> bool {
1855 matches!(self, Self::Inherit)
1856 }
1857}
1858
1859impl std::fmt::Display for CpuPlacement {
1860 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1861 f.write_str(match self {
1862 Self::Inherit => "inherit",
1863 Self::Auto => "auto",
1864 Self::Spread => "spread",
1865 Self::Compact => "compact",
1866 })
1867 }
1868}
1869
1870impl FromStr for CpuPlacement {
1871 type Err = String;
1872
1873 fn from_str(value: &str) -> Result<Self, Self::Err> {
1874 match value {
1875 "inherit" => Ok(Self::Inherit),
1876 "auto" => Ok(Self::Auto),
1877 "spread" => Ok(Self::Spread),
1878 "compact" => Ok(Self::Compact),
1879 _ => Err(format!(
1880 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1881 )),
1882 }
1883 }
1884}
1885
1886impl Default for SandboxRuntimeOptions {
1887 fn default() -> Self {
1888 Self {
1889 workdir: None,
1890 shell: None,
1891 scripts: BTreeMap::new(),
1892 entrypoint: None,
1893 cmd: None,
1894 hostname: None,
1895 user: None,
1896 log_level: None,
1897 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1898 disable_metrics_sample: false,
1899 guest_clock: None,
1900 }
1901 }
1902}
1903
1904impl Default for NetworkSpec {
1905 fn default() -> Self {
1906 Self {
1907 enabled: true,
1908 interface: None,
1909 ports: Vec::new(),
1910 policy: None,
1911 dns: None,
1912 tls: None,
1913 strict: true,
1914 secrets: None,
1915 max_tcp_connections: None,
1916 max_udp_connections: None,
1917 tcp_accept_queue_size: None,
1918 rate_limiter: None,
1919 nat64_prefixes: default_nat64_prefixes(),
1920 trust_host_cas: false,
1921 outbound_proxy: None,
1922 http: HttpConfig::default(),
1923 }
1924 }
1925}
1926
1927pub(crate) fn default_nat64_prefixes() -> Vec<Ipv6Network> {
1928 vec![
1929 WELL_KNOWN_NAT64_PREFIX
1930 .parse()
1931 .expect("well-known NAT64 prefix must be valid"),
1932 ]
1933}
1934
1935impl Default for PublishedPortSpec {
1936 fn default() -> Self {
1937 Self {
1938 host_port: 0,
1939 guest_port: 0,
1940 protocol: PortProtocol::Tcp,
1941 host_bind: "127.0.0.1".into(),
1942 }
1943 }
1944}
1945
1946impl From<(String, String)> for EnvVar {
1947 fn from((key, value): (String, String)) -> Self {
1948 Self { key, value }
1949 }
1950}
1951
1952impl From<EnvVar> for (String, String) {
1953 fn from(var: EnvVar) -> Self {
1954 (var.key, var.value)
1955 }
1956}
1957
1958impl FromStr for SandboxLogLevel {
1959 type Err = String;
1960
1961 fn from_str(s: &str) -> Result<Self, Self::Err> {
1962 match s {
1963 "error" => Ok(Self::Error),
1964 "warn" => Ok(Self::Warn),
1965 "info" => Ok(Self::Info),
1966 "debug" => Ok(Self::Debug),
1967 "trace" => Ok(Self::Trace),
1968 _ => Err(format!("unknown sandbox log level: {s}")),
1969 }
1970 }
1971}
1972
1973impl std::fmt::Display for SandboxLogLevel {
1974 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1975 formatter.write_str(self.as_str())
1976 }
1977}
1978
1979impl Serialize for VolumeMount {
1980 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1981 use serde::ser::SerializeMap;
1982
1983 match self {
1984 Self::Owned {
1985 guest,
1986 storage,
1987 options,
1988 stat_virtualization,
1989 host_permissions,
1990 } => {
1991 let mut map = serializer.serialize_map(Some(6))?;
1994 map.serialize_entry("type", "Owned")?;
1995 map.serialize_entry("guest", guest)?;
1996 map.serialize_entry("storage", storage)?;
1997 map.serialize_entry("options", options)?;
1998 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1999 map.serialize_entry("host_permissions", host_permissions)?;
2000 map.end()
2001 }
2002 Self::Bind {
2003 host,
2004 guest,
2005 options,
2006 stat_virtualization,
2007 host_permissions,
2008 follow_root_symlinks,
2009 quota_mib,
2010 } => {
2011 let mut map = serializer.serialize_map(Some(8))?;
2012 map.serialize_entry("type", "Bind")?;
2013 map.serialize_entry("host", host)?;
2014 map.serialize_entry("guest", guest)?;
2015 map.serialize_entry("options", options)?;
2016 map.serialize_entry("stat_virtualization", stat_virtualization)?;
2017 map.serialize_entry("host_permissions", host_permissions)?;
2018 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
2019 map.serialize_entry("quota_mib", quota_mib)?;
2020 map.end()
2021 }
2022 Self::Named {
2023 name,
2024 guest,
2025 create: _,
2026 options,
2027 stat_virtualization,
2028 host_permissions,
2029 follow_root_symlinks,
2030 } => {
2031 let mut map = serializer.serialize_map(Some(7))?;
2032 map.serialize_entry("type", "Named")?;
2033 map.serialize_entry("name", name)?;
2034 map.serialize_entry("guest", guest)?;
2035 map.serialize_entry("options", options)?;
2036 map.serialize_entry("stat_virtualization", stat_virtualization)?;
2037 map.serialize_entry("host_permissions", host_permissions)?;
2038 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
2039 map.end()
2040 }
2041 Self::Tmpfs {
2042 guest,
2043 size_mib,
2044 options,
2045 } => {
2046 let mut map = serializer.serialize_map(Some(4))?;
2047 map.serialize_entry("type", "Tmpfs")?;
2048 map.serialize_entry("guest", guest)?;
2049 map.serialize_entry("size_mib", size_mib)?;
2050 map.serialize_entry("options", options)?;
2051 map.end()
2052 }
2053 Self::DiskImage {
2054 host,
2055 guest,
2056 format,
2057 fstype,
2058 options,
2059 } => {
2060 let mut map = serializer.serialize_map(Some(6))?;
2061 map.serialize_entry("type", "DiskImage")?;
2062 map.serialize_entry("host", host)?;
2063 map.serialize_entry("guest", guest)?;
2064 map.serialize_entry("format", format)?;
2065 map.serialize_entry("fstype", fstype)?;
2066 map.serialize_entry("options", options)?;
2067 map.end()
2068 }
2069 }
2070 }
2071}
2072
2073impl<'de> Deserialize<'de> for VolumeMount {
2074 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2075 fn default_strict() -> StatVirtualization {
2076 StatVirtualization::Strict
2077 }
2078
2079 fn default_private() -> HostPermissions {
2080 HostPermissions::Private
2081 }
2082
2083 #[derive(Deserialize)]
2084 #[serde(tag = "type")]
2085 enum VolumeMountHelper {
2086 Owned {
2087 guest: String,
2088 storage: OwnedVolumeStorage,
2089 #[serde(default)]
2090 options: MountOptions,
2091 #[serde(default = "default_strict")]
2092 stat_virtualization: StatVirtualization,
2093 #[serde(default = "default_private")]
2094 host_permissions: HostPermissions,
2095 },
2096 Bind {
2097 host: PathBuf,
2098 guest: String,
2099 #[serde(default)]
2100 options: Option<MountOptions>,
2101 #[serde(default)]
2102 readonly: bool,
2103 #[serde(default = "default_strict")]
2104 stat_virtualization: StatVirtualization,
2105 #[serde(default = "default_private")]
2106 host_permissions: HostPermissions,
2107 #[serde(default)]
2108 follow_root_symlinks: bool,
2109 #[serde(default)]
2110 quota_mib: Option<u32>,
2111 },
2112 Named {
2113 name: String,
2114 guest: String,
2115 #[serde(default)]
2116 options: Option<MountOptions>,
2117 #[serde(default)]
2118 readonly: bool,
2119 #[serde(default = "default_strict")]
2120 stat_virtualization: StatVirtualization,
2121 #[serde(default = "default_private")]
2122 host_permissions: HostPermissions,
2123 #[serde(default)]
2124 follow_root_symlinks: bool,
2125 },
2126 Tmpfs {
2127 guest: String,
2128 #[serde(default)]
2129 size_mib: Option<u32>,
2130 #[serde(default)]
2131 options: Option<MountOptions>,
2132 #[serde(default)]
2133 readonly: bool,
2134 },
2135 DiskImage {
2136 host: PathBuf,
2137 guest: String,
2138 format: DiskImageFormat,
2139 #[serde(default)]
2140 fstype: Option<String>,
2141 #[serde(default)]
2142 options: Option<MountOptions>,
2143 #[serde(default)]
2144 readonly: bool,
2145 },
2146 }
2147
2148 let helper = VolumeMountHelper::deserialize(deserializer)?;
2149 Ok(match helper {
2150 VolumeMountHelper::Owned {
2151 guest,
2152 storage,
2153 options,
2154 stat_virtualization,
2155 host_permissions,
2156 } => Self::Owned {
2157 guest,
2158 storage,
2159 options,
2160 stat_virtualization,
2161 host_permissions,
2162 },
2163 VolumeMountHelper::Bind {
2164 host,
2165 guest,
2166 options,
2167 readonly,
2168 stat_virtualization,
2169 host_permissions,
2170 follow_root_symlinks,
2171 quota_mib,
2172 } => Self::Bind {
2173 host,
2174 guest,
2175 options: decode_mount_options(options, readonly),
2176 stat_virtualization,
2177 host_permissions,
2178 follow_root_symlinks,
2179 quota_mib,
2180 },
2181 VolumeMountHelper::Named {
2182 name,
2183 guest,
2184 options,
2185 readonly,
2186 stat_virtualization,
2187 host_permissions,
2188 follow_root_symlinks,
2189 } => Self::Named {
2190 name,
2191 guest,
2192 create: None,
2193 options: decode_mount_options(options, readonly),
2194 stat_virtualization,
2195 host_permissions,
2196 follow_root_symlinks,
2197 },
2198 VolumeMountHelper::Tmpfs {
2199 guest,
2200 size_mib,
2201 options,
2202 readonly,
2203 } => Self::Tmpfs {
2204 guest,
2205 size_mib,
2206 options: decode_mount_options(options, readonly),
2207 },
2208 VolumeMountHelper::DiskImage {
2209 host,
2210 guest,
2211 format,
2212 fstype,
2213 options,
2214 readonly,
2215 } => Self::DiskImage {
2216 host,
2217 guest,
2218 format,
2219 fstype,
2220 options: decode_mount_options(options, readonly),
2221 },
2222 })
2223 }
2224}
2225
2226impl fmt::Debug for VolumeMount {
2227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2228 match self {
2229 Self::Owned {
2230 guest,
2231 storage,
2232 options,
2233 stat_virtualization,
2234 host_permissions,
2235 } => f
2236 .debug_struct("Owned")
2237 .field("guest", guest)
2238 .field("storage", storage)
2239 .field("options", options)
2240 .field("stat_virtualization", stat_virtualization)
2241 .field("host_permissions", host_permissions)
2242 .finish(),
2243 Self::Bind {
2244 host,
2245 guest,
2246 options,
2247 stat_virtualization,
2248 host_permissions,
2249 follow_root_symlinks,
2250 quota_mib,
2251 } => f
2252 .debug_struct("Bind")
2253 .field("host", host)
2254 .field("guest", guest)
2255 .field("options", options)
2256 .field("stat_virtualization", stat_virtualization)
2257 .field("host_permissions", host_permissions)
2258 .field("follow_root_symlinks", follow_root_symlinks)
2259 .field("quota_mib", quota_mib)
2260 .finish(),
2261 Self::Named {
2262 name,
2263 guest,
2264 create,
2265 options,
2266 stat_virtualization,
2267 host_permissions,
2268 follow_root_symlinks,
2269 } => f
2270 .debug_struct("Named")
2271 .field("name", name)
2272 .field("guest", guest)
2273 .field("create", create)
2274 .field("options", options)
2275 .field("stat_virtualization", stat_virtualization)
2276 .field("host_permissions", host_permissions)
2277 .field("follow_root_symlinks", follow_root_symlinks)
2278 .finish(),
2279 Self::Tmpfs {
2280 guest,
2281 size_mib,
2282 options,
2283 } => f
2284 .debug_struct("Tmpfs")
2285 .field("guest", guest)
2286 .field("size_mib", size_mib)
2287 .field("options", options)
2288 .finish(),
2289 Self::DiskImage {
2290 host,
2291 guest,
2292 format,
2293 fstype,
2294 options,
2295 } => f
2296 .debug_struct("DiskImage")
2297 .field("host", host)
2298 .field("guest", guest)
2299 .field("format", format)
2300 .field("fstype", fstype)
2301 .field("options", options)
2302 .finish(),
2303 }
2304 }
2305}
2306
2307impl TryFrom<&str> for RlimitResource {
2309 type Error = String;
2310
2311 fn try_from(s: &str) -> Result<Self, Self::Error> {
2312 match s.to_ascii_lowercase().as_str() {
2313 "cpu" => Ok(Self::Cpu),
2314 "fsize" => Ok(Self::Fsize),
2315 "data" => Ok(Self::Data),
2316 "stack" => Ok(Self::Stack),
2317 "core" => Ok(Self::Core),
2318 "rss" => Ok(Self::Rss),
2319 "nproc" => Ok(Self::Nproc),
2320 "nofile" => Ok(Self::Nofile),
2321 "memlock" => Ok(Self::Memlock),
2322 "as" => Ok(Self::As),
2323 "locks" => Ok(Self::Locks),
2324 "sigpending" => Ok(Self::Sigpending),
2325 "msgqueue" => Ok(Self::Msgqueue),
2326 "nice" => Ok(Self::Nice),
2327 "rtprio" => Ok(Self::Rtprio),
2328 "rttime" => Ok(Self::Rttime),
2329 _ => Err(format!("unknown rlimit resource: {s}")),
2330 }
2331 }
2332}
2333
2334fn default_sandbox_cpus() -> u8 {
2339 DEFAULT_SANDBOX_CPUS
2340}
2341
2342fn default_sandbox_memory_mib() -> u32 {
2343 DEFAULT_SANDBOX_MEMORY_MIB
2344}
2345
2346fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2347 options.unwrap_or(MountOptions {
2348 readonly,
2349 ..MountOptions::default()
2350 })
2351}
2352
2353fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2354 for value in higher {
2355 match base.iter_mut().find(|current| current.key == value.key) {
2356 Some(current) => *current = value,
2357 None => base.push(value),
2358 }
2359 }
2360}
2361
2362fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2363 for value in higher {
2364 match base
2365 .iter_mut()
2366 .find(|current| current.env_var == value.env_var)
2367 {
2368 Some(current) => *current = value,
2369 None => base.push(value),
2370 }
2371 }
2372}
2373
2374pub(crate) fn default_strict() -> StatVirtualization {
2376 StatVirtualization::Strict
2377}
2378
2379pub(crate) fn default_private() -> HostPermissions {
2381 HostPermissions::Private
2382}
2383
2384pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2386
2387#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2398#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2399#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2400pub struct SecretsConfig {
2401 #[doc(hidden)]
2404 #[serde(default, skip_serializing_if = "Option::is_none")]
2405 #[cfg_attr(feature = "ts", ts(skip))]
2406 #[cfg_attr(feature = "utoipa", schema(ignore))]
2407 pub passthrough_hosts: Option<Vec<HostPattern>>,
2408
2409 #[serde(default)]
2411 #[config_patch(merge_with = merge_secret_entries)]
2412 pub secrets: Vec<SecretEntry>,
2413
2414 #[serde(default)]
2416 pub violation_action: SecretViolationAction,
2417}
2418
2419#[derive(Clone, Serialize, Deserialize)]
2424#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2425#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2426pub struct SecretEntry {
2427 pub env_var: String,
2433
2434 #[serde(default = "empty_secret_value")]
2443 #[cfg_attr(feature = "ts", ts(type = "string"))]
2444 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2445 pub value: Zeroizing<String>,
2446
2447 #[serde(default, skip_serializing_if = "Option::is_none")]
2451 pub source: Option<SecretSource>,
2452
2453 pub placeholder: String,
2458
2459 #[serde(default)]
2461 pub allowed_hosts: Vec<HostPattern>,
2462
2463 #[serde(default)]
2465 pub substitution: SecretSubstitution,
2466
2467 #[serde(default)]
2469 pub passthrough_hosts: Vec<HostPattern>,
2470
2471 #[serde(default, skip_serializing_if = "Option::is_none")]
2473 pub violation_action: Option<SecretViolationAction>,
2474
2475 #[serde(default = "default_true")]
2480 pub require_tls_identity: bool,
2481}
2482
2483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2485#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2486#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2487#[serde(rename_all = "kebab-case")]
2488pub enum HostPattern {
2489 #[serde(alias = "Exact")]
2491 Exact(String),
2492 #[serde(alias = "Wildcard")]
2494 Wildcard(String),
2495 #[serde(alias = "Any")]
2497 Any,
2498}
2499
2500#[derive(Debug, Clone, Serialize, Deserialize)]
2502#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2503#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2504pub struct SecretSubstitution {
2505 #[serde(default = "default_true")]
2507 pub headers: bool,
2508
2509 #[serde(default)]
2511 pub query: bool,
2512
2513 #[serde(default)]
2521 pub body: bool,
2522}
2523
2524#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2526#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2527#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2528#[serde(rename_all = "kebab-case")]
2529pub enum SecretViolationAction {
2530 #[serde(alias = "Block")]
2532 Block,
2533 #[default]
2535 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2536 BlockAndLog,
2537 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2539 BlockAndTerminate,
2540}
2541
2542#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2544pub enum SecretConfigError {
2545 #[error("secret #{secret_index}: env_var must not be empty")]
2547 EmptyEnvVar {
2548 secret_index: usize,
2550 },
2551
2552 #[error("secret #{secret_index}: env_var must not contain `=`")]
2554 EnvVarContainsEquals {
2555 secret_index: usize,
2557 },
2558
2559 #[error("secret #{secret_index}: env_var must not contain NUL")]
2561 EnvVarContainsNul {
2562 secret_index: usize,
2564 },
2565
2566 #[error("secret #{secret_index}: at least one allowed host is required")]
2568 MissingAllowedHosts {
2569 secret_index: usize,
2571 },
2572
2573 #[error("secret #{secret_index}: at least one substitution location is required")]
2575 MissingSubstitutionLocation {
2576 secret_index: usize,
2578 },
2579
2580 #[error("secret #{secret_index}: placeholder must not be empty")]
2582 EmptyPlaceholder {
2583 secret_index: usize,
2585 },
2586
2587 #[error(
2589 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2590 )]
2591 PlaceholderTooLong {
2592 secret_index: usize,
2594 actual_bytes: usize,
2596 max_bytes: usize,
2598 },
2599
2600 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2602 PlaceholderContainsNul {
2603 secret_index: usize,
2605 },
2606
2607 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2609 PlaceholderContainsLineBreak {
2610 secret_index: usize,
2612 },
2613}
2614
2615impl SecretsConfig {
2616 pub fn has_tls_identity_secrets(&self) -> bool {
2618 self.secrets
2619 .iter()
2620 .any(|secret| secret.require_tls_identity)
2621 }
2622
2623 pub fn contains_env_var(&self, env_var: &str) -> bool {
2625 self.secrets.iter().any(|secret| secret.env_var == env_var)
2626 }
2627
2628 pub fn validate(&self) -> Result<(), SecretConfigError> {
2630 for (index, secret) in self.secrets.iter().enumerate() {
2631 secret.validate(index)?;
2632 }
2633 Ok(())
2634 }
2635}
2636
2637impl SecretEntry {
2638 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2640 validate_env_var(&self.env_var, secret_index)?;
2641
2642 if self.allowed_hosts.is_empty() {
2643 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2644 }
2645
2646 if !self.substitution.headers && !self.substitution.query && !self.substitution.body {
2647 return Err(SecretConfigError::MissingSubstitutionLocation { secret_index });
2648 }
2649
2650 validate_placeholder(&self.placeholder, secret_index)
2651 }
2652}
2653
2654impl fmt::Debug for SecretEntry {
2656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2657 f.debug_struct("SecretEntry")
2658 .field("env_var", &self.env_var)
2659 .field("value", &"[REDACTED]")
2660 .field("source", &self.source)
2661 .field("placeholder", &self.placeholder)
2662 .field("allowed_hosts", &self.allowed_hosts)
2663 .field("substitution", &self.substitution)
2664 .field("passthrough_hosts", &self.passthrough_hosts)
2665 .field("violation_action", &self.violation_action)
2666 .field("require_tls_identity", &self.require_tls_identity)
2667 .finish()
2668 }
2669}
2670
2671impl HostPattern {
2672 pub fn parse(host: &str) -> Self {
2675 if host == "*" {
2676 HostPattern::Any
2677 } else if host.starts_with("*.") {
2678 HostPattern::Wildcard(host.to_string())
2679 } else {
2680 HostPattern::Exact(host.to_string())
2681 }
2682 }
2683
2684 pub fn matches(&self, hostname: &str) -> bool {
2689 match self {
2690 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2691 HostPattern::Wildcard(pattern) => {
2692 if let Some(suffix) = pattern.strip_prefix("*.") {
2693 hostname.eq_ignore_ascii_case(suffix)
2694 || (hostname.len() > suffix.len() + 1
2695 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2696 && hostname[hostname.len() - suffix.len()..]
2697 .eq_ignore_ascii_case(suffix))
2698 } else {
2699 hostname.eq_ignore_ascii_case(pattern)
2700 }
2701 }
2702 HostPattern::Any => true,
2703 }
2704 }
2705}
2706
2707impl Default for SecretSubstitution {
2708 fn default() -> Self {
2709 Self {
2710 headers: true,
2711 query: false,
2712 body: false,
2713 }
2714 }
2715}
2716
2717fn default_true() -> bool {
2718 true
2719}
2720
2721fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2722 if env_var.is_empty() {
2723 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2724 }
2725 if env_var.contains('=') {
2726 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2727 }
2728 if env_var.contains('\0') {
2729 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2730 }
2731 Ok(())
2732}
2733
2734fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2735 if placeholder.is_empty() {
2736 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2737 }
2738
2739 let actual_bytes = placeholder.len();
2740 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2741 return Err(SecretConfigError::PlaceholderTooLong {
2742 secret_index,
2743 actual_bytes,
2744 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2745 });
2746 }
2747
2748 if placeholder.contains('\0') {
2749 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2750 }
2751 if placeholder.contains('\r') || placeholder.contains('\n') {
2752 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2753 }
2754
2755 Ok(())
2756}
2757
2758#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2768#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2769#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2770pub struct TlsConfig {
2771 #[serde(default)]
2773 pub enabled: bool,
2774
2775 #[serde(default = "default_intercepted_ports")]
2777 pub intercepted_ports: Vec<u16>,
2778
2779 #[serde(default)]
2781 pub bypass: Vec<String>,
2782
2783 #[serde(default = "default_true")]
2785 pub verify_upstream: bool,
2786
2787 #[serde(default = "default_true")]
2790 pub block_quic_on_intercept: bool,
2791
2792 #[serde(default)]
2794 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2795 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2796 pub upstream_ca_cert: Vec<PathBuf>,
2797
2798 #[serde(default, alias = "scoped_upstream_ca_certs")]
2800 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2801
2802 #[serde(default)]
2804 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2805
2806 #[serde(default, alias = "ca")]
2809 pub intercept_ca: InterceptCaConfig,
2810
2811 #[serde(default)]
2813 pub cache: CertCacheConfig,
2814}
2815
2816#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2818#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2819#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2820pub struct InterceptCaConfig {
2821 #[serde(default)]
2824 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2825 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2826 pub cert_path: Option<PathBuf>,
2827
2828 #[serde(default)]
2831 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2832 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2833 pub key_path: Option<PathBuf>,
2834}
2835
2836#[derive(Debug, Clone, Serialize, Deserialize)]
2838#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2839#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2840pub struct CertCacheConfig {
2841 #[serde(default = "default_cache_capacity")]
2843 pub capacity: usize,
2844
2845 #[serde(default = "default_cert_validity_hours")]
2847 pub validity_hours: u64,
2848}
2849
2850#[derive(Debug, Clone, Serialize, Deserialize)]
2852#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2853#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2854pub struct ScopedUpstreamCaCert {
2855 pub pattern: String,
2857
2858 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2860 #[cfg_attr(feature = "ts", ts(type = "string"))]
2861 pub path: PathBuf,
2862}
2863
2864#[derive(Debug, Clone, Serialize, Deserialize)]
2866#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2867#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2868pub struct ScopedVerifyUpstream {
2869 pub pattern: String,
2871
2872 pub verify: bool,
2874}
2875
2876impl Default for TlsConfig {
2877 fn default() -> Self {
2878 Self {
2879 enabled: false,
2880 intercepted_ports: default_intercepted_ports(),
2881 bypass: Vec::new(),
2882 verify_upstream: true,
2883 block_quic_on_intercept: true,
2884 upstream_ca_cert: Vec::new(),
2885 scoped_upstream_ca_cert: Vec::new(),
2886 scoped_verify_upstream: Vec::new(),
2887 intercept_ca: InterceptCaConfig::default(),
2888 cache: CertCacheConfig::default(),
2889 }
2890 }
2891}
2892
2893impl Default for CertCacheConfig {
2894 fn default() -> Self {
2895 Self {
2896 capacity: default_cache_capacity(),
2897 validity_hours: default_cert_validity_hours(),
2898 }
2899 }
2900}
2901
2902fn default_intercepted_ports() -> Vec<u16> {
2903 vec![443]
2904}
2905
2906fn default_cache_capacity() -> usize {
2907 1000
2908}
2909
2910fn default_cert_validity_hours() -> u64 {
2911 24
2912}
2913
2914#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2920#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2921#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2922#[serde(rename_all = "snake_case")]
2923pub enum Action {
2924 Allow,
2926 Deny,
2928}
2929
2930#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2932#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2933#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2934#[serde(rename_all = "snake_case")]
2935pub enum Direction {
2936 Egress,
2938 Ingress,
2940 Any,
2942}
2943
2944#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2946#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2947#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2948#[serde(rename_all = "snake_case")]
2949pub enum Protocol {
2950 Tcp,
2952 Udp,
2954 Icmpv4,
2956 Icmpv6,
2958}
2959
2960#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2962#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2963#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2964#[serde(rename_all = "snake_case")]
2965pub enum DestinationGroup {
2966 Public,
2968 Loopback,
2970 Private,
2972 LinkLocal,
2974 Metadata,
2976 Multicast,
2978 Host,
2980}
2981
2982#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2989#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2990#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2991#[serde(rename_all = "snake_case")]
2992pub enum Destination {
2993 Any,
2995 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2997 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2998 Domain(String),
3000 DomainSuffix(String),
3002 Group(DestinationGroup),
3004}
3005
3006#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3008#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3009#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3010pub struct PortRange {
3011 pub start: u16,
3013 pub end: u16,
3015}
3016
3017#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3020#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3021#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3022pub struct Rule {
3023 pub direction: Direction,
3025 pub destination: Destination,
3027 #[serde(default)]
3029 pub protocols: Vec<Protocol>,
3030 #[serde(default)]
3032 pub ports: Vec<PortRange>,
3033 pub action: Action,
3035}
3036
3037#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3040#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3041#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3042pub struct NetworkPolicy {
3043 #[serde(default = "action_deny")]
3045 pub default_egress: Action,
3046 #[serde(default = "action_deny")]
3048 pub default_ingress: Action,
3049 #[serde(default)]
3051 pub rules: Vec<Rule>,
3052}
3053
3054fn action_deny() -> Action {
3057 Action::Deny
3058}
3059
3060#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3066#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3067#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3068#[serde(default)]
3069pub struct DnsConfig {
3070 pub rebind_protection: bool,
3072 pub nameservers: Vec<String>,
3075 pub query_timeout_ms: u64,
3077}
3078
3079impl Default for DnsConfig {
3080 fn default() -> Self {
3081 Self {
3082 rebind_protection: true,
3083 nameservers: Vec::new(),
3084 query_timeout_ms: 5000,
3085 }
3086 }
3087}
3088
3089#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3093#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3094#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3095#[serde(default)]
3096pub struct InterfaceOverrides {
3097 #[serde(skip_serializing_if = "Option::is_none")]
3099 pub mac: Option<[u8; 6]>,
3100 #[serde(skip_serializing_if = "Option::is_none")]
3102 pub mtu: Option<u16>,
3103 #[serde(skip_serializing_if = "Option::is_none")]
3105 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3106 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3107 pub ipv4_address: Option<Ipv4Addr>,
3108 #[serde(skip_serializing_if = "Option::is_none")]
3110 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3111 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3112 pub ipv4_pool: Option<Ipv4Network>,
3113 #[serde(skip_serializing_if = "Option::is_none")]
3115 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3116 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3117 pub ipv6_address: Option<Ipv6Addr>,
3118 #[serde(skip_serializing_if = "Option::is_none")]
3120 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3121 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3122 pub ipv6_pool: Option<Ipv6Network>,
3123}
3124
3125fn empty_secret_value() -> Zeroizing<String> {
3126 Zeroizing::new(String::new())
3127}
3128
3129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3135pub enum NetworkRateLimitDirection {
3136 Egress,
3138 Ingress,
3140}
3141
3142#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3144#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3145#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3146#[serde(default)]
3147pub struct NetworkRateLimiterConfig {
3148 #[serde(skip_serializing_if = "Option::is_none")]
3150 pub egress: Option<RateLimiterConfig>,
3151
3152 #[serde(skip_serializing_if = "Option::is_none")]
3154 pub ingress: Option<RateLimiterConfig>,
3155}
3156
3157#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3163#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3164#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3165#[serde(default)]
3166pub struct RateLimiterConfig {
3167 #[serde(skip_serializing_if = "Option::is_none")]
3169 pub bandwidth: Option<TokenBucketConfig>,
3170
3171 #[serde(skip_serializing_if = "Option::is_none")]
3173 pub ops: Option<TokenBucketConfig>,
3174}
3175
3176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3182#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3183#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3184pub struct TokenBucketConfig {
3185 pub size: u64,
3187
3188 pub refill_time_ms: u64,
3191
3192 #[serde(default)]
3194 pub one_time_burst: u64,
3195}
3196
3197#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3199pub enum RateLimitConfigError {
3200 #[error("rate limiter must configure at least one of bandwidth or ops")]
3202 EmptyLimiter,
3203
3204 #[error("{bucket} bucket: size must be greater than zero")]
3206 ZeroSize {
3207 bucket: &'static str,
3209 },
3210
3211 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
3213 ZeroRefillTime {
3214 bucket: &'static str,
3216 },
3217}
3218
3219impl RateLimiterConfig {
3220 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
3222 if self.bandwidth.is_none() && self.ops.is_none() {
3223 return Err(RateLimitConfigError::EmptyLimiter);
3224 }
3225 if let Some(bandwidth) = &self.bandwidth {
3226 bandwidth.validate("bandwidth")?;
3227 }
3228 if let Some(ops) = &self.ops {
3229 ops.validate("ops")?;
3230 }
3231 Ok(())
3232 }
3233}
3234
3235impl TokenBucketConfig {
3236 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
3238 if self.size == 0 {
3239 return Err(RateLimitConfigError::ZeroSize { bucket });
3240 }
3241 if self.refill_time_ms == 0 {
3242 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
3243 }
3244 Ok(())
3245 }
3246}
3247
3248impl fmt::Display for NetworkRateLimitDirection {
3249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3250 match self {
3251 Self::Egress => f.write_str("egress"),
3252 Self::Ingress => f.write_str("ingress"),
3253 }
3254 }
3255}
3256
3257#[cfg(test)]
3262mod tests {
3263 use super::*;
3264
3265 fn secret_entry(env_var: &str, require_tls_identity: bool) -> SecretEntry {
3266 SecretEntry {
3267 env_var: env_var.to_owned(),
3268 value: Zeroizing::new("secret".to_owned()),
3269 source: None,
3270 placeholder: format!("$MSB_{env_var}"),
3271 allowed_hosts: vec![HostPattern::Any],
3272 substitution: SecretSubstitution::default(),
3273 passthrough_hosts: Vec::new(),
3274 violation_action: None,
3275 require_tls_identity,
3276 }
3277 }
3278
3279 fn tmpfs_mount(guest: &str) -> VolumeMount {
3280 VolumeMount::Tmpfs {
3281 guest: guest.to_owned(),
3282 size_mib: None,
3283 options: MountOptions::default(),
3284 }
3285 }
3286
3287 #[test]
3288 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
3289 let value = serde_json::to_value(MountOptions::default()).unwrap();
3290 assert!(value.get("override_uid").is_none());
3291 assert!(value.get("override_gid").is_none());
3292
3293 let decoded: MountOptions = serde_json::from_value(value).unwrap();
3294 assert_eq!(decoded.override_uid, None);
3295 assert_eq!(decoded.override_gid, None);
3296 }
3297
3298 #[test]
3299 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
3300 let mut mounts = vec![
3301 tmpfs_mount("/workspace//persist/./logs/"),
3302 tmpfs_mount("/alpha/z"),
3303 tmpfs_mount("/workspace"),
3304 ];
3305
3306 canonicalize_volume_mounts(&mut mounts).unwrap();
3307
3308 assert_eq!(
3309 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
3310 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
3311 );
3312 }
3313
3314 #[test]
3315 fn secrets_config_queries_entries() {
3316 let mut config = SecretsConfig {
3317 secrets: vec![secret_entry("HTTP_TOKEN", false)],
3318 ..Default::default()
3319 };
3320
3321 assert!(!config.has_tls_identity_secrets());
3322 assert!(config.contains_env_var("HTTP_TOKEN"));
3323 assert!(!config.contains_env_var("MISSING"));
3324
3325 config.secrets.push(secret_entry("API_KEY", true));
3326 assert!(config.has_tls_identity_secrets());
3327 }
3328
3329 #[test]
3330 fn volume_mounts_reject_duplicate_canonical_paths() {
3331 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
3332
3333 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3334
3335 assert!(error.to_string().contains("same guest path: /data/cache"));
3336 }
3337
3338 #[test]
3339 fn volume_mounts_reject_parent_components_before_normalizing() {
3340 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3341
3342 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3343
3344 assert!(error.to_string().contains("must not contain '..'"));
3345 }
3346
3347 #[test]
3348 fn disk_image_format_from_extension() {
3349 assert_eq!(
3350 DiskImageFormat::from_extension("qcow2"),
3351 Some(DiskImageFormat::Qcow2)
3352 );
3353 assert_eq!(
3354 DiskImageFormat::from_extension("raw"),
3355 Some(DiskImageFormat::Raw)
3356 );
3357 assert_eq!(
3358 DiskImageFormat::from_extension("vmdk"),
3359 Some(DiskImageFormat::Vmdk)
3360 );
3361 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3362 assert_eq!(DiskImageFormat::from_extension(""), None);
3363 }
3364
3365 #[test]
3366 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3367 let resources: SandboxResources =
3368 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3369
3370 assert_eq!(resources.cpus, 4);
3371 assert_eq!(resources.max_cpus, 4);
3372 assert_eq!(resources.memory_mib, 2048);
3373 assert_eq!(resources.max_memory_mib, 2048);
3374 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3375 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3376 assert_eq!(
3377 serde_json::to_value(resources).unwrap(),
3378 serde_json::json!({
3379 "cpus": 4,
3380 "memory_mib": 2048,
3381 "max_cpus": 4,
3382 "max_memory_mib": 2048
3383 })
3384 );
3385 }
3386
3387 #[test]
3388 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3389 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3390 assert!(inherited.get("cpu_placement").is_none());
3391
3392 for policy in [
3393 CpuPlacement::Auto,
3394 CpuPlacement::Spread,
3395 CpuPlacement::Compact,
3396 ] {
3397 let resources = SandboxResources {
3398 cpu_placement: policy,
3399 ..Default::default()
3400 };
3401 let json = serde_json::to_string(&resources).unwrap();
3402 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3403
3404 assert_eq!(decoded.cpu_placement, policy);
3405 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3406 }
3407 }
3408
3409 #[test]
3410 fn guest_clock_policy_is_omitted_until_set_and_roundtrips() {
3411 let defaults = serde_json::to_value(SandboxRuntimeOptions::default()).unwrap();
3412 assert!(defaults.get("guest_clock").is_none());
3413
3414 let legacy: SandboxRuntimeOptions = serde_json::from_str(r#"{"workdir":"/app"}"#).unwrap();
3415 assert_eq!(legacy.guest_clock, None);
3416
3417 for policy in [GuestClockPolicy::Sync, GuestClockPolicy::Off] {
3418 let runtime = SandboxRuntimeOptions {
3419 guest_clock: Some(policy),
3420 ..Default::default()
3421 };
3422 let json = serde_json::to_value(&runtime).unwrap();
3423 assert_eq!(json["guest_clock"], serde_json::json!(policy.as_str()));
3424 let decoded: SandboxRuntimeOptions = serde_json::from_value(json).unwrap();
3425 assert_eq!(decoded.guest_clock, Some(policy));
3426 assert_eq!(
3427 policy.to_string().parse::<GuestClockPolicy>().unwrap(),
3428 policy
3429 );
3430 }
3431
3432 assert_eq!(GuestClockPolicy::default(), GuestClockPolicy::Sync);
3433 assert!("host_sync".parse::<GuestClockPolicy>().is_err());
3434 }
3435
3436 #[test]
3437 fn transparent_huge_page_policy_roundtrips_non_default() {
3438 let resources: SandboxResources = serde_json::from_str(
3439 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3440 )
3441 .unwrap();
3442
3443 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3444 assert_eq!(
3445 serde_json::to_value(resources).unwrap()["thp"],
3446 serde_json::json!("always")
3447 );
3448 assert_eq!(
3449 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3450 TransparentHugePagePolicy::Never
3451 );
3452 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3453 }
3454
3455 #[test]
3456 fn disk_image_format_display_roundtrip() {
3457 for format in [
3458 DiskImageFormat::Qcow2,
3459 DiskImageFormat::Raw,
3460 DiskImageFormat::Vmdk,
3461 ] {
3462 let rendered = format.to_string();
3463 let parsed: DiskImageFormat = rendered.parse().unwrap();
3464 assert_eq!(parsed, format);
3465 }
3466 }
3467
3468 #[test]
3469 fn disk_image_format_from_str_unknown() {
3470 assert!("ext4".parse::<DiskImageFormat>().is_err());
3471 }
3472
3473 #[test]
3474 fn log_source_effective_uses_default_user_program_sources() {
3475 assert_eq!(
3476 LogSource::effective(&[]),
3477 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3478 );
3479 }
3480
3481 #[test]
3482 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3483 assert_eq!(
3484 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3485 vec![LogSource::Stdout, LogSource::System]
3486 );
3487 }
3488
3489 #[test]
3490 fn rlimit_resource_parses_case_insensitively() {
3491 assert_eq!(
3492 RlimitResource::try_from("NOFILE").unwrap(),
3493 RlimitResource::Nofile
3494 );
3495 assert!(RlimitResource::try_from("bogus").is_err());
3496 }
3497
3498 #[test]
3499 fn sandbox_policy_serde_roundtrip() {
3500 let policy = SandboxPolicy {
3501 ephemeral: true,
3502 max_duration_secs: Some(3600),
3503 idle_timeout_secs: Some(120),
3504 };
3505
3506 let json = serde_json::to_string(&policy).unwrap();
3507 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3508
3509 assert!(decoded.ephemeral);
3510 assert_eq!(decoded.max_duration_secs, Some(3600));
3511 assert_eq!(decoded.idle_timeout_secs, Some(120));
3512 }
3513
3514 #[test]
3515 fn sandbox_policy_defaults_to_persistent() {
3516 assert!(!SandboxPolicy::default().ephemeral);
3517 }
3518
3519 #[test]
3520 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3521 let decoded: SandboxPolicy =
3524 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3525 assert!(!decoded.ephemeral);
3526 assert_eq!(decoded.max_duration_secs, Some(60));
3527 }
3528
3529 #[test]
3530 fn sandbox_spec_default_uses_static_resource_defaults() {
3531 let spec = SandboxSpec::default();
3532
3533 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3534 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3535 assert_eq!(
3536 spec.runtime.metrics_sample_interval_ms,
3537 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3538 );
3539 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3540 }
3541
3542 #[test]
3543 fn deployment_profile_uses_stable_snake_case_wire_values() {
3544 assert_eq!(
3545 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3546 r#""multi_tenant""#
3547 );
3548 assert_eq!(
3549 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3550 DeploymentProfile::SingleTenant
3551 );
3552 }
3553
3554 #[test]
3555 fn sandbox_log_level_roundtrips_lowercase_values() {
3556 for (input, expected) in [
3557 ("error", SandboxLogLevel::Error),
3558 ("warn", SandboxLogLevel::Warn),
3559 ("info", SandboxLogLevel::Info),
3560 ("debug", SandboxLogLevel::Debug),
3561 ("trace", SandboxLogLevel::Trace),
3562 ] {
3563 let parsed: SandboxLogLevel = input.parse().unwrap();
3564 assert_eq!(parsed, expected);
3565 assert_eq!(parsed.as_str(), input);
3566 }
3567 }
3568}