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 pub nat64_prefixes: Vec<Ipv6Network>,
659
660 pub trust_host_cas: bool,
662
663 #[config_patch(nested)]
665 pub http: HttpConfig,
666
667 #[serde(skip_serializing_if = "Option::is_none")]
671 #[config_patch(nullable)]
672 pub outbound_proxy: Option<OutboundProxy>,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
677#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
678#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
679#[serde(tag = "protocol", rename_all = "lowercase")]
680#[non_exhaustive]
681pub enum OutboundProxy {
682 Socks4 {
684 address: String,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
688 user_id: Option<String>,
689 },
690
691 Socks5 {
693 address: String,
695 #[serde(default, skip_serializing_if = "Option::is_none")]
697 credentials: Option<Socks5Credentials>,
698 },
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
706#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
707#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
708pub struct Socks5Credentials {
709 pub username: String,
711
712 pub password: SecretSource,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
719#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
720pub struct PublishedPortSpec {
721 pub host_port: u16,
723
724 pub guest_port: u16,
726
727 #[serde(default)]
729 pub protocol: PortProtocol,
730
731 pub host_bind: String,
733}
734
735#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
737#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
738#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
739pub enum PortProtocol {
740 #[default]
742 #[serde(rename = "tcp")]
743 Tcp,
744
745 #[serde(rename = "udp")]
747 Udp,
748}
749
750#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
756#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
757#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
758#[serde(default)]
759pub struct VsockSpec {
760 pub routes: Vec<VsockRouteSpec>,
762}
763
764impl VsockSpec {
765 pub fn is_empty(&self) -> bool {
767 self.routes.is_empty()
768 }
769}
770
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
774#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
775pub struct VsockRouteSpec {
776 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
778 pub host_socket: PathBuf,
779
780 pub port: u32,
782
783 #[serde(default)]
785 pub socket_type: VsockSocketType,
786}
787
788#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
790#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
791#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
792#[serde(rename_all = "snake_case")]
793pub enum VsockSocketType {
794 #[default]
796 Stream,
797
798 Dgram,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize)]
808#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
809#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
810pub struct HandoffInit {
811 pub cmd: String,
815
816 #[serde(default)]
818 pub args: Vec<String>,
819
820 #[serde(default)]
822 pub env: Vec<(String, String)>,
823}
824
825#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
831#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
832#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
833pub struct SandboxPolicy {
834 #[serde(default)]
843 pub ephemeral: bool,
844
845 pub max_duration_secs: Option<u64>,
847
848 pub idle_timeout_secs: Option<u64>,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
862#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
863pub struct SnapshotSpec {
864 #[serde(default)]
866 pub guest_flush: crate::GuestFlush,
867 pub name: String,
869
870 #[serde(default)]
872 pub group: Option<String>,
873
874 #[serde(default)]
876 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
877 pub dest_dir: Option<PathBuf>,
878
879 pub source_sandbox: String,
881
882 pub labels: Vec<(String, String)>,
884
885 pub force: bool,
887
888 pub record_integrity: bool,
890
891 #[serde(default)]
893 pub full: bool,
894}
895
896#[derive(Debug, Default, Clone, Serialize, Deserialize, ConfigPatch)]
904#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
905#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
906#[serde(default)]
907pub struct SandboxSpec {
908 pub name: String,
910
911 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
913 pub image: RootfsSource,
914
915 #[config_patch(nested)]
917 pub resources: SandboxResources,
918
919 #[config_patch(nested)]
921 pub runtime: SandboxRuntimeOptions,
922
923 #[config_patch(merge_with = merge_env_vars)]
925 pub env: Vec<EnvVar>,
926
927 #[config_patch(merge)]
929 pub labels: BTreeMap<String, String>,
930
931 pub rlimits: Vec<Rlimit>,
933
934 pub mounts: Vec<VolumeMount>,
936
937 pub patches: Vec<Patch>,
939
940 #[config_patch(nested)]
942 pub network: NetworkSpec,
943
944 #[serde(default, skip_serializing_if = "VsockSpec::is_empty")]
946 #[config_patch(nested)]
947 pub vsock: VsockSpec,
948
949 pub init: Option<HandoffInit>,
951
952 pub pull_policy: PullPolicy,
954
955 pub security_profile: SecurityProfile,
957
958 pub deployment_profile: DeploymentProfile,
964
965 #[config_patch(nested)]
967 pub lifecycle: SandboxPolicy,
968}
969
970#[derive(Debug, Clone, Serialize, ConfigPatch)]
972#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
973#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
974pub struct SandboxResources {
975 pub cpus: u8,
977
978 pub memory_mib: u32,
980
981 pub max_cpus: u8,
983
984 pub max_memory_mib: u32,
986
987 #[serde(default, skip_serializing_if = "CpuPlacement::is_inherit")]
989 pub cpu_placement: CpuPlacement,
990
991 #[serde(default, skip_serializing_if = "Option::is_none")]
994 #[config_patch(nullable)]
995 pub placement_profile: Option<String>,
996
997 #[serde(default, skip_serializing_if = "TransparentHugePagePolicy::is_madvise")]
999 pub thp: TransparentHugePagePolicy,
1000}
1001
1002#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1004#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1005#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1006#[serde(rename_all = "lowercase")]
1007pub enum CpuPlacement {
1008 #[default]
1010 Inherit,
1011
1012 Auto,
1014
1015 Spread,
1017
1018 Compact,
1020}
1021
1022#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1024#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1025#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1026#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1027pub enum NumaPlacement {
1028 PreferSingle,
1030 StrictSingle,
1032 Inherit,
1034}
1035
1036#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1038#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1039#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1040#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
1041pub enum MemoryPlacement {
1042 FollowCpu,
1044 Inherit,
1046}
1047
1048#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1050#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1051#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1052#[serde(deny_unknown_fields)]
1053pub struct PlacementProfile {
1054 pub numa: NumaPlacement,
1056 pub memory: MemoryPlacement,
1058}
1059
1060#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1062#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1063#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1064#[serde(rename_all = "lowercase")]
1065pub enum TransparentHugePagePolicy {
1066 Always,
1068
1069 #[default]
1071 Madvise,
1072
1073 Never,
1075}
1076
1077#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
1079#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1080#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1081#[serde(default)]
1082pub struct SandboxRuntimeOptions {
1083 #[config_patch(nullable)]
1086 pub workdir: Option<String>,
1087
1088 #[config_patch(nullable)]
1091 pub shell: Option<String>,
1092
1093 #[config_patch(merge)]
1095 pub scripts: BTreeMap<String, String>,
1096
1097 pub entrypoint: Option<Vec<String>>,
1099
1100 pub cmd: Option<Vec<String>>,
1102
1103 pub hostname: Option<String>,
1105
1106 pub user: Option<String>,
1108
1109 #[config_patch(nullable)]
1112 pub log_level: Option<SandboxLogLevel>,
1113
1114 #[config_patch(nullable)]
1117 pub metrics_sample_interval_ms: Option<u64>,
1118
1119 pub disable_metrics_sample: bool,
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1125#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1126#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1127pub struct EnvVar {
1128 pub key: String,
1130
1131 pub value: String,
1133}
1134
1135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1137#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1138#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1139#[serde(rename_all = "lowercase")]
1140pub enum SandboxLogLevel {
1141 Error,
1143
1144 Warn,
1146
1147 Info,
1149
1150 Debug,
1152
1153 Trace,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1163#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1164#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1165pub enum RlimitResource {
1166 Cpu,
1168 Fsize,
1170 Data,
1172 Stack,
1174 Core,
1176 Rss,
1178 Nproc,
1180 Nofile,
1182 Memlock,
1184 As,
1186 Locks,
1188 Sigpending,
1190 Msgqueue,
1192 Nice,
1194 Rtprio,
1196 Rttime,
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1202#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1203#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1204pub struct Rlimit {
1205 pub resource: RlimitResource,
1207
1208 pub soft: u64,
1210
1211 pub hard: u64,
1213}
1214
1215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1221#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1222#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1223#[serde(rename_all = "lowercase")]
1224pub enum LogSource {
1225 Stdout,
1227
1228 Stderr,
1230
1231 Output,
1233
1234 System,
1236}
1237
1238impl SandboxResourcesPatch {
1243 pub fn has_cpus(&self) -> bool {
1245 self.cpus.is_some()
1246 }
1247
1248 pub fn has_memory_mib(&self) -> bool {
1250 self.memory_mib.is_some()
1251 }
1252
1253 pub fn has_max_cpus(&self) -> bool {
1255 self.max_cpus.is_some()
1256 }
1257
1258 pub fn has_max_memory_mib(&self) -> bool {
1260 self.max_memory_mib.is_some()
1261 }
1262}
1263
1264impl DiskImageFormat {
1265 pub fn as_str(&self) -> &'static str {
1267 match self {
1268 Self::Qcow2 => "qcow2",
1269 Self::Raw => "raw",
1270 Self::Vmdk => "vmdk",
1271 }
1272 }
1273
1274 pub fn from_extension(ext: &str) -> Option<Self> {
1278 match ext {
1279 "qcow2" => Some(Self::Qcow2),
1280 "raw" => Some(Self::Raw),
1281 "vmdk" => Some(Self::Vmdk),
1282 _ => None,
1283 }
1284 }
1285}
1286
1287impl OciRootfsSource {
1288 pub fn new(reference: impl Into<String>) -> Self {
1290 Self {
1291 reference: reference.into(),
1292 root_disk: None,
1293 }
1294 }
1295}
1296
1297impl TransparentHugePagePolicy {
1298 pub fn is_madvise(&self) -> bool {
1300 matches!(self, Self::Madvise)
1301 }
1302
1303 pub fn as_str(self) -> &'static str {
1305 match self {
1306 Self::Always => "always",
1307 Self::Madvise => "madvise",
1308 Self::Never => "never",
1309 }
1310 }
1311}
1312
1313impl RootDisk {
1314 pub fn managed(size_mib: u32) -> Self {
1316 Self::Managed {
1317 size_mib: Some(size_mib),
1318 }
1319 }
1320
1321 pub fn tmpfs(size_mib: u32) -> Self {
1323 Self::Tmpfs {
1324 size_mib: Some(size_mib),
1325 }
1326 }
1327
1328 pub fn flat(size_mib: u32) -> Self {
1330 Self::Flat {
1331 size_mib: Some(size_mib),
1332 fstype: None,
1333 clone: FlatClone::Auto,
1334 }
1335 }
1336
1337 pub fn size_mib(&self) -> Option<u32> {
1339 match self {
1340 Self::Managed { size_mib } | Self::Tmpfs { size_mib } | Self::Flat { size_mib, .. } => {
1341 *size_mib
1342 }
1343 Self::DiskImage { .. } => None,
1344 }
1345 }
1346
1347 pub fn kind_str(&self) -> &'static str {
1349 match self {
1350 Self::Managed { .. } => "managed",
1351 Self::Tmpfs { .. } => "tmpfs",
1352 Self::DiskImage { .. } => "disk-image",
1353 Self::Flat { .. } => "flat",
1354 }
1355 }
1356
1357 pub fn is_managed(&self) -> bool {
1359 matches!(self, Self::Managed { .. })
1360 }
1361}
1362
1363impl FlatClone {
1364 pub const fn as_str(self) -> &'static str {
1366 match self {
1367 Self::Auto => "auto",
1368 Self::Copy => "copy",
1369 Self::Reflink => "reflink",
1370 }
1371 }
1372
1373 pub const fn is_auto(&self) -> bool {
1375 matches!(self, Self::Auto)
1376 }
1377}
1378
1379impl RootfsSource {
1380 pub fn oci(reference: impl Into<String>) -> Self {
1382 Self::Oci(OciRootfsSource::new(reference))
1383 }
1384
1385 pub fn oci_reference(&self) -> Option<&str> {
1387 match self {
1388 Self::Oci(oci) => Some(&oci.reference),
1389 _ => None,
1390 }
1391 }
1392
1393 pub fn oci_root_disk(&self) -> Option<&RootDisk> {
1395 match self {
1396 Self::Oci(oci) => oci.root_disk.as_ref(),
1397 _ => None,
1398 }
1399 }
1400
1401 pub fn oci_managed_root_disk_size_mib(&self) -> Option<u32> {
1404 match self {
1405 Self::Oci(oci) => match &oci.root_disk {
1406 Some(RootDisk::Managed { size_mib }) => *size_mib,
1407 Some(_) => None,
1408 None => None,
1409 },
1410 _ => None,
1411 }
1412 }
1413}
1414
1415impl EnvVar {
1416 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1418 Self {
1419 key: key.into(),
1420 value: value.into(),
1421 }
1422 }
1423
1424 pub fn as_pair(&self) -> (&str, &str) {
1426 (&self.key, &self.value)
1427 }
1428}
1429
1430impl VolumeKind {
1431 pub fn as_str(self) -> &'static str {
1433 match self {
1434 Self::Directory => "dir",
1435 Self::Disk => "disk",
1436 }
1437 }
1438
1439 pub fn from_db_value(value: &str) -> Self {
1441 match value {
1442 "disk" => Self::Disk,
1443 _ => Self::Directory,
1444 }
1445 }
1446}
1447
1448impl VolumeSpec {
1449 pub fn new(name: impl Into<String>) -> Self {
1451 Self {
1452 name: name.into(),
1453 kind: VolumeKind::Directory,
1454 quota_mib: None,
1455 capacity_mib: None,
1456 labels: Vec::new(),
1457 }
1458 }
1459}
1460
1461impl NamedVolumeCreate {
1462 pub fn mode(&self) -> NamedVolumeMode {
1464 self.mode
1465 }
1466
1467 pub fn name(&self) -> &str {
1469 &self.name
1470 }
1471
1472 pub fn kind(&self) -> VolumeKind {
1474 self.kind
1475 }
1476
1477 pub fn quota_mib(&self) -> Option<u32> {
1479 self.quota_mib
1480 }
1481
1482 pub fn capacity_mib(&self) -> Option<u32> {
1484 self.capacity_mib
1485 }
1486
1487 pub fn labels(&self) -> &[(String, String)] {
1489 &self.labels
1490 }
1491}
1492
1493impl VolumeMount {
1494 pub fn guest(&self) -> &str {
1496 match self {
1497 Self::Bind { guest, .. }
1498 | Self::Owned { guest, .. }
1499 | Self::Named { guest, .. }
1500 | Self::Tmpfs { guest, .. }
1501 | Self::DiskImage { guest, .. } => guest,
1502 }
1503 }
1504
1505 fn guest_mut(&mut self) -> &mut String {
1506 match self {
1507 Self::Bind { guest, .. }
1508 | Self::Owned { guest, .. }
1509 | Self::Named { guest, .. }
1510 | Self::Tmpfs { guest, .. }
1511 | Self::DiskImage { guest, .. } => guest,
1512 }
1513 }
1514
1515 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1517 match self {
1518 Self::Named { create, .. } => create.as_ref(),
1519 _ => None,
1520 }
1521 }
1522}
1523
1524pub fn owned_volume_mount_id(guest: &str) -> String {
1531 use std::fmt::Write as _;
1532 let slug: String = guest
1533 .trim_start_matches('/')
1534 .chars()
1535 .take(11)
1536 .map(|character| {
1537 if character.is_ascii_alphanumeric() || character == '-' {
1538 character
1539 } else {
1540 '_'
1541 }
1542 })
1543 .collect();
1544 let mut id = if slug.is_empty() {
1545 String::new()
1546 } else {
1547 format!("{slug}_")
1548 };
1549 for byte in Sha256::digest(guest.as_bytes()).iter().take(4) {
1550 let _ = write!(id, "{byte:02x}");
1551 }
1552 id
1553}
1554
1555pub fn canonicalize_volume_mounts(mounts: &mut [VolumeMount]) -> TypesResult<()> {
1562 for mount in mounts.iter_mut() {
1563 let canonical = canonical_guest_mount_path(mount.guest())?;
1564 *mount.guest_mut() = canonical;
1565 }
1566
1567 mounts.sort_by_cached_key(|mount| guest_mount_order_key(mount.guest()));
1568
1569 for pair in mounts.windows(2) {
1570 if pair[0].guest() == pair[1].guest() {
1571 return Err(TypesError::invalid_config(format!(
1572 "multiple volumes cannot mount the same guest path: {}",
1573 pair[0].guest()
1574 )));
1575 }
1576 }
1577
1578 Ok(())
1579}
1580
1581fn canonical_guest_mount_path(guest: &str) -> TypesResult<String> {
1582 let path = Utf8UnixPath::new(guest);
1583
1584 if !path.is_valid() {
1585 return Err(TypesError::invalid_config(format!(
1586 "guest mount path must be a valid Unix path: {guest}"
1587 )));
1588 }
1589 if !path.is_absolute() {
1590 return Err(TypesError::invalid_config(format!(
1591 "guest mount path must be absolute: {guest}"
1592 )));
1593 }
1594 if path
1595 .components()
1596 .any(|component| matches!(component, Utf8UnixComponent::ParentDir))
1597 {
1598 return Err(TypesError::invalid_config(format!(
1599 "guest mount path must not contain '..': {guest}"
1600 )));
1601 }
1602 if guest.contains(':') || guest.contains(';') || guest.contains(',') {
1603 return Err(TypesError::invalid_config(format!(
1604 "guest mount path must not contain ':', ';', or ',': {guest}"
1605 )));
1606 }
1607
1608 let canonical = path.normalize().to_string();
1609 if canonical == "/" {
1610 return Err(TypesError::invalid_config(
1611 "cannot mount a volume at guest root /",
1612 ));
1613 }
1614
1615 Ok(canonical)
1616}
1617
1618fn guest_mount_order_key(guest: &str) -> (usize, String) {
1619 let path = Utf8UnixPath::new(guest);
1620 let depth = path.components().filter(Utf8Component::is_normal).count();
1621 (depth, guest.to_owned())
1622}
1623
1624impl RlimitResource {
1625 pub fn as_str(&self) -> &'static str {
1627 match self {
1628 Self::Cpu => "cpu",
1629 Self::Fsize => "fsize",
1630 Self::Data => "data",
1631 Self::Stack => "stack",
1632 Self::Core => "core",
1633 Self::Rss => "rss",
1634 Self::Nproc => "nproc",
1635 Self::Nofile => "nofile",
1636 Self::Memlock => "memlock",
1637 Self::As => "as",
1638 Self::Locks => "locks",
1639 Self::Sigpending => "sigpending",
1640 Self::Msgqueue => "msgqueue",
1641 Self::Nice => "nice",
1642 Self::Rtprio => "rtprio",
1643 Self::Rttime => "rttime",
1644 }
1645 }
1646}
1647
1648impl LogSource {
1649 pub fn effective(requested: &[Self]) -> Vec<Self> {
1651 if requested.is_empty() {
1652 vec![Self::Stdout, Self::Stderr, Self::Output]
1653 } else {
1654 let mut sources = requested.to_vec();
1655 sources.sort_by_key(|src| match src {
1656 Self::Stdout => 0,
1657 Self::Stderr => 1,
1658 Self::Output => 2,
1659 Self::System => 3,
1660 });
1661 sources.dedup();
1662 sources
1663 }
1664 }
1665}
1666
1667impl SandboxLogLevel {
1668 pub const fn as_str(self) -> &'static str {
1670 match self {
1671 Self::Error => "error",
1672 Self::Warn => "warn",
1673 Self::Info => "info",
1674 Self::Debug => "debug",
1675 Self::Trace => "trace",
1676 }
1677 }
1678}
1679
1680impl std::fmt::Display for DiskImageFormat {
1685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1686 f.write_str(self.as_str())
1687 }
1688}
1689
1690impl FromStr for DiskImageFormat {
1691 type Err = String;
1692
1693 fn from_str(s: &str) -> Result<Self, Self::Err> {
1694 match s {
1695 "qcow2" => Ok(Self::Qcow2),
1696 "raw" => Ok(Self::Raw),
1697 "vmdk" => Ok(Self::Vmdk),
1698 _ => Err(format!("unknown disk image format: {s}")),
1699 }
1700 }
1701}
1702
1703impl fmt::Display for TransparentHugePagePolicy {
1704 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1705 f.write_str(self.as_str())
1706 }
1707}
1708
1709impl FromStr for TransparentHugePagePolicy {
1710 type Err = String;
1711
1712 fn from_str(value: &str) -> Result<Self, Self::Err> {
1713 match value {
1714 "always" => Ok(Self::Always),
1715 "madvise" => Ok(Self::Madvise),
1716 "never" => Ok(Self::Never),
1717 _ => Err(format!(
1718 "unknown transparent huge-page policy: {value}; expected always, madvise, or never"
1719 )),
1720 }
1721 }
1722}
1723
1724impl Default for RootfsSource {
1725 fn default() -> Self {
1726 Self::oci(String::new())
1727 }
1728}
1729
1730impl Default for SandboxResources {
1731 fn default() -> Self {
1732 Self {
1733 cpus: DEFAULT_SANDBOX_CPUS,
1734 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1735 max_cpus: DEFAULT_SANDBOX_CPUS,
1736 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1737 cpu_placement: CpuPlacement::Inherit,
1738 placement_profile: None,
1739 thp: TransparentHugePagePolicy::Madvise,
1740 }
1741 }
1742}
1743
1744impl<'de> Deserialize<'de> for SandboxResources {
1745 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1746 where
1747 D: serde::Deserializer<'de>,
1748 {
1749 #[derive(Deserialize)]
1750 struct RawResources {
1751 #[serde(default = "default_sandbox_cpus")]
1752 cpus: u8,
1753 #[serde(default = "default_sandbox_memory_mib")]
1754 memory_mib: u32,
1755 max_cpus: Option<u8>,
1756 max_memory_mib: Option<u32>,
1757 #[serde(default)]
1758 cpu_placement: CpuPlacement,
1759 #[serde(default)]
1760 placement_profile: Option<String>,
1761 #[serde(default)]
1762 thp: TransparentHugePagePolicy,
1763 }
1764
1765 let raw = RawResources::deserialize(deserializer)?;
1766 Ok(Self {
1767 cpus: raw.cpus,
1768 memory_mib: raw.memory_mib,
1769 max_cpus: raw.max_cpus.unwrap_or(raw.cpus),
1773 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1774 cpu_placement: raw.cpu_placement,
1775 placement_profile: raw.placement_profile,
1776 thp: raw.thp,
1777 })
1778 }
1779}
1780
1781impl CpuPlacement {
1782 pub const fn is_inherit(&self) -> bool {
1784 matches!(self, Self::Inherit)
1785 }
1786}
1787
1788impl std::fmt::Display for CpuPlacement {
1789 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1790 f.write_str(match self {
1791 Self::Inherit => "inherit",
1792 Self::Auto => "auto",
1793 Self::Spread => "spread",
1794 Self::Compact => "compact",
1795 })
1796 }
1797}
1798
1799impl FromStr for CpuPlacement {
1800 type Err = String;
1801
1802 fn from_str(value: &str) -> Result<Self, Self::Err> {
1803 match value {
1804 "inherit" => Ok(Self::Inherit),
1805 "auto" => Ok(Self::Auto),
1806 "spread" => Ok(Self::Spread),
1807 "compact" => Ok(Self::Compact),
1808 _ => Err(format!(
1809 "unknown CPU placement: {value} (expected: inherit, auto, spread, compact)"
1810 )),
1811 }
1812 }
1813}
1814
1815impl Default for SandboxRuntimeOptions {
1816 fn default() -> Self {
1817 Self {
1818 workdir: None,
1819 shell: None,
1820 scripts: BTreeMap::new(),
1821 entrypoint: None,
1822 cmd: None,
1823 hostname: None,
1824 user: None,
1825 log_level: None,
1826 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1827 disable_metrics_sample: false,
1828 }
1829 }
1830}
1831
1832impl Default for NetworkSpec {
1833 fn default() -> Self {
1834 Self {
1835 enabled: true,
1836 interface: None,
1837 ports: Vec::new(),
1838 policy: None,
1839 dns: None,
1840 tls: None,
1841 strict: true,
1842 secrets: None,
1843 max_tcp_connections: None,
1844 max_udp_connections: None,
1845 tcp_accept_queue_size: None,
1846 rate_limiter: None,
1847 nat64_prefixes: default_nat64_prefixes(),
1848 trust_host_cas: false,
1849 outbound_proxy: None,
1850 http: HttpConfig::default(),
1851 }
1852 }
1853}
1854
1855pub(crate) fn default_nat64_prefixes() -> Vec<Ipv6Network> {
1856 vec![
1857 WELL_KNOWN_NAT64_PREFIX
1858 .parse()
1859 .expect("well-known NAT64 prefix must be valid"),
1860 ]
1861}
1862
1863impl Default for PublishedPortSpec {
1864 fn default() -> Self {
1865 Self {
1866 host_port: 0,
1867 guest_port: 0,
1868 protocol: PortProtocol::Tcp,
1869 host_bind: "127.0.0.1".into(),
1870 }
1871 }
1872}
1873
1874impl From<(String, String)> for EnvVar {
1875 fn from((key, value): (String, String)) -> Self {
1876 Self { key, value }
1877 }
1878}
1879
1880impl From<EnvVar> for (String, String) {
1881 fn from(var: EnvVar) -> Self {
1882 (var.key, var.value)
1883 }
1884}
1885
1886impl FromStr for SandboxLogLevel {
1887 type Err = String;
1888
1889 fn from_str(s: &str) -> Result<Self, Self::Err> {
1890 match s {
1891 "error" => Ok(Self::Error),
1892 "warn" => Ok(Self::Warn),
1893 "info" => Ok(Self::Info),
1894 "debug" => Ok(Self::Debug),
1895 "trace" => Ok(Self::Trace),
1896 _ => Err(format!("unknown sandbox log level: {s}")),
1897 }
1898 }
1899}
1900
1901impl std::fmt::Display for SandboxLogLevel {
1902 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1903 formatter.write_str(self.as_str())
1904 }
1905}
1906
1907impl Serialize for VolumeMount {
1908 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1909 use serde::ser::SerializeMap;
1910
1911 match self {
1912 Self::Owned {
1913 guest,
1914 storage,
1915 options,
1916 stat_virtualization,
1917 host_permissions,
1918 } => {
1919 let mut map = serializer.serialize_map(Some(6))?;
1922 map.serialize_entry("type", "Owned")?;
1923 map.serialize_entry("guest", guest)?;
1924 map.serialize_entry("storage", storage)?;
1925 map.serialize_entry("options", options)?;
1926 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1927 map.serialize_entry("host_permissions", host_permissions)?;
1928 map.end()
1929 }
1930 Self::Bind {
1931 host,
1932 guest,
1933 options,
1934 stat_virtualization,
1935 host_permissions,
1936 follow_root_symlinks,
1937 quota_mib,
1938 } => {
1939 let mut map = serializer.serialize_map(Some(8))?;
1940 map.serialize_entry("type", "Bind")?;
1941 map.serialize_entry("host", host)?;
1942 map.serialize_entry("guest", guest)?;
1943 map.serialize_entry("options", options)?;
1944 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1945 map.serialize_entry("host_permissions", host_permissions)?;
1946 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1947 map.serialize_entry("quota_mib", quota_mib)?;
1948 map.end()
1949 }
1950 Self::Named {
1951 name,
1952 guest,
1953 create: _,
1954 options,
1955 stat_virtualization,
1956 host_permissions,
1957 follow_root_symlinks,
1958 } => {
1959 let mut map = serializer.serialize_map(Some(7))?;
1960 map.serialize_entry("type", "Named")?;
1961 map.serialize_entry("name", name)?;
1962 map.serialize_entry("guest", guest)?;
1963 map.serialize_entry("options", options)?;
1964 map.serialize_entry("stat_virtualization", stat_virtualization)?;
1965 map.serialize_entry("host_permissions", host_permissions)?;
1966 map.serialize_entry("follow_root_symlinks", follow_root_symlinks)?;
1967 map.end()
1968 }
1969 Self::Tmpfs {
1970 guest,
1971 size_mib,
1972 options,
1973 } => {
1974 let mut map = serializer.serialize_map(Some(4))?;
1975 map.serialize_entry("type", "Tmpfs")?;
1976 map.serialize_entry("guest", guest)?;
1977 map.serialize_entry("size_mib", size_mib)?;
1978 map.serialize_entry("options", options)?;
1979 map.end()
1980 }
1981 Self::DiskImage {
1982 host,
1983 guest,
1984 format,
1985 fstype,
1986 options,
1987 } => {
1988 let mut map = serializer.serialize_map(Some(6))?;
1989 map.serialize_entry("type", "DiskImage")?;
1990 map.serialize_entry("host", host)?;
1991 map.serialize_entry("guest", guest)?;
1992 map.serialize_entry("format", format)?;
1993 map.serialize_entry("fstype", fstype)?;
1994 map.serialize_entry("options", options)?;
1995 map.end()
1996 }
1997 }
1998 }
1999}
2000
2001impl<'de> Deserialize<'de> for VolumeMount {
2002 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2003 fn default_strict() -> StatVirtualization {
2004 StatVirtualization::Strict
2005 }
2006
2007 fn default_private() -> HostPermissions {
2008 HostPermissions::Private
2009 }
2010
2011 #[derive(Deserialize)]
2012 #[serde(tag = "type")]
2013 enum VolumeMountHelper {
2014 Owned {
2015 guest: String,
2016 storage: OwnedVolumeStorage,
2017 #[serde(default)]
2018 options: MountOptions,
2019 #[serde(default = "default_strict")]
2020 stat_virtualization: StatVirtualization,
2021 #[serde(default = "default_private")]
2022 host_permissions: HostPermissions,
2023 },
2024 Bind {
2025 host: PathBuf,
2026 guest: String,
2027 #[serde(default)]
2028 options: Option<MountOptions>,
2029 #[serde(default)]
2030 readonly: bool,
2031 #[serde(default = "default_strict")]
2032 stat_virtualization: StatVirtualization,
2033 #[serde(default = "default_private")]
2034 host_permissions: HostPermissions,
2035 #[serde(default)]
2036 follow_root_symlinks: bool,
2037 #[serde(default)]
2038 quota_mib: Option<u32>,
2039 },
2040 Named {
2041 name: String,
2042 guest: String,
2043 #[serde(default)]
2044 options: Option<MountOptions>,
2045 #[serde(default)]
2046 readonly: bool,
2047 #[serde(default = "default_strict")]
2048 stat_virtualization: StatVirtualization,
2049 #[serde(default = "default_private")]
2050 host_permissions: HostPermissions,
2051 #[serde(default)]
2052 follow_root_symlinks: bool,
2053 },
2054 Tmpfs {
2055 guest: String,
2056 #[serde(default)]
2057 size_mib: Option<u32>,
2058 #[serde(default)]
2059 options: Option<MountOptions>,
2060 #[serde(default)]
2061 readonly: bool,
2062 },
2063 DiskImage {
2064 host: PathBuf,
2065 guest: String,
2066 format: DiskImageFormat,
2067 #[serde(default)]
2068 fstype: Option<String>,
2069 #[serde(default)]
2070 options: Option<MountOptions>,
2071 #[serde(default)]
2072 readonly: bool,
2073 },
2074 }
2075
2076 let helper = VolumeMountHelper::deserialize(deserializer)?;
2077 Ok(match helper {
2078 VolumeMountHelper::Owned {
2079 guest,
2080 storage,
2081 options,
2082 stat_virtualization,
2083 host_permissions,
2084 } => Self::Owned {
2085 guest,
2086 storage,
2087 options,
2088 stat_virtualization,
2089 host_permissions,
2090 },
2091 VolumeMountHelper::Bind {
2092 host,
2093 guest,
2094 options,
2095 readonly,
2096 stat_virtualization,
2097 host_permissions,
2098 follow_root_symlinks,
2099 quota_mib,
2100 } => Self::Bind {
2101 host,
2102 guest,
2103 options: decode_mount_options(options, readonly),
2104 stat_virtualization,
2105 host_permissions,
2106 follow_root_symlinks,
2107 quota_mib,
2108 },
2109 VolumeMountHelper::Named {
2110 name,
2111 guest,
2112 options,
2113 readonly,
2114 stat_virtualization,
2115 host_permissions,
2116 follow_root_symlinks,
2117 } => Self::Named {
2118 name,
2119 guest,
2120 create: None,
2121 options: decode_mount_options(options, readonly),
2122 stat_virtualization,
2123 host_permissions,
2124 follow_root_symlinks,
2125 },
2126 VolumeMountHelper::Tmpfs {
2127 guest,
2128 size_mib,
2129 options,
2130 readonly,
2131 } => Self::Tmpfs {
2132 guest,
2133 size_mib,
2134 options: decode_mount_options(options, readonly),
2135 },
2136 VolumeMountHelper::DiskImage {
2137 host,
2138 guest,
2139 format,
2140 fstype,
2141 options,
2142 readonly,
2143 } => Self::DiskImage {
2144 host,
2145 guest,
2146 format,
2147 fstype,
2148 options: decode_mount_options(options, readonly),
2149 },
2150 })
2151 }
2152}
2153
2154impl fmt::Debug for VolumeMount {
2155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2156 match self {
2157 Self::Owned {
2158 guest,
2159 storage,
2160 options,
2161 stat_virtualization,
2162 host_permissions,
2163 } => f
2164 .debug_struct("Owned")
2165 .field("guest", guest)
2166 .field("storage", storage)
2167 .field("options", options)
2168 .field("stat_virtualization", stat_virtualization)
2169 .field("host_permissions", host_permissions)
2170 .finish(),
2171 Self::Bind {
2172 host,
2173 guest,
2174 options,
2175 stat_virtualization,
2176 host_permissions,
2177 follow_root_symlinks,
2178 quota_mib,
2179 } => f
2180 .debug_struct("Bind")
2181 .field("host", host)
2182 .field("guest", guest)
2183 .field("options", options)
2184 .field("stat_virtualization", stat_virtualization)
2185 .field("host_permissions", host_permissions)
2186 .field("follow_root_symlinks", follow_root_symlinks)
2187 .field("quota_mib", quota_mib)
2188 .finish(),
2189 Self::Named {
2190 name,
2191 guest,
2192 create,
2193 options,
2194 stat_virtualization,
2195 host_permissions,
2196 follow_root_symlinks,
2197 } => f
2198 .debug_struct("Named")
2199 .field("name", name)
2200 .field("guest", guest)
2201 .field("create", create)
2202 .field("options", options)
2203 .field("stat_virtualization", stat_virtualization)
2204 .field("host_permissions", host_permissions)
2205 .field("follow_root_symlinks", follow_root_symlinks)
2206 .finish(),
2207 Self::Tmpfs {
2208 guest,
2209 size_mib,
2210 options,
2211 } => f
2212 .debug_struct("Tmpfs")
2213 .field("guest", guest)
2214 .field("size_mib", size_mib)
2215 .field("options", options)
2216 .finish(),
2217 Self::DiskImage {
2218 host,
2219 guest,
2220 format,
2221 fstype,
2222 options,
2223 } => f
2224 .debug_struct("DiskImage")
2225 .field("host", host)
2226 .field("guest", guest)
2227 .field("format", format)
2228 .field("fstype", fstype)
2229 .field("options", options)
2230 .finish(),
2231 }
2232 }
2233}
2234
2235impl TryFrom<&str> for RlimitResource {
2237 type Error = String;
2238
2239 fn try_from(s: &str) -> Result<Self, Self::Error> {
2240 match s.to_ascii_lowercase().as_str() {
2241 "cpu" => Ok(Self::Cpu),
2242 "fsize" => Ok(Self::Fsize),
2243 "data" => Ok(Self::Data),
2244 "stack" => Ok(Self::Stack),
2245 "core" => Ok(Self::Core),
2246 "rss" => Ok(Self::Rss),
2247 "nproc" => Ok(Self::Nproc),
2248 "nofile" => Ok(Self::Nofile),
2249 "memlock" => Ok(Self::Memlock),
2250 "as" => Ok(Self::As),
2251 "locks" => Ok(Self::Locks),
2252 "sigpending" => Ok(Self::Sigpending),
2253 "msgqueue" => Ok(Self::Msgqueue),
2254 "nice" => Ok(Self::Nice),
2255 "rtprio" => Ok(Self::Rtprio),
2256 "rttime" => Ok(Self::Rttime),
2257 _ => Err(format!("unknown rlimit resource: {s}")),
2258 }
2259 }
2260}
2261
2262fn default_sandbox_cpus() -> u8 {
2267 DEFAULT_SANDBOX_CPUS
2268}
2269
2270fn default_sandbox_memory_mib() -> u32 {
2271 DEFAULT_SANDBOX_MEMORY_MIB
2272}
2273
2274fn decode_mount_options(options: Option<MountOptions>, readonly: bool) -> MountOptions {
2275 options.unwrap_or(MountOptions {
2276 readonly,
2277 ..MountOptions::default()
2278 })
2279}
2280
2281fn merge_env_vars(base: &mut Vec<EnvVar>, higher: Vec<EnvVar>) {
2282 for value in higher {
2283 match base.iter_mut().find(|current| current.key == value.key) {
2284 Some(current) => *current = value,
2285 None => base.push(value),
2286 }
2287 }
2288}
2289
2290fn merge_secret_entries(base: &mut Vec<SecretEntry>, higher: Vec<SecretEntry>) {
2291 for value in higher {
2292 match base
2293 .iter_mut()
2294 .find(|current| current.env_var == value.env_var)
2295 {
2296 Some(current) => *current = value,
2297 None => base.push(value),
2298 }
2299 }
2300}
2301
2302pub(crate) fn default_strict() -> StatVirtualization {
2304 StatVirtualization::Strict
2305}
2306
2307pub(crate) fn default_private() -> HostPermissions {
2309 HostPermissions::Private
2310}
2311
2312pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
2314
2315#[derive(Debug, Clone, Default, Serialize, Deserialize, ConfigPatch)]
2326#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2327#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2328pub struct SecretsConfig {
2329 #[doc(hidden)]
2332 #[serde(default, skip_serializing_if = "Option::is_none")]
2333 #[cfg_attr(feature = "ts", ts(skip))]
2334 #[cfg_attr(feature = "utoipa", schema(ignore))]
2335 pub passthrough_hosts: Option<Vec<HostPattern>>,
2336
2337 #[serde(default)]
2339 #[config_patch(merge_with = merge_secret_entries)]
2340 pub secrets: Vec<SecretEntry>,
2341
2342 #[serde(default)]
2344 pub violation_action: SecretViolationAction,
2345}
2346
2347#[derive(Clone, Serialize, Deserialize)]
2352#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2353#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2354pub struct SecretEntry {
2355 pub env_var: String,
2361
2362 #[serde(default = "empty_secret_value")]
2371 #[cfg_attr(feature = "ts", ts(type = "string"))]
2372 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2373 pub value: Zeroizing<String>,
2374
2375 #[serde(default, skip_serializing_if = "Option::is_none")]
2379 pub source: Option<SecretSource>,
2380
2381 pub placeholder: String,
2386
2387 #[serde(default)]
2389 pub allowed_hosts: Vec<HostPattern>,
2390
2391 #[serde(default)]
2393 pub substitution: SecretSubstitution,
2394
2395 #[serde(default)]
2397 pub passthrough_hosts: Vec<HostPattern>,
2398
2399 #[serde(default, skip_serializing_if = "Option::is_none")]
2401 pub violation_action: Option<SecretViolationAction>,
2402
2403 #[serde(default = "default_true")]
2408 pub require_tls_identity: bool,
2409}
2410
2411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2413#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2414#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2415#[serde(rename_all = "kebab-case")]
2416pub enum HostPattern {
2417 #[serde(alias = "Exact")]
2419 Exact(String),
2420 #[serde(alias = "Wildcard")]
2422 Wildcard(String),
2423 #[serde(alias = "Any")]
2425 Any,
2426}
2427
2428#[derive(Debug, Clone, Serialize, Deserialize)]
2430#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2431#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2432pub struct SecretSubstitution {
2433 #[serde(default = "default_true")]
2435 pub headers: bool,
2436
2437 #[serde(default)]
2439 pub query: bool,
2440
2441 #[serde(default)]
2449 pub body: bool,
2450}
2451
2452#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2454#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2455#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2456#[serde(rename_all = "kebab-case")]
2457pub enum SecretViolationAction {
2458 #[serde(alias = "Block")]
2460 Block,
2461 #[default]
2463 #[serde(alias = "BlockAndLog", alias = "block_and_log")]
2464 BlockAndLog,
2465 #[serde(alias = "BlockAndTerminate", alias = "block_and_terminate")]
2467 BlockAndTerminate,
2468}
2469
2470#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2472pub enum SecretConfigError {
2473 #[error("secret #{secret_index}: env_var must not be empty")]
2475 EmptyEnvVar {
2476 secret_index: usize,
2478 },
2479
2480 #[error("secret #{secret_index}: env_var must not contain `=`")]
2482 EnvVarContainsEquals {
2483 secret_index: usize,
2485 },
2486
2487 #[error("secret #{secret_index}: env_var must not contain NUL")]
2489 EnvVarContainsNul {
2490 secret_index: usize,
2492 },
2493
2494 #[error("secret #{secret_index}: at least one allowed host is required")]
2496 MissingAllowedHosts {
2497 secret_index: usize,
2499 },
2500
2501 #[error("secret #{secret_index}: at least one substitution location is required")]
2503 MissingSubstitutionLocation {
2504 secret_index: usize,
2506 },
2507
2508 #[error("secret #{secret_index}: placeholder must not be empty")]
2510 EmptyPlaceholder {
2511 secret_index: usize,
2513 },
2514
2515 #[error(
2517 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
2518 )]
2519 PlaceholderTooLong {
2520 secret_index: usize,
2522 actual_bytes: usize,
2524 max_bytes: usize,
2526 },
2527
2528 #[error("secret #{secret_index}: placeholder must not contain NUL")]
2530 PlaceholderContainsNul {
2531 secret_index: usize,
2533 },
2534
2535 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
2537 PlaceholderContainsLineBreak {
2538 secret_index: usize,
2540 },
2541}
2542
2543impl SecretsConfig {
2544 pub fn has_tls_identity_secrets(&self) -> bool {
2546 self.secrets
2547 .iter()
2548 .any(|secret| secret.require_tls_identity)
2549 }
2550
2551 pub fn contains_env_var(&self, env_var: &str) -> bool {
2553 self.secrets.iter().any(|secret| secret.env_var == env_var)
2554 }
2555
2556 pub fn validate(&self) -> Result<(), SecretConfigError> {
2558 for (index, secret) in self.secrets.iter().enumerate() {
2559 secret.validate(index)?;
2560 }
2561 Ok(())
2562 }
2563}
2564
2565impl SecretEntry {
2566 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
2568 validate_env_var(&self.env_var, secret_index)?;
2569
2570 if self.allowed_hosts.is_empty() {
2571 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
2572 }
2573
2574 if !self.substitution.headers && !self.substitution.query && !self.substitution.body {
2575 return Err(SecretConfigError::MissingSubstitutionLocation { secret_index });
2576 }
2577
2578 validate_placeholder(&self.placeholder, secret_index)
2579 }
2580}
2581
2582impl fmt::Debug for SecretEntry {
2584 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2585 f.debug_struct("SecretEntry")
2586 .field("env_var", &self.env_var)
2587 .field("value", &"[REDACTED]")
2588 .field("source", &self.source)
2589 .field("placeholder", &self.placeholder)
2590 .field("allowed_hosts", &self.allowed_hosts)
2591 .field("substitution", &self.substitution)
2592 .field("passthrough_hosts", &self.passthrough_hosts)
2593 .field("violation_action", &self.violation_action)
2594 .field("require_tls_identity", &self.require_tls_identity)
2595 .finish()
2596 }
2597}
2598
2599impl HostPattern {
2600 pub fn parse(host: &str) -> Self {
2603 if host == "*" {
2604 HostPattern::Any
2605 } else if host.starts_with("*.") {
2606 HostPattern::Wildcard(host.to_string())
2607 } else {
2608 HostPattern::Exact(host.to_string())
2609 }
2610 }
2611
2612 pub fn matches(&self, hostname: &str) -> bool {
2617 match self {
2618 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
2619 HostPattern::Wildcard(pattern) => {
2620 if let Some(suffix) = pattern.strip_prefix("*.") {
2621 hostname.eq_ignore_ascii_case(suffix)
2622 || (hostname.len() > suffix.len() + 1
2623 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
2624 && hostname[hostname.len() - suffix.len()..]
2625 .eq_ignore_ascii_case(suffix))
2626 } else {
2627 hostname.eq_ignore_ascii_case(pattern)
2628 }
2629 }
2630 HostPattern::Any => true,
2631 }
2632 }
2633}
2634
2635impl Default for SecretSubstitution {
2636 fn default() -> Self {
2637 Self {
2638 headers: true,
2639 query: false,
2640 body: false,
2641 }
2642 }
2643}
2644
2645fn default_true() -> bool {
2646 true
2647}
2648
2649fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2650 if env_var.is_empty() {
2651 return Err(SecretConfigError::EmptyEnvVar { secret_index });
2652 }
2653 if env_var.contains('=') {
2654 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
2655 }
2656 if env_var.contains('\0') {
2657 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
2658 }
2659 Ok(())
2660}
2661
2662fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
2663 if placeholder.is_empty() {
2664 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
2665 }
2666
2667 let actual_bytes = placeholder.len();
2668 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
2669 return Err(SecretConfigError::PlaceholderTooLong {
2670 secret_index,
2671 actual_bytes,
2672 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2673 });
2674 }
2675
2676 if placeholder.contains('\0') {
2677 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
2678 }
2679 if placeholder.contains('\r') || placeholder.contains('\n') {
2680 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
2681 }
2682
2683 Ok(())
2684}
2685
2686#[derive(Debug, Clone, Serialize, Deserialize, ConfigPatch)]
2696#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2697#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2698pub struct TlsConfig {
2699 #[serde(default)]
2701 pub enabled: bool,
2702
2703 #[serde(default = "default_intercepted_ports")]
2705 pub intercepted_ports: Vec<u16>,
2706
2707 #[serde(default)]
2709 pub bypass: Vec<String>,
2710
2711 #[serde(default = "default_true")]
2713 pub verify_upstream: bool,
2714
2715 #[serde(default = "default_true")]
2718 pub block_quic_on_intercept: bool,
2719
2720 #[serde(default)]
2722 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
2723 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
2724 pub upstream_ca_cert: Vec<PathBuf>,
2725
2726 #[serde(default, alias = "scoped_upstream_ca_certs")]
2728 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
2729
2730 #[serde(default)]
2732 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
2733
2734 #[serde(default, alias = "ca")]
2737 pub intercept_ca: InterceptCaConfig,
2738
2739 #[serde(default)]
2741 pub cache: CertCacheConfig,
2742}
2743
2744#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2746#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2747#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2748pub struct InterceptCaConfig {
2749 #[serde(default)]
2752 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2753 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2754 pub cert_path: Option<PathBuf>,
2755
2756 #[serde(default)]
2759 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
2760 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
2761 pub key_path: Option<PathBuf>,
2762}
2763
2764#[derive(Debug, Clone, Serialize, Deserialize)]
2766#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2767#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2768pub struct CertCacheConfig {
2769 #[serde(default = "default_cache_capacity")]
2771 pub capacity: usize,
2772
2773 #[serde(default = "default_cert_validity_hours")]
2775 pub validity_hours: u64,
2776}
2777
2778#[derive(Debug, Clone, Serialize, Deserialize)]
2780#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2781#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2782pub struct ScopedUpstreamCaCert {
2783 pub pattern: String,
2785
2786 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2788 #[cfg_attr(feature = "ts", ts(type = "string"))]
2789 pub path: PathBuf,
2790}
2791
2792#[derive(Debug, Clone, Serialize, Deserialize)]
2794#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2795#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2796pub struct ScopedVerifyUpstream {
2797 pub pattern: String,
2799
2800 pub verify: bool,
2802}
2803
2804impl Default for TlsConfig {
2805 fn default() -> Self {
2806 Self {
2807 enabled: false,
2808 intercepted_ports: default_intercepted_ports(),
2809 bypass: Vec::new(),
2810 verify_upstream: true,
2811 block_quic_on_intercept: true,
2812 upstream_ca_cert: Vec::new(),
2813 scoped_upstream_ca_cert: Vec::new(),
2814 scoped_verify_upstream: Vec::new(),
2815 intercept_ca: InterceptCaConfig::default(),
2816 cache: CertCacheConfig::default(),
2817 }
2818 }
2819}
2820
2821impl Default for CertCacheConfig {
2822 fn default() -> Self {
2823 Self {
2824 capacity: default_cache_capacity(),
2825 validity_hours: default_cert_validity_hours(),
2826 }
2827 }
2828}
2829
2830fn default_intercepted_ports() -> Vec<u16> {
2831 vec![443]
2832}
2833
2834fn default_cache_capacity() -> usize {
2835 1000
2836}
2837
2838fn default_cert_validity_hours() -> u64 {
2839 24
2840}
2841
2842#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2848#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2849#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2850#[serde(rename_all = "snake_case")]
2851pub enum Action {
2852 Allow,
2854 Deny,
2856}
2857
2858#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2860#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2861#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2862#[serde(rename_all = "snake_case")]
2863pub enum Direction {
2864 Egress,
2866 Ingress,
2868 Any,
2870}
2871
2872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2874#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2875#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2876#[serde(rename_all = "snake_case")]
2877pub enum Protocol {
2878 Tcp,
2880 Udp,
2882 Icmpv4,
2884 Icmpv6,
2886}
2887
2888#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2890#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2891#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2892#[serde(rename_all = "snake_case")]
2893pub enum DestinationGroup {
2894 Public,
2896 Loopback,
2898 Private,
2900 LinkLocal,
2902 Metadata,
2904 Multicast,
2906 Host,
2908}
2909
2910#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2917#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2918#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2919#[serde(rename_all = "snake_case")]
2920pub enum Destination {
2921 Any,
2923 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
2925 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
2926 Domain(String),
2928 DomainSuffix(String),
2930 Group(DestinationGroup),
2932}
2933
2934#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2936#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2937#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2938pub struct PortRange {
2939 pub start: u16,
2941 pub end: u16,
2943}
2944
2945#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2948#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2949#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2950pub struct Rule {
2951 pub direction: Direction,
2953 pub destination: Destination,
2955 #[serde(default)]
2957 pub protocols: Vec<Protocol>,
2958 #[serde(default)]
2960 pub ports: Vec<PortRange>,
2961 pub action: Action,
2963}
2964
2965#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2968#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2969#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2970pub struct NetworkPolicy {
2971 #[serde(default = "action_deny")]
2973 pub default_egress: Action,
2974 #[serde(default = "action_deny")]
2976 pub default_ingress: Action,
2977 #[serde(default)]
2979 pub rules: Vec<Rule>,
2980}
2981
2982fn action_deny() -> Action {
2985 Action::Deny
2986}
2987
2988#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
2994#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
2995#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
2996#[serde(default)]
2997pub struct DnsConfig {
2998 pub rebind_protection: bool,
3000 pub nameservers: Vec<String>,
3003 pub query_timeout_ms: u64,
3005}
3006
3007impl Default for DnsConfig {
3008 fn default() -> Self {
3009 Self {
3010 rebind_protection: true,
3011 nameservers: Vec::new(),
3012 query_timeout_ms: 5000,
3013 }
3014 }
3015}
3016
3017#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3021#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3022#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3023#[serde(default)]
3024pub struct InterfaceOverrides {
3025 #[serde(skip_serializing_if = "Option::is_none")]
3027 pub mac: Option<[u8; 6]>,
3028 #[serde(skip_serializing_if = "Option::is_none")]
3030 pub mtu: Option<u16>,
3031 #[serde(skip_serializing_if = "Option::is_none")]
3033 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3034 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3035 pub ipv4_address: Option<Ipv4Addr>,
3036 #[serde(skip_serializing_if = "Option::is_none")]
3038 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3039 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3040 pub ipv4_pool: Option<Ipv4Network>,
3041 #[serde(skip_serializing_if = "Option::is_none")]
3043 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3044 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3045 pub ipv6_address: Option<Ipv6Addr>,
3046 #[serde(skip_serializing_if = "Option::is_none")]
3048 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
3049 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
3050 pub ipv6_pool: Option<Ipv6Network>,
3051}
3052
3053fn empty_secret_value() -> Zeroizing<String> {
3054 Zeroizing::new(String::new())
3055}
3056
3057#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3063pub enum NetworkRateLimitDirection {
3064 Egress,
3066 Ingress,
3068}
3069
3070#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ConfigPatch)]
3072#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3073#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3074#[serde(default)]
3075pub struct NetworkRateLimiterConfig {
3076 #[serde(skip_serializing_if = "Option::is_none")]
3078 pub egress: Option<RateLimiterConfig>,
3079
3080 #[serde(skip_serializing_if = "Option::is_none")]
3082 pub ingress: Option<RateLimiterConfig>,
3083}
3084
3085#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3091#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3092#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3093#[serde(default)]
3094pub struct RateLimiterConfig {
3095 #[serde(skip_serializing_if = "Option::is_none")]
3097 pub bandwidth: Option<TokenBucketConfig>,
3098
3099 #[serde(skip_serializing_if = "Option::is_none")]
3101 pub ops: Option<TokenBucketConfig>,
3102}
3103
3104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3110#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
3111#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
3112pub struct TokenBucketConfig {
3113 pub size: u64,
3115
3116 pub refill_time_ms: u64,
3119
3120 #[serde(default)]
3122 pub one_time_burst: u64,
3123}
3124
3125#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
3127pub enum RateLimitConfigError {
3128 #[error("rate limiter must configure at least one of bandwidth or ops")]
3130 EmptyLimiter,
3131
3132 #[error("{bucket} bucket: size must be greater than zero")]
3134 ZeroSize {
3135 bucket: &'static str,
3137 },
3138
3139 #[error("{bucket} bucket: refill_time_ms must be greater than zero")]
3141 ZeroRefillTime {
3142 bucket: &'static str,
3144 },
3145}
3146
3147impl RateLimiterConfig {
3148 pub fn validate(&self) -> Result<(), RateLimitConfigError> {
3150 if self.bandwidth.is_none() && self.ops.is_none() {
3151 return Err(RateLimitConfigError::EmptyLimiter);
3152 }
3153 if let Some(bandwidth) = &self.bandwidth {
3154 bandwidth.validate("bandwidth")?;
3155 }
3156 if let Some(ops) = &self.ops {
3157 ops.validate("ops")?;
3158 }
3159 Ok(())
3160 }
3161}
3162
3163impl TokenBucketConfig {
3164 pub fn validate(&self, bucket: &'static str) -> Result<(), RateLimitConfigError> {
3166 if self.size == 0 {
3167 return Err(RateLimitConfigError::ZeroSize { bucket });
3168 }
3169 if self.refill_time_ms == 0 {
3170 return Err(RateLimitConfigError::ZeroRefillTime { bucket });
3171 }
3172 Ok(())
3173 }
3174}
3175
3176impl fmt::Display for NetworkRateLimitDirection {
3177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3178 match self {
3179 Self::Egress => f.write_str("egress"),
3180 Self::Ingress => f.write_str("ingress"),
3181 }
3182 }
3183}
3184
3185#[cfg(test)]
3190mod tests {
3191 use super::*;
3192
3193 fn secret_entry(env_var: &str, require_tls_identity: bool) -> SecretEntry {
3194 SecretEntry {
3195 env_var: env_var.to_owned(),
3196 value: Zeroizing::new("secret".to_owned()),
3197 source: None,
3198 placeholder: format!("$MSB_{env_var}"),
3199 allowed_hosts: vec![HostPattern::Any],
3200 substitution: SecretSubstitution::default(),
3201 passthrough_hosts: Vec::new(),
3202 violation_action: None,
3203 require_tls_identity,
3204 }
3205 }
3206
3207 fn tmpfs_mount(guest: &str) -> VolumeMount {
3208 VolumeMount::Tmpfs {
3209 guest: guest.to_owned(),
3210 size_mib: None,
3211 options: MountOptions::default(),
3212 }
3213 }
3214
3215 #[test]
3216 fn mount_options_omit_unset_owner_but_accept_missing_fields() {
3217 let value = serde_json::to_value(MountOptions::default()).unwrap();
3218 assert!(value.get("override_uid").is_none());
3219 assert!(value.get("override_gid").is_none());
3220
3221 let decoded: MountOptions = serde_json::from_value(value).unwrap();
3222 assert_eq!(decoded.override_uid, None);
3223 assert_eq!(decoded.override_gid, None);
3224 }
3225
3226 #[test]
3227 fn volume_mounts_are_canonicalized_and_ordered_parent_first() {
3228 let mut mounts = vec![
3229 tmpfs_mount("/workspace//persist/./logs/"),
3230 tmpfs_mount("/alpha/z"),
3231 tmpfs_mount("/workspace"),
3232 ];
3233
3234 canonicalize_volume_mounts(&mut mounts).unwrap();
3235
3236 assert_eq!(
3237 mounts.iter().map(VolumeMount::guest).collect::<Vec<_>>(),
3238 vec!["/workspace", "/alpha/z", "/workspace/persist/logs"]
3239 );
3240 }
3241
3242 #[test]
3243 fn secrets_config_queries_entries() {
3244 let mut config = SecretsConfig {
3245 secrets: vec![secret_entry("HTTP_TOKEN", false)],
3246 ..Default::default()
3247 };
3248
3249 assert!(!config.has_tls_identity_secrets());
3250 assert!(config.contains_env_var("HTTP_TOKEN"));
3251 assert!(!config.contains_env_var("MISSING"));
3252
3253 config.secrets.push(secret_entry("API_KEY", true));
3254 assert!(config.has_tls_identity_secrets());
3255 }
3256
3257 #[test]
3258 fn volume_mounts_reject_duplicate_canonical_paths() {
3259 let mut mounts = vec![tmpfs_mount("/data/cache"), tmpfs_mount("/data//./cache/")];
3260
3261 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3262
3263 assert!(error.to_string().contains("same guest path: /data/cache"));
3264 }
3265
3266 #[test]
3267 fn volume_mounts_reject_parent_components_before_normalizing() {
3268 let mut mounts = vec![tmpfs_mount("/workspace/../secrets")];
3269
3270 let error = canonicalize_volume_mounts(&mut mounts).unwrap_err();
3271
3272 assert!(error.to_string().contains("must not contain '..'"));
3273 }
3274
3275 #[test]
3276 fn disk_image_format_from_extension() {
3277 assert_eq!(
3278 DiskImageFormat::from_extension("qcow2"),
3279 Some(DiskImageFormat::Qcow2)
3280 );
3281 assert_eq!(
3282 DiskImageFormat::from_extension("raw"),
3283 Some(DiskImageFormat::Raw)
3284 );
3285 assert_eq!(
3286 DiskImageFormat::from_extension("vmdk"),
3287 Some(DiskImageFormat::Vmdk)
3288 );
3289 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
3290 assert_eq!(DiskImageFormat::from_extension(""), None);
3291 }
3292
3293 #[test]
3294 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
3295 let resources: SandboxResources =
3296 serde_json::from_str(r#"{"cpus":4,"memory_mib":2048}"#).unwrap();
3297
3298 assert_eq!(resources.cpus, 4);
3299 assert_eq!(resources.max_cpus, 4);
3300 assert_eq!(resources.memory_mib, 2048);
3301 assert_eq!(resources.max_memory_mib, 2048);
3302 assert_eq!(resources.cpu_placement, CpuPlacement::Inherit);
3303 assert_eq!(resources.thp, TransparentHugePagePolicy::Madvise);
3304 assert_eq!(
3305 serde_json::to_value(resources).unwrap(),
3306 serde_json::json!({
3307 "cpus": 4,
3308 "memory_mib": 2048,
3309 "max_cpus": 4,
3310 "max_memory_mib": 2048
3311 })
3312 );
3313 }
3314
3315 #[test]
3316 fn cpu_placement_omits_inherit_and_roundtrips_managed_policies() {
3317 let inherited = serde_json::to_value(SandboxResources::default()).unwrap();
3318 assert!(inherited.get("cpu_placement").is_none());
3319
3320 for policy in [
3321 CpuPlacement::Auto,
3322 CpuPlacement::Spread,
3323 CpuPlacement::Compact,
3324 ] {
3325 let resources = SandboxResources {
3326 cpu_placement: policy,
3327 ..Default::default()
3328 };
3329 let json = serde_json::to_string(&resources).unwrap();
3330 let decoded: SandboxResources = serde_json::from_str(&json).unwrap();
3331
3332 assert_eq!(decoded.cpu_placement, policy);
3333 assert_eq!(policy.to_string().parse::<CpuPlacement>().unwrap(), policy);
3334 }
3335 }
3336
3337 #[test]
3338 fn transparent_huge_page_policy_roundtrips_non_default() {
3339 let resources: SandboxResources = serde_json::from_str(
3340 r#"{"cpus":2,"memory_mib":8192,"max_cpus":2,"max_memory_mib":8192,"thp":"always"}"#,
3341 )
3342 .unwrap();
3343
3344 assert_eq!(resources.thp, TransparentHugePagePolicy::Always);
3345 assert_eq!(
3346 serde_json::to_value(resources).unwrap()["thp"],
3347 serde_json::json!("always")
3348 );
3349 assert_eq!(
3350 "never".parse::<TransparentHugePagePolicy>().unwrap(),
3351 TransparentHugePagePolicy::Never
3352 );
3353 assert!("auto".parse::<TransparentHugePagePolicy>().is_err());
3354 }
3355
3356 #[test]
3357 fn disk_image_format_display_roundtrip() {
3358 for format in [
3359 DiskImageFormat::Qcow2,
3360 DiskImageFormat::Raw,
3361 DiskImageFormat::Vmdk,
3362 ] {
3363 let rendered = format.to_string();
3364 let parsed: DiskImageFormat = rendered.parse().unwrap();
3365 assert_eq!(parsed, format);
3366 }
3367 }
3368
3369 #[test]
3370 fn disk_image_format_from_str_unknown() {
3371 assert!("ext4".parse::<DiskImageFormat>().is_err());
3372 }
3373
3374 #[test]
3375 fn log_source_effective_uses_default_user_program_sources() {
3376 assert_eq!(
3377 LogSource::effective(&[]),
3378 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
3379 );
3380 }
3381
3382 #[test]
3383 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
3384 assert_eq!(
3385 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
3386 vec![LogSource::Stdout, LogSource::System]
3387 );
3388 }
3389
3390 #[test]
3391 fn rlimit_resource_parses_case_insensitively() {
3392 assert_eq!(
3393 RlimitResource::try_from("NOFILE").unwrap(),
3394 RlimitResource::Nofile
3395 );
3396 assert!(RlimitResource::try_from("bogus").is_err());
3397 }
3398
3399 #[test]
3400 fn sandbox_policy_serde_roundtrip() {
3401 let policy = SandboxPolicy {
3402 ephemeral: true,
3403 max_duration_secs: Some(3600),
3404 idle_timeout_secs: Some(120),
3405 };
3406
3407 let json = serde_json::to_string(&policy).unwrap();
3408 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
3409
3410 assert!(decoded.ephemeral);
3411 assert_eq!(decoded.max_duration_secs, Some(3600));
3412 assert_eq!(decoded.idle_timeout_secs, Some(120));
3413 }
3414
3415 #[test]
3416 fn sandbox_policy_defaults_to_persistent() {
3417 assert!(!SandboxPolicy::default().ephemeral);
3418 }
3419
3420 #[test]
3421 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
3422 let decoded: SandboxPolicy =
3425 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
3426 assert!(!decoded.ephemeral);
3427 assert_eq!(decoded.max_duration_secs, Some(60));
3428 }
3429
3430 #[test]
3431 fn sandbox_spec_default_uses_static_resource_defaults() {
3432 let spec = SandboxSpec::default();
3433
3434 assert_eq!(spec.resources.cpus, DEFAULT_SANDBOX_CPUS);
3435 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
3436 assert_eq!(
3437 spec.runtime.metrics_sample_interval_ms,
3438 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
3439 );
3440 assert_eq!(spec.deployment_profile, DeploymentProfile::SingleTenant);
3441 }
3442
3443 #[test]
3444 fn deployment_profile_uses_stable_snake_case_wire_values() {
3445 assert_eq!(
3446 serde_json::to_string(&DeploymentProfile::MultiTenant).unwrap(),
3447 r#""multi_tenant""#
3448 );
3449 assert_eq!(
3450 serde_json::from_str::<DeploymentProfile>(r#""single_tenant""#).unwrap(),
3451 DeploymentProfile::SingleTenant
3452 );
3453 }
3454
3455 #[test]
3456 fn sandbox_log_level_roundtrips_lowercase_values() {
3457 for (input, expected) in [
3458 ("error", SandboxLogLevel::Error),
3459 ("warn", SandboxLogLevel::Warn),
3460 ("info", SandboxLogLevel::Info),
3461 ("debug", SandboxLogLevel::Debug),
3462 ("trace", SandboxLogLevel::Trace),
3463 ] {
3464 let parsed: SandboxLogLevel = input.parse().unwrap();
3465 assert_eq!(parsed, expected);
3466 assert_eq!(parsed.as_str(), input);
3467 }
3468 }
3469}