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 serde::{Deserialize, Serialize};
11use zeroize::Zeroizing;
12
13use crate::modify::SecretSource;
14
15pub const DEFAULT_SANDBOX_VCPUS: u8 = 1;
21
22pub const DEFAULT_SANDBOX_MEMORY_MIB: u32 = 512;
24
25pub const DEFAULT_METRICS_SAMPLE_INTERVAL_MS: u64 = 1000;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
35#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
36#[serde(rename_all = "snake_case")]
37pub enum DiskImageFormat {
38 #[serde(alias = "Qcow2")]
40 Qcow2,
41 #[serde(alias = "Raw")]
43 Raw,
44 #[serde(alias = "Vmdk")]
46 Vmdk,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
52#[serde(rename_all = "snake_case")]
53pub enum RootfsSource {
54 #[serde(alias = "Bind")]
56 Bind(
57 #[cfg_attr(feature = "ts", ts(type = "string"))]
59 PathBuf,
60 ),
61
62 #[serde(alias = "Oci")]
64 Oci(OciRootfsSource),
65
66 #[serde(alias = "DiskImage")]
68 DiskImage {
69 #[cfg_attr(feature = "ts", ts(type = "string"))]
71 path: PathBuf,
72 format: DiskImageFormat,
74 fstype: Option<String>,
76 },
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
82#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
83pub struct OciRootfsSource {
84 pub reference: String,
86
87 #[serde(
89 default,
90 alias = "disk_size_mib",
91 skip_serializing_if = "Option::is_none"
92 )]
93 pub upper_size_mib: Option<u32>,
94}
95
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
98#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
99#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
100#[serde(rename_all = "snake_case")]
101pub enum PullPolicy {
102 #[default]
104 #[serde(alias = "IfMissing")]
105 IfMissing,
106
107 #[serde(alias = "Always")]
109 Always,
110
111 #[serde(alias = "Never")]
113 Never,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
125#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
126#[serde(rename_all = "lowercase")]
127pub enum StatVirtualization {
128 Strict,
130 Relaxed,
132 Off,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
141#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
142#[serde(rename_all = "lowercase")]
143pub enum HostPermissions {
144 Private,
146 Mirror,
148}
149
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
152#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
153#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
154#[serde(rename_all = "lowercase")]
155pub enum SecurityProfile {
156 #[default]
160 Default,
161
162 Restricted,
166}
167
168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
170#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
171#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
172#[serde(default)]
173pub struct MountOptions {
174 pub readonly: bool,
178
179 pub noexec: bool,
183
184 pub nosuid: bool,
186
187 pub nodev: bool,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
194#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
195#[serde(rename_all = "snake_case")]
196pub enum VolumeKind {
197 #[serde(alias = "Directory")]
199 Directory,
200
201 #[serde(alias = "Disk")]
203 Disk,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
209#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
210pub struct VolumeSpec {
211 pub name: String,
213
214 pub kind: VolumeKind,
216
217 pub quota_mib: Option<u32>,
219
220 pub capacity_mib: Option<u32>,
222
223 pub labels: Vec<(String, String)>,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
230#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
231#[serde(rename_all = "snake_case")]
232pub enum NamedVolumeMode {
233 #[serde(alias = "Existing")]
235 Existing,
236
237 #[serde(alias = "Create")]
239 Create,
240
241 #[serde(alias = "EnsureExists")]
243 EnsureExists,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
249#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
250pub struct NamedVolumeCreate {
251 pub mode: NamedVolumeMode,
253
254 pub name: String,
256
257 pub kind: VolumeKind,
259
260 pub quota_mib: Option<u32>,
262
263 pub capacity_mib: Option<u32>,
265
266 pub labels: Vec<(String, String)>,
268}
269
270fn default_strict() -> StatVirtualization {
272 StatVirtualization::Strict
273}
274
275fn default_private() -> HostPermissions {
277 HostPermissions::Private
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
283#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
284#[serde(rename_all = "snake_case")]
285pub enum VolumeMount {
286 #[serde(alias = "Bind")]
288 Bind {
289 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
291 #[cfg_attr(feature = "ts", ts(type = "string"))]
292 host: PathBuf,
293 guest: String,
295 #[serde(default)]
297 options: MountOptions,
298 #[serde(default = "default_strict")]
300 stat_virtualization: StatVirtualization,
301 #[serde(default = "default_private")]
303 host_permissions: HostPermissions,
304 #[serde(default)]
310 quota_mib: Option<u32>,
311 },
312
313 #[serde(alias = "Named")]
315 Named {
316 name: String,
318 guest: String,
320 #[serde(skip)]
324 create: Option<NamedVolumeCreate>,
325 #[serde(default)]
327 options: MountOptions,
328 #[serde(default = "default_strict")]
330 stat_virtualization: StatVirtualization,
331 #[serde(default = "default_private")]
333 host_permissions: HostPermissions,
334 },
335
336 #[serde(alias = "Tmpfs")]
338 Tmpfs {
339 guest: String,
341 #[serde(default)]
343 size_mib: Option<u32>,
344 #[serde(default)]
346 options: MountOptions,
347 },
348
349 #[serde(alias = "DiskImage")]
351 DiskImage {
352 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
354 #[cfg_attr(feature = "ts", ts(type = "string"))]
355 host: PathBuf,
356 guest: String,
358 format: DiskImageFormat,
360 #[serde(default)]
362 fstype: Option<String>,
363 #[serde(default)]
365 options: MountOptions,
366 },
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
372#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
373#[serde(rename_all = "snake_case")]
374pub enum Patch {
375 #[serde(alias = "Text")]
377 Text {
378 path: String,
380 content: String,
382 mode: Option<u32>,
384 replace: bool,
386 },
387
388 #[serde(alias = "File")]
390 File {
391 path: String,
393 content: Vec<u8>,
395 mode: Option<u32>,
397 replace: bool,
399 },
400
401 #[serde(alias = "CopyFile")]
403 CopyFile {
404 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
406 #[cfg_attr(feature = "ts", ts(type = "string"))]
407 src: PathBuf,
408 dst: String,
410 mode: Option<u32>,
412 replace: bool,
414 },
415
416 #[serde(alias = "CopyDir")]
418 CopyDir {
419 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
421 #[cfg_attr(feature = "ts", ts(type = "string"))]
422 src: PathBuf,
423 dst: String,
425 replace: bool,
427 },
428
429 #[serde(alias = "Symlink")]
431 Symlink {
432 target: String,
434 link: String,
436 replace: bool,
438 },
439
440 #[serde(alias = "Mkdir")]
442 Mkdir {
443 path: String,
445 mode: Option<u32>,
447 },
448
449 #[serde(alias = "Remove")]
451 Remove {
452 path: String,
454 },
455
456 #[serde(alias = "Append")]
458 Append {
459 path: String,
461 content: String,
463 },
464}
465
466pub const MAX_SECRET_PLACEHOLDER_BYTES: usize = 1024;
472
473#[derive(Debug, Clone, Default, Serialize, Deserialize)]
480#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
481#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
482pub struct SecretsConfig {
483 #[serde(default, alias = "secrets")]
485 pub entries: Vec<SecretEntry>,
486
487 #[serde(default)]
489 pub on_violation: ViolationAction,
490}
491
492#[derive(Clone, Serialize, Deserialize)]
497#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
498#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
499pub struct SecretEntry {
500 pub env_var: String,
506
507 #[serde(default = "empty_secret_value")]
516 #[cfg_attr(feature = "ts", ts(type = "string"))]
517 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
518 pub value: Zeroizing<String>,
519
520 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub source: Option<SecretSource>,
525
526 pub placeholder: String,
531
532 #[serde(default)]
534 pub allowed_hosts: Vec<HostPattern>,
535
536 #[serde(default)]
538 pub injection: SecretInjection,
539
540 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub on_violation: Option<ViolationAction>,
543
544 #[serde(default = "default_true")]
549 pub require_tls_identity: bool,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
554#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
555#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
556#[serde(rename_all = "snake_case")]
557pub enum HostPattern {
558 #[serde(alias = "Exact")]
560 Exact(String),
561 #[serde(alias = "Wildcard")]
563 Wildcard(String),
564 #[serde(alias = "Any")]
566 Any,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
571#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
572#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
573pub struct SecretInjection {
574 #[serde(default = "default_true")]
576 pub headers: bool,
577
578 #[serde(default = "default_true")]
580 pub basic_auth: bool,
581
582 #[serde(default)]
584 pub query_params: bool,
585
586 #[serde(default)]
594 pub body: bool,
595}
596
597#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
599#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
600#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
601#[serde(rename_all = "snake_case")]
602pub enum ViolationAction {
603 #[serde(alias = "Block")]
605 Block,
606 #[default]
608 #[serde(alias = "BlockAndLog", alias = "block-and-log")]
609 BlockAndLog,
610 #[serde(alias = "BlockAndTerminate", alias = "block-and-terminate")]
612 BlockAndTerminate,
613 #[serde(alias = "Passthrough")]
615 Passthrough(Vec<HostPattern>),
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
620pub enum SecretConfigError {
621 #[error("secret #{secret_index}: env_var must not be empty")]
623 EmptyEnvVar {
624 secret_index: usize,
626 },
627
628 #[error("secret #{secret_index}: env_var must not contain `=`")]
630 EnvVarContainsEquals {
631 secret_index: usize,
633 },
634
635 #[error("secret #{secret_index}: env_var must not contain NUL")]
637 EnvVarContainsNul {
638 secret_index: usize,
640 },
641
642 #[error("secret #{secret_index}: at least one allowed host is required")]
644 MissingAllowedHosts {
645 secret_index: usize,
647 },
648
649 #[error("secret #{secret_index}: placeholder must not be empty")]
651 EmptyPlaceholder {
652 secret_index: usize,
654 },
655
656 #[error(
658 "secret #{secret_index}: placeholder must be at most {max_bytes} bytes, got {actual_bytes}"
659 )]
660 PlaceholderTooLong {
661 secret_index: usize,
663 actual_bytes: usize,
665 max_bytes: usize,
667 },
668
669 #[error("secret #{secret_index}: placeholder must not contain NUL")]
671 PlaceholderContainsNul {
672 secret_index: usize,
674 },
675
676 #[error("secret #{secret_index}: placeholder must not contain CR or LF")]
678 PlaceholderContainsLineBreak {
679 secret_index: usize,
681 },
682}
683
684impl SecretsConfig {
685 pub fn validate(&self) -> Result<(), SecretConfigError> {
687 for (index, secret) in self.entries.iter().enumerate() {
688 secret.validate(index)?;
689 }
690 Ok(())
691 }
692}
693
694impl SecretEntry {
695 pub fn validate(&self, secret_index: usize) -> Result<(), SecretConfigError> {
697 validate_env_var(&self.env_var, secret_index)?;
698
699 if self.allowed_hosts.is_empty() {
700 return Err(SecretConfigError::MissingAllowedHosts { secret_index });
701 }
702
703 validate_placeholder(&self.placeholder, secret_index)
704 }
705}
706
707impl fmt::Debug for SecretEntry {
709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710 f.debug_struct("SecretEntry")
711 .field("env_var", &self.env_var)
712 .field("value", &"[REDACTED]")
713 .field("source", &self.source)
714 .field("placeholder", &self.placeholder)
715 .field("allowed_hosts", &self.allowed_hosts)
716 .field("injection", &self.injection)
717 .field("on_violation", &self.on_violation)
718 .field("require_tls_identity", &self.require_tls_identity)
719 .finish()
720 }
721}
722
723impl HostPattern {
724 pub fn parse(host: &str) -> Self {
727 if host == "*" {
728 HostPattern::Any
729 } else if host.starts_with("*.") {
730 HostPattern::Wildcard(host.to_string())
731 } else {
732 HostPattern::Exact(host.to_string())
733 }
734 }
735
736 pub fn matches(&self, hostname: &str) -> bool {
741 match self {
742 HostPattern::Exact(h) => hostname.eq_ignore_ascii_case(h),
743 HostPattern::Wildcard(pattern) => {
744 if let Some(suffix) = pattern.strip_prefix("*.") {
745 hostname.eq_ignore_ascii_case(suffix)
746 || (hostname.len() > suffix.len() + 1
747 && hostname.as_bytes()[hostname.len() - suffix.len() - 1] == b'.'
748 && hostname[hostname.len() - suffix.len()..]
749 .eq_ignore_ascii_case(suffix))
750 } else {
751 hostname.eq_ignore_ascii_case(pattern)
752 }
753 }
754 HostPattern::Any => true,
755 }
756 }
757}
758
759impl Default for SecretInjection {
760 fn default() -> Self {
761 Self {
762 headers: true,
763 basic_auth: true,
764 query_params: false,
765 body: false,
766 }
767 }
768}
769
770fn default_true() -> bool {
771 true
772}
773
774fn validate_env_var(env_var: &str, secret_index: usize) -> Result<(), SecretConfigError> {
775 if env_var.is_empty() {
776 return Err(SecretConfigError::EmptyEnvVar { secret_index });
777 }
778 if env_var.contains('=') {
779 return Err(SecretConfigError::EnvVarContainsEquals { secret_index });
780 }
781 if env_var.contains('\0') {
782 return Err(SecretConfigError::EnvVarContainsNul { secret_index });
783 }
784 Ok(())
785}
786
787fn validate_placeholder(placeholder: &str, secret_index: usize) -> Result<(), SecretConfigError> {
788 if placeholder.is_empty() {
789 return Err(SecretConfigError::EmptyPlaceholder { secret_index });
790 }
791
792 let actual_bytes = placeholder.len();
793 if actual_bytes > MAX_SECRET_PLACEHOLDER_BYTES {
794 return Err(SecretConfigError::PlaceholderTooLong {
795 secret_index,
796 actual_bytes,
797 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
798 });
799 }
800
801 if placeholder.contains('\0') {
802 return Err(SecretConfigError::PlaceholderContainsNul { secret_index });
803 }
804 if placeholder.contains('\r') || placeholder.contains('\n') {
805 return Err(SecretConfigError::PlaceholderContainsLineBreak { secret_index });
806 }
807
808 Ok(())
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
821#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
822#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
823pub struct TlsConfig {
824 #[serde(default)]
826 pub enabled: bool,
827
828 #[serde(default = "default_intercepted_ports")]
830 pub intercepted_ports: Vec<u16>,
831
832 #[serde(default)]
834 pub bypass: Vec<String>,
835
836 #[serde(default = "default_true")]
838 pub verify_upstream: bool,
839
840 #[serde(default = "default_true")]
843 pub block_quic_on_intercept: bool,
844
845 #[serde(default)]
847 #[cfg_attr(feature = "utoipa", schema(value_type = Vec<String>))]
848 #[cfg_attr(feature = "ts", ts(type = "Array<string>"))]
849 pub upstream_ca_cert: Vec<PathBuf>,
850
851 #[serde(default, alias = "scoped_upstream_ca_certs")]
853 pub scoped_upstream_ca_cert: Vec<ScopedUpstreamCaCert>,
854
855 #[serde(default)]
857 pub scoped_verify_upstream: Vec<ScopedVerifyUpstream>,
858
859 #[serde(default, alias = "ca")]
862 pub intercept_ca: InterceptCaConfig,
863
864 #[serde(default)]
866 pub cache: CertCacheConfig,
867}
868
869#[derive(Debug, Clone, Default, Serialize, Deserialize)]
871#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
872#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
873pub struct InterceptCaConfig {
874 #[serde(default)]
877 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
878 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
879 pub cert_path: Option<PathBuf>,
880
881 #[serde(default)]
884 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
885 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
886 pub key_path: Option<PathBuf>,
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
891#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
892#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
893pub struct CertCacheConfig {
894 #[serde(default = "default_cache_capacity")]
896 pub capacity: usize,
897
898 #[serde(default = "default_cert_validity_hours")]
900 pub validity_hours: u64,
901}
902
903#[derive(Debug, Clone, Serialize, Deserialize)]
905#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
906#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
907pub struct ScopedUpstreamCaCert {
908 pub pattern: String,
910
911 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
913 #[cfg_attr(feature = "ts", ts(type = "string"))]
914 pub path: PathBuf,
915}
916
917#[derive(Debug, Clone, Serialize, Deserialize)]
919#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
920#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
921pub struct ScopedVerifyUpstream {
922 pub pattern: String,
924
925 pub verify: bool,
927}
928
929impl Default for TlsConfig {
930 fn default() -> Self {
931 Self {
932 enabled: false,
933 intercepted_ports: default_intercepted_ports(),
934 bypass: Vec::new(),
935 verify_upstream: true,
936 block_quic_on_intercept: true,
937 upstream_ca_cert: Vec::new(),
938 scoped_upstream_ca_cert: Vec::new(),
939 scoped_verify_upstream: Vec::new(),
940 intercept_ca: InterceptCaConfig::default(),
941 cache: CertCacheConfig::default(),
942 }
943 }
944}
945
946impl Default for CertCacheConfig {
947 fn default() -> Self {
948 Self {
949 capacity: default_cache_capacity(),
950 validity_hours: default_cert_validity_hours(),
951 }
952 }
953}
954
955fn default_intercepted_ports() -> Vec<u16> {
956 vec![443]
957}
958
959fn default_cache_capacity() -> usize {
960 1000
961}
962
963fn default_cert_validity_hours() -> u64 {
964 24
965}
966
967#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
973#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
974#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
975#[serde(rename_all = "kebab-case")]
976pub enum Action {
977 Allow,
979 Deny,
981}
982
983#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
985#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
986#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
987#[serde(rename_all = "kebab-case")]
988pub enum Direction {
989 Egress,
991 Ingress,
993 Any,
995}
996
997#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
999#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1000#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1001#[serde(rename_all = "kebab-case")]
1002pub enum Protocol {
1003 Tcp,
1005 Udp,
1007 Icmpv4,
1009 Icmpv6,
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1015#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1016#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1017#[serde(rename_all = "snake_case")]
1018pub enum DestinationGroup {
1019 Public,
1021 Loopback,
1023 Private,
1025 #[serde(alias = "link-local")]
1027 LinkLocal,
1028 Metadata,
1030 Multicast,
1032 Host,
1034}
1035
1036#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1043#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1044#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1045#[serde(rename_all = "snake_case")]
1046pub enum Destination {
1047 Any,
1049 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1051 Cidr(#[cfg_attr(feature = "ts", ts(type = "string"))] IpNetwork),
1052 Domain(String),
1054 #[serde(alias = "domain-suffix")]
1056 DomainSuffix(String),
1057 Group(DestinationGroup),
1059}
1060
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1063#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1064#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1065pub struct PortRange {
1066 pub start: u16,
1068 pub end: u16,
1070}
1071
1072#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1075#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1076#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1077pub struct Rule {
1078 pub direction: Direction,
1080 pub destination: Destination,
1082 #[serde(default)]
1084 pub protocols: Vec<Protocol>,
1085 #[serde(default)]
1087 pub ports: Vec<PortRange>,
1088 pub action: Action,
1090}
1091
1092#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1095#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1096#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1097pub struct NetworkPolicy {
1098 #[serde(default = "action_deny")]
1100 pub default_egress: Action,
1101 #[serde(default = "action_deny")]
1103 pub default_ingress: Action,
1104 #[serde(default)]
1106 pub rules: Vec<Rule>,
1107}
1108
1109fn action_deny() -> Action {
1112 Action::Deny
1113}
1114
1115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1121#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1122#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1123#[serde(default)]
1124pub struct DnsConfig {
1125 pub rebind_protection: bool,
1127 pub nameservers: Vec<String>,
1130 pub query_timeout_ms: u64,
1132}
1133
1134impl Default for DnsConfig {
1135 fn default() -> Self {
1136 Self {
1137 rebind_protection: true,
1138 nameservers: Vec::new(),
1139 query_timeout_ms: 5000,
1140 }
1141 }
1142}
1143
1144#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1148#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1149#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1150#[serde(default)]
1151pub struct InterfaceOverrides {
1152 #[serde(skip_serializing_if = "Option::is_none")]
1154 pub mac: Option<[u8; 6]>,
1155 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub mtu: Option<u16>,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1160 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1161 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
1162 pub ipv4_address: Option<Ipv4Addr>,
1163 #[serde(skip_serializing_if = "Option::is_none")]
1165 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1166 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
1167 pub ipv4_pool: Option<Ipv4Network>,
1168 #[serde(skip_serializing_if = "Option::is_none")]
1170 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1171 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
1172 pub ipv6_address: Option<Ipv6Addr>,
1173 #[serde(skip_serializing_if = "Option::is_none")]
1175 #[cfg_attr(feature = "utoipa", schema(value_type = Option<String>))]
1176 #[cfg_attr(feature = "ts", ts(type = "string | null"))]
1177 pub ipv6_pool: Option<Ipv6Network>,
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1193#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1194#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1195#[serde(default)]
1196pub struct NetworkSpec {
1197 pub enabled: bool,
1199
1200 #[serde(skip_serializing_if = "Option::is_none")]
1202 pub interface: Option<InterfaceOverrides>,
1203
1204 pub ports: Vec<PublishedPortSpec>,
1206
1207 #[serde(skip_serializing_if = "Option::is_none")]
1209 pub policy: Option<NetworkPolicy>,
1210
1211 #[serde(skip_serializing_if = "Option::is_none")]
1213 pub dns: Option<DnsConfig>,
1214
1215 #[serde(skip_serializing_if = "Option::is_none")]
1217 pub tls: Option<TlsConfig>,
1218
1219 #[serde(skip_serializing_if = "Option::is_none")]
1221 pub secrets: Option<SecretsConfig>,
1222
1223 pub max_connections: Option<usize>,
1225
1226 pub trust_host_cas: bool,
1228}
1229
1230#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1233#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1234pub struct PublishedPortSpec {
1235 pub host_port: u16,
1237
1238 pub guest_port: u16,
1240
1241 #[serde(default)]
1243 pub protocol: PortProtocol,
1244
1245 pub host_bind: String,
1247}
1248
1249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1251#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1252#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1253pub enum PortProtocol {
1254 #[default]
1256 #[serde(rename = "tcp")]
1257 Tcp,
1258
1259 #[serde(rename = "udp")]
1261 Udp,
1262}
1263
1264#[derive(Debug, Clone, Serialize, Deserialize)]
1270#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1271#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1272pub struct HandoffInit {
1273 #[cfg_attr(feature = "utoipa", schema(value_type = String))]
1275 #[cfg_attr(feature = "ts", ts(type = "string"))]
1276 pub cmd: PathBuf,
1277
1278 #[serde(default)]
1280 pub args: Vec<String>,
1281
1282 #[serde(default)]
1284 pub env: Vec<(String, String)>,
1285}
1286
1287#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1293#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1294#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1295pub struct SandboxPolicy {
1296 #[serde(default)]
1305 pub ephemeral: bool,
1306
1307 pub max_duration_secs: Option<u64>,
1312
1313 pub idle_timeout_secs: Option<u64>,
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize)]
1323#[serde(rename_all = "lowercase")]
1324pub enum SnapshotDestination {
1325 #[serde(alias = "Name")]
1327 Name(String),
1328
1329 #[serde(alias = "Path")]
1331 Path(
1332 PathBuf,
1334 ),
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1339pub struct SnapshotSpec {
1340 pub source_sandbox: String,
1342
1343 pub destination: SnapshotDestination,
1345
1346 pub labels: Vec<(String, String)>,
1348
1349 pub force: bool,
1351
1352 pub record_integrity: bool,
1354}
1355
1356#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1364#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1365#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1366#[serde(default)]
1367pub struct SandboxSpec {
1368 pub name: String,
1370
1371 #[cfg_attr(feature = "utoipa", schema(value_type = Object))]
1373 pub image: RootfsSource,
1374
1375 pub resources: SandboxResources,
1377
1378 pub runtime: SandboxRuntimeOptions,
1380
1381 pub env: Vec<EnvVar>,
1383
1384 pub labels: BTreeMap<String, String>,
1386
1387 pub rlimits: Vec<Rlimit>,
1389
1390 pub mounts: Vec<VolumeMount>,
1392
1393 pub patches: Vec<Patch>,
1395
1396 pub network: NetworkSpec,
1398
1399 pub init: Option<HandoffInit>,
1401
1402 pub pull_policy: PullPolicy,
1404
1405 pub security_profile: SecurityProfile,
1407
1408 pub lifecycle: SandboxPolicy,
1410}
1411
1412#[derive(Debug, Clone, Copy, Serialize)]
1414#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1415#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1416pub struct SandboxResources {
1417 pub vcpus: u8,
1419
1420 pub memory_mib: u32,
1422
1423 pub max_vcpus: u8,
1425
1426 pub max_memory_mib: u32,
1428}
1429
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1432#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1433#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1434#[serde(default)]
1435pub struct SandboxRuntimeOptions {
1436 pub workdir: Option<String>,
1438
1439 pub shell: Option<String>,
1441
1442 pub scripts: BTreeMap<String, String>,
1446
1447 pub entrypoint: Option<Vec<String>>,
1449
1450 pub cmd: Option<Vec<String>>,
1452
1453 pub hostname: Option<String>,
1455
1456 pub user: Option<String>,
1458
1459 pub log_level: Option<SandboxLogLevel>,
1461
1462 pub metrics_sample_interval_ms: Option<u64>,
1464
1465 pub disable_metrics_sample: bool,
1467}
1468
1469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1471#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1472#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1473pub struct EnvVar {
1474 pub key: String,
1476
1477 pub value: String,
1479}
1480
1481#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1483#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1484#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1485#[serde(rename_all = "lowercase")]
1486pub enum SandboxLogLevel {
1487 Error,
1489
1490 Warn,
1492
1493 Info,
1495
1496 Debug,
1498
1499 Trace,
1501}
1502
1503#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1509#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1510#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1511#[serde(rename_all = "lowercase")]
1512pub enum RlimitResource {
1513 #[serde(alias = "Cpu")]
1515 Cpu,
1516 #[serde(alias = "Fsize")]
1518 Fsize,
1519 #[serde(alias = "Data")]
1521 Data,
1522 #[serde(alias = "Stack")]
1524 Stack,
1525 #[serde(alias = "Core")]
1527 Core,
1528 #[serde(alias = "Rss")]
1530 Rss,
1531 #[serde(alias = "Nproc")]
1533 Nproc,
1534 #[serde(alias = "Nofile")]
1536 Nofile,
1537 #[serde(alias = "Memlock")]
1539 Memlock,
1540 #[serde(alias = "As")]
1542 As,
1543 #[serde(alias = "Locks")]
1545 Locks,
1546 #[serde(alias = "Sigpending")]
1548 Sigpending,
1549 #[serde(alias = "Msgqueue")]
1551 Msgqueue,
1552 #[serde(alias = "Nice")]
1554 Nice,
1555 #[serde(alias = "Rtprio")]
1557 Rtprio,
1558 #[serde(alias = "Rttime")]
1560 Rttime,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize)]
1565#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1566#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
1567pub struct Rlimit {
1568 pub resource: RlimitResource,
1570
1571 pub soft: u64,
1573
1574 pub hard: u64,
1576}
1577
1578#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1584#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1585#[serde(rename_all = "lowercase")]
1586pub enum LogSource {
1587 Stdout,
1589
1590 Stderr,
1592
1593 Output,
1595
1596 System,
1598}
1599
1600impl DiskImageFormat {
1605 pub fn as_str(&self) -> &'static str {
1607 match self {
1608 Self::Qcow2 => "qcow2",
1609 Self::Raw => "raw",
1610 Self::Vmdk => "vmdk",
1611 }
1612 }
1613
1614 pub fn from_extension(ext: &str) -> Option<Self> {
1618 match ext {
1619 "qcow2" => Some(Self::Qcow2),
1620 "raw" => Some(Self::Raw),
1621 "vmdk" => Some(Self::Vmdk),
1622 _ => None,
1623 }
1624 }
1625}
1626
1627impl OciRootfsSource {
1628 pub fn new(reference: impl Into<String>) -> Self {
1630 Self {
1631 reference: reference.into(),
1632 upper_size_mib: None,
1633 }
1634 }
1635}
1636
1637impl RootfsSource {
1638 pub fn oci(reference: impl Into<String>) -> Self {
1640 Self::Oci(OciRootfsSource::new(reference))
1641 }
1642
1643 pub fn oci_reference(&self) -> Option<&str> {
1645 match self {
1646 Self::Oci(oci) => Some(&oci.reference),
1647 _ => None,
1648 }
1649 }
1650
1651 pub fn oci_upper_size_mib(&self) -> Option<u32> {
1653 match self {
1654 Self::Oci(oci) => oci.upper_size_mib,
1655 _ => None,
1656 }
1657 }
1658}
1659
1660impl EnvVar {
1661 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1663 Self {
1664 key: key.into(),
1665 value: value.into(),
1666 }
1667 }
1668
1669 pub fn as_pair(&self) -> (&str, &str) {
1671 (&self.key, &self.value)
1672 }
1673}
1674
1675impl VolumeKind {
1676 pub fn as_str(self) -> &'static str {
1678 match self {
1679 Self::Directory => "dir",
1680 Self::Disk => "disk",
1681 }
1682 }
1683
1684 pub fn from_db_value(value: &str) -> Self {
1686 match value {
1687 "disk" => Self::Disk,
1688 _ => Self::Directory,
1689 }
1690 }
1691}
1692
1693impl VolumeSpec {
1694 pub fn new(name: impl Into<String>) -> Self {
1696 Self {
1697 name: name.into(),
1698 kind: VolumeKind::Directory,
1699 quota_mib: None,
1700 capacity_mib: None,
1701 labels: Vec::new(),
1702 }
1703 }
1704}
1705
1706impl NamedVolumeCreate {
1707 pub fn mode(&self) -> NamedVolumeMode {
1709 self.mode
1710 }
1711
1712 pub fn name(&self) -> &str {
1714 &self.name
1715 }
1716
1717 pub fn kind(&self) -> VolumeKind {
1719 self.kind
1720 }
1721
1722 pub fn quota_mib(&self) -> Option<u32> {
1724 self.quota_mib
1725 }
1726
1727 pub fn capacity_mib(&self) -> Option<u32> {
1729 self.capacity_mib
1730 }
1731
1732 pub fn labels(&self) -> &[(String, String)] {
1734 &self.labels
1735 }
1736}
1737
1738impl VolumeMount {
1739 pub fn guest(&self) -> &str {
1741 match self {
1742 Self::Bind { guest, .. }
1743 | Self::Named { guest, .. }
1744 | Self::Tmpfs { guest, .. }
1745 | Self::DiskImage { guest, .. } => guest,
1746 }
1747 }
1748
1749 pub fn named_create(&self) -> Option<&NamedVolumeCreate> {
1751 match self {
1752 Self::Named { create, .. } => create.as_ref(),
1753 _ => None,
1754 }
1755 }
1756}
1757
1758impl RlimitResource {
1759 pub fn as_str(&self) -> &'static str {
1761 match self {
1762 Self::Cpu => "cpu",
1763 Self::Fsize => "fsize",
1764 Self::Data => "data",
1765 Self::Stack => "stack",
1766 Self::Core => "core",
1767 Self::Rss => "rss",
1768 Self::Nproc => "nproc",
1769 Self::Nofile => "nofile",
1770 Self::Memlock => "memlock",
1771 Self::As => "as",
1772 Self::Locks => "locks",
1773 Self::Sigpending => "sigpending",
1774 Self::Msgqueue => "msgqueue",
1775 Self::Nice => "nice",
1776 Self::Rtprio => "rtprio",
1777 Self::Rttime => "rttime",
1778 }
1779 }
1780}
1781
1782impl LogSource {
1783 pub fn effective(requested: &[Self]) -> Vec<Self> {
1785 if requested.is_empty() {
1786 vec![Self::Stdout, Self::Stderr, Self::Output]
1787 } else {
1788 let mut sources = requested.to_vec();
1789 sources.sort_by_key(|src| match src {
1790 Self::Stdout => 0,
1791 Self::Stderr => 1,
1792 Self::Output => 2,
1793 Self::System => 3,
1794 });
1795 sources.dedup();
1796 sources
1797 }
1798 }
1799}
1800
1801impl SandboxLogLevel {
1802 pub const fn as_str(self) -> &'static str {
1804 match self {
1805 Self::Error => "error",
1806 Self::Warn => "warn",
1807 Self::Info => "info",
1808 Self::Debug => "debug",
1809 Self::Trace => "trace",
1810 }
1811 }
1812}
1813
1814impl std::fmt::Display for DiskImageFormat {
1819 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1820 f.write_str(self.as_str())
1821 }
1822}
1823
1824impl FromStr for DiskImageFormat {
1825 type Err = String;
1826
1827 fn from_str(s: &str) -> Result<Self, Self::Err> {
1828 match s {
1829 "qcow2" => Ok(Self::Qcow2),
1830 "raw" => Ok(Self::Raw),
1831 "vmdk" => Ok(Self::Vmdk),
1832 _ => Err(format!("unknown disk image format: {s}")),
1833 }
1834 }
1835}
1836
1837impl Default for RootfsSource {
1838 fn default() -> Self {
1839 Self::oci(String::new())
1840 }
1841}
1842
1843impl Default for SandboxResources {
1844 fn default() -> Self {
1845 Self {
1846 vcpus: DEFAULT_SANDBOX_VCPUS,
1847 memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1848 max_vcpus: DEFAULT_SANDBOX_VCPUS,
1849 max_memory_mib: DEFAULT_SANDBOX_MEMORY_MIB,
1850 }
1851 }
1852}
1853
1854impl<'de> Deserialize<'de> for SandboxResources {
1855 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1856 where
1857 D: serde::Deserializer<'de>,
1858 {
1859 #[derive(Deserialize)]
1860 struct RawResources {
1861 #[serde(default = "default_sandbox_vcpus")]
1862 vcpus: u8,
1863 #[serde(default = "default_sandbox_memory_mib")]
1864 memory_mib: u32,
1865 max_vcpus: Option<u8>,
1866 max_memory_mib: Option<u32>,
1867 }
1868
1869 let raw = RawResources::deserialize(deserializer)?;
1870 Ok(Self {
1871 vcpus: raw.vcpus,
1872 memory_mib: raw.memory_mib,
1873 max_vcpus: raw.max_vcpus.unwrap_or(raw.vcpus),
1877 max_memory_mib: raw.max_memory_mib.unwrap_or(raw.memory_mib),
1878 })
1879 }
1880}
1881
1882impl Default for SandboxRuntimeOptions {
1883 fn default() -> Self {
1884 Self {
1885 workdir: None,
1886 shell: None,
1887 scripts: BTreeMap::new(),
1888 entrypoint: None,
1889 cmd: None,
1890 hostname: None,
1891 user: None,
1892 log_level: None,
1893 metrics_sample_interval_ms: Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS),
1894 disable_metrics_sample: false,
1895 }
1896 }
1897}
1898
1899impl Default for NetworkSpec {
1900 fn default() -> Self {
1901 Self {
1902 enabled: true,
1903 interface: None,
1904 ports: Vec::new(),
1905 policy: None,
1906 dns: None,
1907 tls: None,
1908 secrets: None,
1909 max_connections: None,
1910 trust_host_cas: false,
1911 }
1912 }
1913}
1914
1915impl Default for PublishedPortSpec {
1916 fn default() -> Self {
1917 Self {
1918 host_port: 0,
1919 guest_port: 0,
1920 protocol: PortProtocol::Tcp,
1921 host_bind: "127.0.0.1".into(),
1922 }
1923 }
1924}
1925
1926impl From<(String, String)> for EnvVar {
1927 fn from((key, value): (String, String)) -> Self {
1928 Self { key, value }
1929 }
1930}
1931
1932impl From<EnvVar> for (String, String) {
1933 fn from(var: EnvVar) -> Self {
1934 (var.key, var.value)
1935 }
1936}
1937
1938impl FromStr for SandboxLogLevel {
1939 type Err = String;
1940
1941 fn from_str(s: &str) -> Result<Self, Self::Err> {
1942 match s {
1943 "error" => Ok(Self::Error),
1944 "warn" => Ok(Self::Warn),
1945 "info" => Ok(Self::Info),
1946 "debug" => Ok(Self::Debug),
1947 "trace" => Ok(Self::Trace),
1948 _ => Err(format!("unknown sandbox log level: {s}")),
1949 }
1950 }
1951}
1952
1953impl TryFrom<&str> for RlimitResource {
1955 type Error = String;
1956
1957 fn try_from(s: &str) -> Result<Self, Self::Error> {
1958 match s.to_ascii_lowercase().as_str() {
1959 "cpu" => Ok(Self::Cpu),
1960 "fsize" => Ok(Self::Fsize),
1961 "data" => Ok(Self::Data),
1962 "stack" => Ok(Self::Stack),
1963 "core" => Ok(Self::Core),
1964 "rss" => Ok(Self::Rss),
1965 "nproc" => Ok(Self::Nproc),
1966 "nofile" => Ok(Self::Nofile),
1967 "memlock" => Ok(Self::Memlock),
1968 "as" => Ok(Self::As),
1969 "locks" => Ok(Self::Locks),
1970 "sigpending" => Ok(Self::Sigpending),
1971 "msgqueue" => Ok(Self::Msgqueue),
1972 "nice" => Ok(Self::Nice),
1973 "rtprio" => Ok(Self::Rtprio),
1974 "rttime" => Ok(Self::Rttime),
1975 _ => Err(format!("unknown rlimit resource: {s}")),
1976 }
1977 }
1978}
1979
1980fn default_sandbox_vcpus() -> u8 {
1985 DEFAULT_SANDBOX_VCPUS
1986}
1987
1988fn default_sandbox_memory_mib() -> u32 {
1989 DEFAULT_SANDBOX_MEMORY_MIB
1990}
1991
1992fn empty_secret_value() -> Zeroizing<String> {
1993 Zeroizing::new(String::new())
1994}
1995
1996#[cfg(test)]
2001mod tests {
2002 use super::*;
2003
2004 #[test]
2005 fn casing_is_canonical_with_legacy_aliases() {
2006 assert_eq!(
2008 serde_json::to_string(&RlimitResource::Nofile).unwrap(),
2009 r#""nofile""#
2010 );
2011 assert_eq!(
2012 serde_json::from_str::<RlimitResource>(r#""Nofile""#).unwrap(),
2013 RlimitResource::Nofile
2014 );
2015
2016 assert_eq!(
2018 serde_json::to_string(&SnapshotDestination::Name("snap".into())).unwrap(),
2019 r#"{"name":"snap"}"#
2020 );
2021 assert!(matches!(
2022 serde_json::from_str::<SnapshotDestination>(r#"{"Name":"snap"}"#).unwrap(),
2023 SnapshotDestination::Name(_)
2024 ));
2025 assert!(matches!(
2026 serde_json::from_str::<SnapshotDestination>(r#"{"path":"/tmp/x"}"#).unwrap(),
2027 SnapshotDestination::Path(_)
2028 ));
2029 }
2030
2031 #[test]
2032 fn disk_image_format_from_extension() {
2033 assert_eq!(
2034 DiskImageFormat::from_extension("qcow2"),
2035 Some(DiskImageFormat::Qcow2)
2036 );
2037 assert_eq!(
2038 DiskImageFormat::from_extension("raw"),
2039 Some(DiskImageFormat::Raw)
2040 );
2041 assert_eq!(
2042 DiskImageFormat::from_extension("vmdk"),
2043 Some(DiskImageFormat::Vmdk)
2044 );
2045 assert_eq!(DiskImageFormat::from_extension("ext4"), None);
2046 assert_eq!(DiskImageFormat::from_extension(""), None);
2047 }
2048
2049 #[test]
2050 fn sandbox_resources_deserialize_legacy_capacity_from_effective_values() {
2051 let resources: SandboxResources =
2052 serde_json::from_str(r#"{"vcpus":4,"memory_mib":2048}"#).unwrap();
2053
2054 assert_eq!(resources.vcpus, 4);
2055 assert_eq!(resources.max_vcpus, 4);
2056 assert_eq!(resources.memory_mib, 2048);
2057 assert_eq!(resources.max_memory_mib, 2048);
2058 }
2059
2060 #[test]
2061 fn disk_image_format_display_roundtrip() {
2062 for format in [
2063 DiskImageFormat::Qcow2,
2064 DiskImageFormat::Raw,
2065 DiskImageFormat::Vmdk,
2066 ] {
2067 let rendered = format.to_string();
2068 let parsed: DiskImageFormat = rendered.parse().unwrap();
2069 assert_eq!(parsed, format);
2070 }
2071 }
2072
2073 #[test]
2074 fn disk_image_format_from_str_unknown() {
2075 assert!("ext4".parse::<DiskImageFormat>().is_err());
2076 }
2077
2078 #[test]
2079 fn log_source_effective_uses_default_user_program_sources() {
2080 assert_eq!(
2081 LogSource::effective(&[]),
2082 vec![LogSource::Stdout, LogSource::Stderr, LogSource::Output]
2083 );
2084 }
2085
2086 #[test]
2087 fn log_source_effective_sorts_and_deduplicates_requested_sources() {
2088 assert_eq!(
2089 LogSource::effective(&[LogSource::System, LogSource::Stdout, LogSource::System]),
2090 vec![LogSource::Stdout, LogSource::System]
2091 );
2092 }
2093
2094 #[test]
2095 fn rlimit_resource_parses_case_insensitively() {
2096 assert_eq!(
2097 RlimitResource::try_from("NOFILE").unwrap(),
2098 RlimitResource::Nofile
2099 );
2100 assert!(RlimitResource::try_from("bogus").is_err());
2101 }
2102
2103 #[test]
2104 fn sandbox_policy_serde_roundtrip() {
2105 let policy = SandboxPolicy {
2106 ephemeral: true,
2107 max_duration_secs: Some(3600),
2108 idle_timeout_secs: Some(120),
2109 };
2110
2111 let json = serde_json::to_string(&policy).unwrap();
2112 let decoded: SandboxPolicy = serde_json::from_str(&json).unwrap();
2113
2114 assert!(decoded.ephemeral);
2115 assert_eq!(decoded.max_duration_secs, Some(3600));
2116 assert_eq!(decoded.idle_timeout_secs, Some(120));
2117 }
2118
2119 #[test]
2120 fn sandbox_policy_defaults_to_persistent() {
2121 assert!(!SandboxPolicy::default().ephemeral);
2122 }
2123
2124 #[test]
2125 fn sandbox_policy_deserializes_missing_ephemeral_as_persistent() {
2126 let decoded: SandboxPolicy =
2129 serde_json::from_str(r#"{"max_duration_secs":60,"idle_timeout_secs":null}"#).unwrap();
2130 assert!(!decoded.ephemeral);
2131 assert_eq!(decoded.max_duration_secs, Some(60));
2132 }
2133
2134 #[test]
2135 fn sandbox_spec_default_uses_static_resource_defaults() {
2136 let spec = SandboxSpec::default();
2137
2138 assert_eq!(spec.resources.vcpus, DEFAULT_SANDBOX_VCPUS);
2139 assert_eq!(spec.resources.memory_mib, DEFAULT_SANDBOX_MEMORY_MIB);
2140 assert_eq!(
2141 spec.runtime.metrics_sample_interval_ms,
2142 Some(DEFAULT_METRICS_SAMPLE_INTERVAL_MS)
2143 );
2144 }
2145
2146 #[test]
2147 fn sandbox_log_level_roundtrips_lowercase_values() {
2148 for (input, expected) in [
2149 ("error", SandboxLogLevel::Error),
2150 ("warn", SandboxLogLevel::Warn),
2151 ("info", SandboxLogLevel::Info),
2152 ("debug", SandboxLogLevel::Debug),
2153 ("trace", SandboxLogLevel::Trace),
2154 ] {
2155 let parsed: SandboxLogLevel = input.parse().unwrap();
2156 assert_eq!(parsed, expected);
2157 assert_eq!(parsed.as_str(), input);
2158 }
2159 }
2160}
2161
2162#[cfg(test)]
2163mod secret_tests {
2164 use super::*;
2165
2166 fn valid_secret() -> SecretEntry {
2167 SecretEntry {
2168 env_var: "API_KEY".into(),
2169 value: "secret".to_string().into(),
2170 source: None,
2171 placeholder: "$MSB_API_KEY".into(),
2172 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
2173 injection: SecretInjection::default(),
2174 on_violation: None,
2175 require_tls_identity: true,
2176 }
2177 }
2178
2179 #[test]
2180 fn exact_host_match() {
2181 let p = HostPattern::Exact("api.openai.com".into());
2182 assert!(p.matches("api.openai.com"));
2183 assert!(p.matches("API.OpenAI.com"));
2184 assert!(!p.matches("evil.com"));
2185 }
2186
2187 #[test]
2188 fn wildcard_host_match() {
2189 let p = HostPattern::Wildcard("*.openai.com".into());
2190 assert!(p.matches("api.openai.com"));
2191 assert!(p.matches("openai.com"));
2192 assert!(!p.matches("evil.com"));
2193 }
2194
2195 #[test]
2196 fn any_host_match() {
2197 assert!(HostPattern::Any.matches("anything.com"));
2198 }
2199
2200 #[test]
2201 fn default_injection_scopes() {
2202 let inj = SecretInjection::default();
2203 assert!(inj.headers);
2204 assert!(inj.basic_auth);
2205 assert!(!inj.query_params);
2206 assert!(!inj.body);
2207 }
2208
2209 #[test]
2210 fn default_require_tls_identity_when_deserialized() {
2211 let entry: SecretEntry = serde_json::from_str(
2212 r#"{"env_var":"K","value":"v","placeholder":"$K","allowed_hosts":[{"exact":"h"}]}"#,
2213 )
2214 .unwrap();
2215 assert!(entry.require_tls_identity);
2216 }
2217
2218 #[test]
2219 fn secret_validation_accepts_linux_environment_name_shape() {
2220 let mut entry = valid_secret();
2221 entry.env_var = "1TOKEN.with-dashes".into();
2222 assert!(entry.validate(0).is_ok());
2223 }
2224
2225 #[test]
2226 fn secret_validation_rejects_invalid_env_var_names() {
2227 let cases = [
2228 ("", SecretConfigError::EmptyEnvVar { secret_index: 0 }),
2229 (
2230 "API=KEY",
2231 SecretConfigError::EnvVarContainsEquals { secret_index: 0 },
2232 ),
2233 (
2234 "API\0KEY",
2235 SecretConfigError::EnvVarContainsNul { secret_index: 0 },
2236 ),
2237 ];
2238 for (env_var, expected) in cases {
2239 let mut entry = valid_secret();
2240 entry.env_var = env_var.into();
2241 assert_eq!(entry.validate(0), Err(expected));
2242 }
2243 }
2244
2245 #[test]
2246 fn secret_validation_rejects_missing_allowed_hosts() {
2247 let mut entry = valid_secret();
2248 entry.allowed_hosts.clear();
2249 assert_eq!(
2250 entry.validate(0),
2251 Err(SecretConfigError::MissingAllowedHosts { secret_index: 0 })
2252 );
2253 }
2254
2255 #[test]
2256 fn secret_validation_rejects_invalid_placeholders() {
2257 let too_long = "x".repeat(MAX_SECRET_PLACEHOLDER_BYTES + 1);
2258 let cases = [
2259 ("", SecretConfigError::EmptyPlaceholder { secret_index: 0 }),
2260 (
2261 too_long.as_str(),
2262 SecretConfigError::PlaceholderTooLong {
2263 secret_index: 0,
2264 actual_bytes: MAX_SECRET_PLACEHOLDER_BYTES + 1,
2265 max_bytes: MAX_SECRET_PLACEHOLDER_BYTES,
2266 },
2267 ),
2268 (
2269 "abc\0def",
2270 SecretConfigError::PlaceholderContainsNul { secret_index: 0 },
2271 ),
2272 (
2273 "abc\rdef",
2274 SecretConfigError::PlaceholderContainsLineBreak { secret_index: 0 },
2275 ),
2276 (
2277 "abc\ndef",
2278 SecretConfigError::PlaceholderContainsLineBreak { secret_index: 0 },
2279 ),
2280 ];
2281 for (placeholder, expected) in cases {
2282 let mut entry = valid_secret();
2283 entry.placeholder = placeholder.into();
2284 assert_eq!(entry.validate(0), Err(expected));
2285 }
2286 }
2287
2288 #[test]
2289 fn violation_action_serializes_with_sdk_casing() {
2290 let action = ViolationAction::Passthrough(vec![
2291 HostPattern::Exact("api.anthropic.com".into()),
2292 HostPattern::Wildcard("*.anthropic.com".into()),
2293 HostPattern::Any,
2294 ]);
2295 assert_eq!(
2296 serde_json::to_string(&action).unwrap(),
2297 r#"{"passthrough":[{"exact":"api.anthropic.com"},{"wildcard":"*.anthropic.com"},"any"]}"#
2298 );
2299 assert_eq!(
2300 serde_json::to_string(&ViolationAction::BlockAndLog).unwrap(),
2301 r#""block_and_log""#
2302 );
2303 assert_eq!(
2304 serde_json::to_string(&ViolationAction::BlockAndTerminate).unwrap(),
2305 r#""block_and_terminate""#
2306 );
2307 }
2308
2309 #[test]
2310 fn violation_action_accepts_legacy_pascal_case() {
2311 let action: ViolationAction =
2312 serde_json::from_str(r#"{"Passthrough":[{"Exact":"api.anthropic.com"}]}"#).unwrap();
2313 assert_eq!(
2314 action,
2315 ViolationAction::Passthrough(vec![HostPattern::Exact("api.anthropic.com".into())])
2316 );
2317 assert_eq!(
2318 serde_json::from_str::<ViolationAction>(r#""BlockAndTerminate""#).unwrap(),
2319 ViolationAction::BlockAndTerminate
2320 );
2321 }
2322
2323 #[test]
2324 fn secret_entry_debug_redacts_value() {
2325 let mut entry = valid_secret();
2326 entry.value = "uniq-sensitive-12345".to_string().into();
2327 let dbg = format!("{entry:?}");
2328 assert!(dbg.contains("[REDACTED]"));
2329 assert!(!dbg.contains("uniq-sensitive-12345"));
2330 }
2331}
2332
2333#[cfg(test)]
2334mod tls_tests {
2335 use super::*;
2336
2337 #[test]
2338 fn tls_config_defaults() {
2339 let t = TlsConfig::default();
2340 assert!(!t.enabled);
2341 assert_eq!(t.intercepted_ports, vec![443]);
2342 assert!(t.verify_upstream);
2343 assert!(t.block_quic_on_intercept);
2344 assert_eq!(t.cache.capacity, 1000);
2345 assert_eq!(t.cache.validity_hours, 24);
2346 }
2347
2348 #[test]
2349 fn tls_config_round_trips_and_accepts_ca_alias() {
2350 let cfg: TlsConfig = serde_json::from_str(
2352 r#"{"enabled":true,"bypass":["*.internal"],"ca":{"cert_path":"/etc/ca.pem"}}"#,
2353 )
2354 .unwrap();
2355 assert!(cfg.enabled);
2356 assert_eq!(cfg.bypass, vec!["*.internal".to_string()]);
2357 assert_eq!(
2358 cfg.intercept_ca.cert_path.as_deref(),
2359 Some(std::path::Path::new("/etc/ca.pem"))
2360 );
2361 let back: TlsConfig = serde_json::from_str(&serde_json::to_string(&cfg).unwrap()).unwrap();
2362 assert_eq!(back.bypass, cfg.bypass);
2363 }
2364}