1use std::net::IpAddr;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use ipnetwork::{Ipv4Network, Ipv6Network};
10use microsandbox_types::{
11 NetworkRateLimitDirection, NetworkRateLimiterConfig, RateLimiterConfig, ScopedUpstreamCaCert,
12 ScopedVerifyUpstream, TlsConfig, TokenBucketConfig,
13};
14use microsandbox_utils::size::Bytes;
15use zeroize::Zeroizing;
16
17use crate::config::{
18 ConnectionLimit, DnsConfig, InterfaceOverrides, NetworkConfig, PortProtocol, PublishedPort,
19};
20use crate::dns::Nameserver;
21use crate::policy::{BuildError, NetworkPolicy};
22use crate::secrets::config::{
23 HostPattern, SecretEntry, SecretSource, SecretSubstitution, SecretViolationAction,
24};
25
26#[derive(Clone)]
32pub struct NetworkBuilder {
33 config: NetworkConfig,
34 errors: Vec<BuildError>,
35}
36
37pub struct DnsBuilder {
39 config: DnsConfig,
40}
41
42pub struct TlsBuilder {
44 config: TlsConfig,
45}
46
47pub struct SecretBuilder {
57 env_var: Option<String>,
58 value: Option<String>,
59 source: Option<SecretSource>,
60 placeholder: Option<String>,
61 allowed_hosts: Vec<HostPattern>,
62 substitution: SecretSubstitution,
63 passthrough_hosts: Vec<HostPattern>,
64 violation_action: Option<SecretViolationAction>,
65 require_tls_identity: bool,
66}
67
68#[derive(Default)]
77pub struct NetworkRateLimiterBuilder {
78 config: NetworkRateLimiterConfig,
79 errors: Vec<BuildError>,
80}
81
82pub struct RateLimiterBuilder {
93 direction: NetworkRateLimitDirection,
94 bandwidth: Option<TokenBucketConfig>,
95 ops: Option<TokenBucketConfig>,
96 bandwidth_burst: Option<u64>,
97 ops_burst: Option<u64>,
98 refill_error: Option<(&'static str, RefillTimeError)>,
100}
101
102#[derive(Clone, Copy, Debug)]
103enum RefillTimeError {
104 TooShort,
105 Precision,
106 TooLong,
107}
108
109impl NetworkBuilder {
114 pub fn new() -> Self {
116 Self {
117 config: NetworkConfig::default(),
118 errors: Vec::new(),
119 }
120 }
121
122 pub fn from_config(config: NetworkConfig) -> Self {
124 Self {
125 config,
126 errors: Vec::new(),
127 }
128 }
129
130 pub fn enabled(mut self, enabled: bool) -> Self {
132 self.config.enabled = enabled;
133 self
134 }
135
136 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
138 self.port_bind(
139 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
140 host_port,
141 guest_port,
142 )
143 }
144
145 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
147 self.port_udp_bind(
148 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
149 host_port,
150 guest_port,
151 )
152 }
153
154 pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
156 self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
157 }
158
159 pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
161 self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
162 }
163
164 fn add_port(
165 mut self,
166 host_bind: IpAddr,
167 host_port: u16,
168 guest_port: u16,
169 protocol: PortProtocol,
170 ) -> Self {
171 self.config.ports.push(PublishedPort {
172 host_port,
173 guest_port,
174 protocol,
175 host_bind,
176 });
177 self
178 }
179
180 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
182 self.config.policy = policy;
183 self
184 }
185
186 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
195 self.config.dns = f(DnsBuilder::new()).build();
196 self
197 }
198
199 #[doc(hidden)]
201 pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
202 self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
203 self
204 }
205
206 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
208 self.config.tls = f(TlsBuilder::new()).build();
209 self
210 }
211
212 #[doc(hidden)]
214 pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
215 self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
216 self
217 }
218
219 pub fn strict(mut self, enabled: bool) -> Self {
221 self.config.strict = enabled;
222 self
223 }
224
225 pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
235 self.secret_entry(f(SecretBuilder::new()).build())
236 }
237
238 pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
240 self.config.secrets.secrets.push(entry);
241 self
242 }
243
244 pub fn secret_env(
246 mut self,
247 env_var: impl Into<String>,
248 value: impl Into<String>,
249 placeholder: impl Into<String>,
250 allowed_host: impl Into<String>,
251 ) -> Self {
252 self.config.secrets.secrets.push(SecretEntry {
253 env_var: env_var.into(),
254 value: Zeroizing::new(value.into()),
255 source: None,
256 placeholder: placeholder.into(),
257 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
258 substitution: SecretSubstitution::default(),
259 passthrough_hosts: Vec::new(),
260 violation_action: None,
261 require_tls_identity: true,
262 });
263 self
264 }
265
266 pub fn secret_violation_action(mut self, action: SecretViolationAction) -> Self {
268 self.config.secrets.violation_action = action;
269 self
270 }
271
272 #[deprecated(note = "use max_tcp_connections instead")]
274 pub fn max_connections(self, max: usize) -> Self {
275 self.max_tcp_connections(max)
276 }
277
278 pub fn max_tcp_connections(mut self, max: usize) -> Self {
280 self.config.max_tcp_connections = Some(ConnectionLimit::from(max));
281 self
282 }
283
284 pub fn max_udp_connections(mut self, max: usize) -> Self {
286 self.config.max_udp_connections = Some(ConnectionLimit::from(max));
287 self
288 }
289
290 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
292 self.config.interface = overrides;
293 self
294 }
295
296 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
300 if pool.prefix() > 30 {
301 self.errors.push(BuildError::InvalidIpv4Pool {
302 raw: pool.to_string(),
303 });
304 } else {
305 self.config.interface.ipv4_pool = Some(pool);
306 }
307 self
308 }
309
310 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
314 if pool.prefix() > 64 {
315 self.errors.push(BuildError::InvalidIpv6Pool {
316 raw: pool.to_string(),
317 });
318 } else {
319 self.config.interface.ipv6_pool = Some(pool);
320 }
321 self
322 }
323
324 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
330 self.config.trust_host_cas = enabled;
331 self
332 }
333
334 pub fn rate_limiter(
345 mut self,
346 f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
347 ) -> Self {
348 match f(NetworkRateLimiterBuilder::new()).build() {
349 Ok(limiter) => self.config.rate_limiter = Some(limiter),
350 Err(err) => self.errors.push(err),
351 }
352 self
353 }
354
355 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
361 if let Some(err) = self.errors.drain(..).next() {
362 return Err(err);
363 }
364 if self.config.tls.enabled
365 && (self.config.tls.intercept_ca.cert_path.is_some()
366 != self.config.tls.intercept_ca.key_path.is_some())
367 {
368 return Err(BuildError::IncompleteInterceptCaConfig);
369 }
370 self.config.secrets.validate()?;
371 Ok(self.config)
372 }
373}
374
375impl DnsBuilder {
376 pub fn new() -> Self {
378 Self {
379 config: DnsConfig::default(),
380 }
381 }
382
383 fn from_config(config: DnsConfig) -> Self {
384 Self { config }
385 }
386
387 pub fn rebind_protection(mut self, enabled: bool) -> Self {
389 self.config.rebind_protection = enabled;
390 self
391 }
392
393 pub fn nameservers<I>(mut self, nameservers: I) -> Self
400 where
401 I: IntoIterator,
402 I::Item: Into<Nameserver>,
403 {
404 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
405 self
406 }
407
408 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
410 self.config.query_timeout_ms = ms;
411 self
412 }
413
414 pub fn build(self) -> DnsConfig {
416 self.config
417 }
418}
419
420impl Default for DnsBuilder {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426impl TlsBuilder {
427 pub fn new() -> Self {
429 Self {
430 config: TlsConfig {
431 enabled: true,
432 ..TlsConfig::default()
433 },
434 }
435 }
436
437 fn from_config(config: TlsConfig) -> Self {
438 Self { config }
439 }
440
441 pub fn enabled(mut self, enabled: bool) -> Self {
443 self.config.enabled = enabled;
444 self
445 }
446
447 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
449 self.config.bypass.push(pattern.into());
450 self
451 }
452
453 pub fn verify_upstream(mut self, verify: bool) -> Self {
455 self.config.verify_upstream = verify;
456 self
457 }
458
459 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
465 self.config
466 .scoped_verify_upstream
467 .push(ScopedVerifyUpstream {
468 pattern: pattern.into(),
469 verify,
470 });
471 self
472 }
473
474 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
476 self.config.intercepted_ports = ports;
477 self
478 }
479
480 pub fn block_quic(mut self, block: bool) -> Self {
482 self.config.block_quic_on_intercept = block;
483 self
484 }
485
486 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
491 self.config.upstream_ca_cert.push(path.into());
492 self
493 }
494
495 pub fn upstream_ca_cert_for(
502 mut self,
503 pattern: impl Into<String>,
504 path: impl Into<PathBuf>,
505 ) -> Self {
506 self.config
507 .scoped_upstream_ca_cert
508 .push(ScopedUpstreamCaCert {
509 pattern: pattern.into(),
510 path: path.into(),
511 });
512 self
513 }
514
515 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
517 self.config.intercept_ca.cert_path = Some(path.into());
518 self
519 }
520
521 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
523 self.config.intercept_ca.key_path = Some(path.into());
524 self
525 }
526
527 pub fn build(self) -> TlsConfig {
529 self.config
530 }
531}
532
533impl SecretBuilder {
534 pub fn new() -> Self {
536 Self {
537 env_var: None,
538 value: None,
539 source: None,
540 placeholder: None,
541 allowed_hosts: Vec::new(),
542 substitution: SecretSubstitution::default(),
543 passthrough_hosts: Vec::new(),
544 violation_action: None,
545 require_tls_identity: true,
546 }
547 }
548
549 pub fn env(mut self, var: impl Into<String>) -> Self {
554 self.env_var = Some(var.into());
555 self
556 }
557
558 pub fn value(mut self, value: impl Into<String>) -> Self {
564 self.value = Some(value.into());
565 self
566 }
567
568 pub fn source(mut self, source: SecretSource) -> Self {
575 self.source = Some(source);
576 self
577 }
578
579 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
585 self.placeholder = Some(placeholder.into());
586 self
587 }
588
589 pub fn allow(mut self, host: impl AsRef<str>) -> Self {
594 let host = host.as_ref();
595 assert!(
596 host != "*",
597 "SecretBuilder: use .allow_any_host_dangerous(true) for an explicit any-host secret"
598 );
599 self.allowed_hosts.push(HostPattern::parse(host));
600 self
601 }
602
603 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
606 if i_understand_the_risk {
607 self.allowed_hosts.push(HostPattern::Any);
608 }
609 self
610 }
611
612 pub fn allow_placeholder_for(mut self, host: impl AsRef<str>) -> Self {
617 self.passthrough_hosts
618 .push(HostPattern::parse(host.as_ref()));
619 self
620 }
621
622 #[deprecated(note = "use allow_placeholder_for instead")]
624 pub fn allow_passthrough_for(self, host: impl AsRef<str>) -> Self {
625 self.allow_placeholder_for(host)
626 }
627
628 pub fn violation_action(mut self, action: SecretViolationAction) -> Self {
630 self.violation_action = Some(action);
631 self
632 }
633
634 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
636 self.require_tls_identity = enabled;
637 self
638 }
639
640 pub fn substitute_in_headers(mut self, enabled: bool) -> Self {
642 self.substitution.headers = enabled;
643 self
644 }
645
646 pub fn substitute_in_query(mut self, enabled: bool) -> Self {
648 self.substitution.query = enabled;
649 self
650 }
651
652 pub fn substitute_in_body(mut self, enabled: bool) -> Self {
659 self.substitution.body = enabled;
660 self
661 }
662
663 pub fn build(self) -> SecretEntry {
673 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
674 assert!(
675 self.value.is_some() ^ self.source.is_some(),
676 "SecretBuilder: exactly one of .value() or .source() is required"
677 );
678 assert!(
679 !self.allowed_hosts.is_empty(),
680 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
681 );
682 let placeholder = self
683 .placeholder
684 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
685
686 SecretEntry {
687 env_var,
688 value: Zeroizing::new(self.value.unwrap_or_default()),
689 source: self.source,
690 placeholder,
691 allowed_hosts: self.allowed_hosts,
692 substitution: self.substitution,
693 passthrough_hosts: self.passthrough_hosts,
694 violation_action: self.violation_action,
695 require_tls_identity: self.require_tls_identity,
696 }
697 }
698}
699
700impl NetworkRateLimiterBuilder {
701 fn new() -> Self {
702 Self::default()
703 }
704
705 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
707 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
708 Ok(limiter) => self.config.egress = Some(limiter),
709 Err(err) => self.errors.push(err),
710 }
711 self
712 }
713
714 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
716 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
717 Ok(limiter) => self.config.ingress = Some(limiter),
718 Err(err) => self.errors.push(err),
719 }
720 self
721 }
722
723 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
725 if let Some(error) = self.errors.drain(..).next() {
726 return Err(error);
727 }
728 if self.config.egress.is_none() && self.config.ingress.is_none() {
729 return Err(BuildError::EmptyNetworkRateLimiter);
730 }
731 Ok(self.config)
732 }
733}
734
735impl RateLimiterBuilder {
736 fn new(direction: NetworkRateLimitDirection) -> Self {
737 Self {
738 direction,
739 bandwidth: None,
740 ops: None,
741 bandwidth_burst: None,
742 ops_burst: None,
743 refill_error: None,
744 }
745 }
746
747 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
756 match refill_time_ms(refill_time) {
757 Ok(refill_time_ms) => {
758 self.bandwidth = Some(TokenBucketConfig {
759 size: size.into().as_u64(),
760 refill_time_ms,
761 one_time_burst: 0,
762 });
763 }
764 Err(error) => {
765 self.refill_error.get_or_insert(("bandwidth", error));
766 }
767 }
768 self
769 }
770
771 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
774 self.bandwidth_burst = Some(burst.into().as_u64());
775 self
776 }
777
778 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
787 match refill_time_ms(refill_time) {
788 Ok(refill_time_ms) => {
789 self.ops = Some(TokenBucketConfig {
790 size: count,
791 refill_time_ms,
792 one_time_burst: 0,
793 });
794 }
795 Err(error) => {
796 self.refill_error.get_or_insert(("ops", error));
797 }
798 }
799 self
800 }
801
802 pub fn ops_burst(mut self, count: u64) -> Self {
805 self.ops_burst = Some(count);
806 self
807 }
808
809 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
811 let direction = self.direction;
812 if let Some((bucket, error)) = self.refill_error {
813 return Err(match error {
814 RefillTimeError::TooShort => {
815 BuildError::RateLimitRefillTooShort { direction, bucket }
816 }
817 RefillTimeError::Precision => {
818 BuildError::RateLimitRefillPrecision { direction, bucket }
819 }
820 RefillTimeError::TooLong => {
821 BuildError::RateLimitRefillTooLong { direction, bucket }
822 }
823 });
824 }
825
826 let mut config = RateLimiterConfig {
827 bandwidth: self.bandwidth,
828 ops: self.ops,
829 };
830 if let Some(burst) = self.bandwidth_burst {
831 let Some(bandwidth) = &mut config.bandwidth else {
832 return Err(BuildError::RateLimitBurstWithoutBucket {
833 direction,
834 bucket: "bandwidth",
835 });
836 };
837 bandwidth.one_time_burst = burst;
838 }
839 if let Some(burst) = self.ops_burst {
840 let Some(ops) = &mut config.ops else {
841 return Err(BuildError::RateLimitBurstWithoutBucket {
842 direction,
843 bucket: "ops",
844 });
845 };
846 ops.one_time_burst = burst;
847 }
848
849 config
850 .validate()
851 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
852 Ok(config)
853 }
854}
855
856fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
862 if refill_time < Duration::from_millis(1) {
863 return Err(RefillTimeError::TooShort);
864 }
865 let refill_time_ms =
866 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
867 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
868 return Err(RefillTimeError::Precision);
869 }
870 Ok(refill_time_ms)
871}
872
873impl Default for NetworkBuilder {
878 fn default() -> Self {
879 Self::new()
880 }
881}
882
883impl Default for TlsBuilder {
884 fn default() -> Self {
885 Self::new()
886 }
887}
888
889impl Default for SecretBuilder {
890 fn default() -> Self {
891 Self::new()
892 }
893}
894#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 #[allow(deprecated)]
904 fn deprecated_tcp_builder_delegates_to_the_new_name() {
905 let canonical_last = NetworkBuilder::new()
906 .max_connections(0)
907 .max_tcp_connections(64)
908 .build()
909 .unwrap();
910 assert_eq!(
911 canonical_last.max_tcp_connections,
912 Some(ConnectionLimit::from(64))
913 );
914 let legacy_last = NetworkBuilder::new()
915 .max_tcp_connections(64)
916 .max_connections(0)
917 .build()
918 .unwrap();
919 assert_eq!(
920 legacy_last.max_tcp_connections,
921 Some(ConnectionLimit::Unlimited)
922 );
923 let config = NetworkBuilder::new()
924 .max_connections(0)
925 .max_udp_connections(7)
926 .build()
927 .unwrap();
928 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
929 assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
930 assert!(
931 NetworkBuilder::new()
932 .max_tcp_connections(1)
933 .max_tcp_connections(2)
934 .build()
935 .is_ok()
936 );
937 }
938
939 #[test]
941 fn network_builder_happy_path_returns_config() {
942 let cfg = NetworkBuilder::new()
943 .dns(|d| d.rebind_protection(false))
944 .build()
945 .unwrap();
946 assert!(!cfg.dns.rebind_protection);
947 }
948
949 #[test]
950 fn network_builder_preserves_explicit_large_caps() {
951 for limit in [10000, usize::MAX] {
952 let config = NetworkBuilder::new()
953 .max_tcp_connections(limit)
954 .build()
955 .unwrap();
956 assert_eq!(
957 config.max_tcp_connections,
958 Some(ConnectionLimit::from(limit))
959 );
960 }
961 }
962
963 #[test]
964 fn network_builder_rejects_incomplete_intercept_ca_config() {
965 let err = NetworkBuilder::new()
966 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
967 .build()
968 .unwrap_err();
969
970 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
971 }
972
973 #[test]
974 fn port_bind_sets_host_bind() {
975 let bind = "0.0.0.0".parse().unwrap();
976 let cfg = NetworkBuilder::new()
977 .port_bind(bind, 8080, 80)
978 .port_udp_bind(bind, 5353, 53)
979 .build()
980 .unwrap();
981
982 assert_eq!(cfg.ports[0].host_bind, bind);
983 assert_eq!(cfg.ports[0].host_port, 8080);
984 assert_eq!(cfg.ports[0].guest_port, 80);
985 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
986 assert_eq!(cfg.ports[1].host_bind, bind);
987 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
988 }
989
990 #[test]
991 fn port_helpers_default_to_loopback() {
992 let cfg = NetworkBuilder::new()
993 .port(8080, 80)
994 .port_udp(5353, 53)
995 .build()
996 .unwrap();
997
998 assert_eq!(
999 cfg.ports[0].host_bind,
1000 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1001 );
1002 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1003 assert_eq!(
1004 cfg.ports[1].host_bind,
1005 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1006 );
1007 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1008 }
1009
1010 #[test]
1011 fn outbound_proxy_defaults_to_none() {
1012 let cfg = NetworkBuilder::new().build().unwrap();
1013 assert_eq!(cfg.outbound_proxy, None);
1014 }
1015
1016 #[test]
1017 fn network_builder_sets_strict_mode() {
1018 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1019 assert!(cfg.strict);
1020 }
1021
1022 #[test]
1023 fn network_builder_sets_global_violation_action() {
1024 let cfg = NetworkBuilder::new()
1025 .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1026 .build()
1027 .unwrap();
1028
1029 assert_eq!(
1030 cfg.secrets.violation_action,
1031 SecretViolationAction::BlockAndTerminate
1032 );
1033 }
1034
1035 #[test]
1036 #[allow(deprecated)] fn secret_builder_sets_passthrough_and_violation_policies() {
1038 let secret = SecretBuilder::new()
1039 .env("TOKEN")
1040 .value("secret-value")
1041 .allow("api.github.com")
1042 .allow_placeholder_for("api.anthropic.com")
1043 .allow_passthrough_for("*.anthropic.com")
1044 .violation_action(SecretViolationAction::BlockAndTerminate)
1045 .build();
1046
1047 assert_eq!(
1048 secret.violation_action,
1049 Some(SecretViolationAction::BlockAndTerminate),
1050 );
1051 assert_eq!(
1052 secret.passthrough_hosts,
1053 vec![
1054 HostPattern::Exact("api.anthropic.com".into()),
1055 HostPattern::Wildcard("*.anthropic.com".into()),
1056 ],
1057 );
1058 }
1059
1060 #[test]
1061 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1062 fn secret_builder_rejects_empty_allowed_hosts() {
1063 let _ = SecretBuilder::new()
1064 .env("TOKEN")
1065 .value("secret-value")
1066 .build();
1067 }
1068
1069 #[test]
1070 fn secret_builder_source_yields_reference_and_empty_value() {
1071 let secret = SecretBuilder::new()
1072 .env("API_KEY")
1073 .source(SecretSource::Env {
1074 var: "HOST_API_KEY".into(),
1075 })
1076 .allow("api.example.com")
1077 .build();
1078
1079 assert!(secret.value.is_empty());
1080 assert_eq!(
1081 secret.source,
1082 Some(SecretSource::Env {
1083 var: "HOST_API_KEY".into()
1084 })
1085 );
1086
1087 let json = serde_json::to_string(&secret).unwrap();
1089 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1090 }
1091
1092 #[test]
1093 #[should_panic(expected = "exactly one of .value() or .source()")]
1094 fn secret_builder_rejects_both_value_and_source() {
1095 let _ = SecretBuilder::new()
1096 .env("API_KEY")
1097 .value("inline")
1098 .source(SecretSource::Env {
1099 var: "HOST_API_KEY".into(),
1100 })
1101 .allow("api.example.com")
1102 .build();
1103 }
1104
1105 #[test]
1106 fn network_builder_rejects_invalid_secret_config() {
1107 let err = NetworkBuilder::new()
1108 .secret_entry(SecretEntry {
1109 env_var: "API=KEY".into(),
1110 value: Zeroizing::new("secret-value".into()),
1111 source: None,
1112 placeholder: "$MSB_API_KEY".into(),
1113 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1114 substitution: SecretSubstitution::default(),
1115 passthrough_hosts: Vec::new(),
1116 violation_action: None,
1117 require_tls_identity: true,
1118 })
1119 .build()
1120 .unwrap_err();
1121
1122 assert!(err.to_string().contains("env_var must not contain `=`"));
1123 }
1124
1125 #[test]
1126 fn rate_limiter_builder_sets_buckets_and_bursts() {
1127 use microsandbox_utils::size::SizeExt;
1128
1129 let cfg = NetworkBuilder::new()
1130 .rate_limiter(|r| {
1131 r.egress(|r| {
1132 r.bandwidth(1.mib(), Duration::from_secs(1))
1133 .bandwidth_burst(512.kib())
1134 .ops(1_000, Duration::from_secs(1))
1135 .ops_burst(500)
1136 })
1137 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1138 })
1139 .build()
1140 .unwrap();
1141
1142 let rate_limiter = cfg.rate_limiter.unwrap();
1143 let egress = rate_limiter.egress.unwrap();
1144 let bandwidth = egress.bandwidth.unwrap();
1145 assert_eq!(bandwidth.size, 1024 * 1024);
1146 assert_eq!(bandwidth.refill_time_ms, 1000);
1147 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1148 let ops = egress.ops.unwrap();
1149 assert_eq!(ops.size, 1_000);
1150 assert_eq!(ops.refill_time_ms, 1000);
1151 assert_eq!(ops.one_time_burst, 500);
1152
1153 let ingress = rate_limiter.ingress.unwrap();
1154 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1155 assert!(ingress.ops.is_none());
1156 }
1157
1158 #[test]
1159 fn rate_limiters_default_to_unlimited() {
1160 let cfg = NetworkBuilder::new().build().unwrap();
1161 assert!(cfg.rate_limiter.is_none());
1162 }
1163
1164 #[test]
1165 fn rate_limiter_builder_rejects_empty_limiter() {
1166 let err = NetworkBuilder::new()
1167 .rate_limiter(|r| r.egress(|r| r))
1168 .build()
1169 .unwrap_err();
1170 assert_eq!(
1171 err.to_string(),
1172 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1173 );
1174 }
1175
1176 #[test]
1177 fn network_rate_limiter_builder_rejects_missing_directions() {
1178 let err = NetworkBuilder::new()
1179 .rate_limiter(|r| r)
1180 .build()
1181 .unwrap_err();
1182 assert_eq!(
1183 err.to_string(),
1184 "rate limiter must configure at least one of egress or ingress"
1185 );
1186 }
1187
1188 #[test]
1189 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1190 let err = NetworkBuilder::new()
1191 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1192 .build()
1193 .unwrap_err();
1194 assert_eq!(
1195 err.to_string(),
1196 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1197 );
1198
1199 let err = NetworkBuilder::new()
1200 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1201 .build()
1202 .unwrap_err();
1203 assert_eq!(
1204 err.to_string(),
1205 "egress rate limiter: ops refill interval must be at least one millisecond"
1206 );
1207
1208 let err = NetworkBuilder::new()
1209 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1210 .build()
1211 .unwrap_err();
1212 assert_eq!(
1213 err.to_string(),
1214 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1215 );
1216 }
1217
1218 #[test]
1219 fn rate_limiter_builder_rejects_burst_without_bucket() {
1220 use microsandbox_utils::size::SizeExt;
1221
1222 let err = NetworkBuilder::new()
1223 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1224 .build()
1225 .unwrap_err();
1226 assert_eq!(
1227 err.to_string(),
1228 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1229 );
1230
1231 let err = NetworkBuilder::new()
1232 .rate_limiter(|r| {
1233 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1234 })
1235 .build()
1236 .unwrap_err();
1237 assert_eq!(
1238 err.to_string(),
1239 "ingress rate limiter: ops_burst requires the ops bucket"
1240 );
1241 }
1242
1243 #[test]
1244 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1245 let err = NetworkBuilder::new()
1246 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1247 .build()
1248 .unwrap_err();
1249 assert_eq!(
1250 err.to_string(),
1251 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1252 );
1253 }
1254}