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_passthrough_for(mut self, host: impl AsRef<str>) -> Self {
616 self.passthrough_hosts
617 .push(HostPattern::parse(host.as_ref()));
618 self
619 }
620
621 pub fn violation_action(mut self, action: SecretViolationAction) -> Self {
623 self.violation_action = Some(action);
624 self
625 }
626
627 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
629 self.require_tls_identity = enabled;
630 self
631 }
632
633 pub fn substitute_in_headers(mut self, enabled: bool) -> Self {
635 self.substitution.headers = enabled;
636 self
637 }
638
639 pub fn substitute_in_query(mut self, enabled: bool) -> Self {
641 self.substitution.query = enabled;
642 self
643 }
644
645 pub fn substitute_in_body(mut self, enabled: bool) -> Self {
652 self.substitution.body = enabled;
653 self
654 }
655
656 pub fn build(self) -> SecretEntry {
666 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
667 assert!(
668 self.value.is_some() ^ self.source.is_some(),
669 "SecretBuilder: exactly one of .value() or .source() is required"
670 );
671 assert!(
672 !self.allowed_hosts.is_empty(),
673 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
674 );
675 let placeholder = self
676 .placeholder
677 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
678
679 SecretEntry {
680 env_var,
681 value: Zeroizing::new(self.value.unwrap_or_default()),
682 source: self.source,
683 placeholder,
684 allowed_hosts: self.allowed_hosts,
685 substitution: self.substitution,
686 passthrough_hosts: self.passthrough_hosts,
687 violation_action: self.violation_action,
688 require_tls_identity: self.require_tls_identity,
689 }
690 }
691}
692
693impl NetworkRateLimiterBuilder {
694 fn new() -> Self {
695 Self::default()
696 }
697
698 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
700 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
701 Ok(limiter) => self.config.egress = Some(limiter),
702 Err(err) => self.errors.push(err),
703 }
704 self
705 }
706
707 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
709 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
710 Ok(limiter) => self.config.ingress = Some(limiter),
711 Err(err) => self.errors.push(err),
712 }
713 self
714 }
715
716 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
718 if let Some(error) = self.errors.drain(..).next() {
719 return Err(error);
720 }
721 if self.config.egress.is_none() && self.config.ingress.is_none() {
722 return Err(BuildError::EmptyNetworkRateLimiter);
723 }
724 Ok(self.config)
725 }
726}
727
728impl RateLimiterBuilder {
729 fn new(direction: NetworkRateLimitDirection) -> Self {
730 Self {
731 direction,
732 bandwidth: None,
733 ops: None,
734 bandwidth_burst: None,
735 ops_burst: None,
736 refill_error: None,
737 }
738 }
739
740 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
749 match refill_time_ms(refill_time) {
750 Ok(refill_time_ms) => {
751 self.bandwidth = Some(TokenBucketConfig {
752 size: size.into().as_u64(),
753 refill_time_ms,
754 one_time_burst: 0,
755 });
756 }
757 Err(error) => {
758 self.refill_error.get_or_insert(("bandwidth", error));
759 }
760 }
761 self
762 }
763
764 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
767 self.bandwidth_burst = Some(burst.into().as_u64());
768 self
769 }
770
771 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
780 match refill_time_ms(refill_time) {
781 Ok(refill_time_ms) => {
782 self.ops = Some(TokenBucketConfig {
783 size: count,
784 refill_time_ms,
785 one_time_burst: 0,
786 });
787 }
788 Err(error) => {
789 self.refill_error.get_or_insert(("ops", error));
790 }
791 }
792 self
793 }
794
795 pub fn ops_burst(mut self, count: u64) -> Self {
798 self.ops_burst = Some(count);
799 self
800 }
801
802 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
804 let direction = self.direction;
805 if let Some((bucket, error)) = self.refill_error {
806 return Err(match error {
807 RefillTimeError::TooShort => {
808 BuildError::RateLimitRefillTooShort { direction, bucket }
809 }
810 RefillTimeError::Precision => {
811 BuildError::RateLimitRefillPrecision { direction, bucket }
812 }
813 RefillTimeError::TooLong => {
814 BuildError::RateLimitRefillTooLong { direction, bucket }
815 }
816 });
817 }
818
819 let mut config = RateLimiterConfig {
820 bandwidth: self.bandwidth,
821 ops: self.ops,
822 };
823 if let Some(burst) = self.bandwidth_burst {
824 let Some(bandwidth) = &mut config.bandwidth else {
825 return Err(BuildError::RateLimitBurstWithoutBucket {
826 direction,
827 bucket: "bandwidth",
828 });
829 };
830 bandwidth.one_time_burst = burst;
831 }
832 if let Some(burst) = self.ops_burst {
833 let Some(ops) = &mut config.ops else {
834 return Err(BuildError::RateLimitBurstWithoutBucket {
835 direction,
836 bucket: "ops",
837 });
838 };
839 ops.one_time_burst = burst;
840 }
841
842 config
843 .validate()
844 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
845 Ok(config)
846 }
847}
848
849fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
855 if refill_time < Duration::from_millis(1) {
856 return Err(RefillTimeError::TooShort);
857 }
858 let refill_time_ms =
859 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
860 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
861 return Err(RefillTimeError::Precision);
862 }
863 Ok(refill_time_ms)
864}
865
866impl Default for NetworkBuilder {
871 fn default() -> Self {
872 Self::new()
873 }
874}
875
876impl Default for TlsBuilder {
877 fn default() -> Self {
878 Self::new()
879 }
880}
881
882impl Default for SecretBuilder {
883 fn default() -> Self {
884 Self::new()
885 }
886}
887#[cfg(test)]
892mod tests {
893 use super::*;
894
895 #[test]
896 #[allow(deprecated)]
897 fn deprecated_tcp_builder_delegates_to_the_new_name() {
898 let canonical_last = NetworkBuilder::new()
899 .max_connections(0)
900 .max_tcp_connections(64)
901 .build()
902 .unwrap();
903 assert_eq!(
904 canonical_last.max_tcp_connections,
905 Some(ConnectionLimit::from(64))
906 );
907 let legacy_last = NetworkBuilder::new()
908 .max_tcp_connections(64)
909 .max_connections(0)
910 .build()
911 .unwrap();
912 assert_eq!(
913 legacy_last.max_tcp_connections,
914 Some(ConnectionLimit::Unlimited)
915 );
916 let config = NetworkBuilder::new()
917 .max_connections(0)
918 .max_udp_connections(7)
919 .build()
920 .unwrap();
921 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
922 assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
923 assert!(
924 NetworkBuilder::new()
925 .max_tcp_connections(1)
926 .max_tcp_connections(2)
927 .build()
928 .is_ok()
929 );
930 }
931
932 #[test]
934 fn network_builder_happy_path_returns_config() {
935 let cfg = NetworkBuilder::new()
936 .dns(|d| d.rebind_protection(false))
937 .build()
938 .unwrap();
939 assert!(!cfg.dns.rebind_protection);
940 }
941
942 #[test]
943 fn network_builder_preserves_explicit_large_caps() {
944 for limit in [10000, usize::MAX] {
945 let config = NetworkBuilder::new()
946 .max_tcp_connections(limit)
947 .build()
948 .unwrap();
949 assert_eq!(
950 config.max_tcp_connections,
951 Some(ConnectionLimit::from(limit))
952 );
953 }
954 }
955
956 #[test]
957 fn network_builder_rejects_incomplete_intercept_ca_config() {
958 let err = NetworkBuilder::new()
959 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
960 .build()
961 .unwrap_err();
962
963 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
964 }
965
966 #[test]
967 fn port_bind_sets_host_bind() {
968 let bind = "0.0.0.0".parse().unwrap();
969 let cfg = NetworkBuilder::new()
970 .port_bind(bind, 8080, 80)
971 .port_udp_bind(bind, 5353, 53)
972 .build()
973 .unwrap();
974
975 assert_eq!(cfg.ports[0].host_bind, bind);
976 assert_eq!(cfg.ports[0].host_port, 8080);
977 assert_eq!(cfg.ports[0].guest_port, 80);
978 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
979 assert_eq!(cfg.ports[1].host_bind, bind);
980 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
981 }
982
983 #[test]
984 fn port_helpers_default_to_loopback() {
985 let cfg = NetworkBuilder::new()
986 .port(8080, 80)
987 .port_udp(5353, 53)
988 .build()
989 .unwrap();
990
991 assert_eq!(
992 cfg.ports[0].host_bind,
993 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
994 );
995 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
996 assert_eq!(
997 cfg.ports[1].host_bind,
998 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
999 );
1000 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1001 }
1002
1003 #[test]
1004 fn outbound_proxy_defaults_to_none() {
1005 let cfg = NetworkBuilder::new().build().unwrap();
1006 assert_eq!(cfg.outbound_proxy, None);
1007 }
1008
1009 #[test]
1010 fn network_builder_sets_strict_mode() {
1011 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1012 assert!(cfg.strict);
1013 }
1014
1015 #[test]
1016 fn network_builder_sets_global_violation_action() {
1017 let cfg = NetworkBuilder::new()
1018 .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1019 .build()
1020 .unwrap();
1021
1022 assert_eq!(
1023 cfg.secrets.violation_action,
1024 SecretViolationAction::BlockAndTerminate
1025 );
1026 }
1027
1028 #[test]
1029 fn secret_builder_sets_passthrough_and_violation_policies() {
1030 let secret = SecretBuilder::new()
1031 .env("TOKEN")
1032 .value("secret-value")
1033 .allow("api.github.com")
1034 .allow_passthrough_for("api.anthropic.com")
1035 .allow_passthrough_for("*.anthropic.com")
1036 .violation_action(SecretViolationAction::BlockAndTerminate)
1037 .build();
1038
1039 assert_eq!(
1040 secret.violation_action,
1041 Some(SecretViolationAction::BlockAndTerminate),
1042 );
1043 assert_eq!(
1044 secret.passthrough_hosts,
1045 vec![
1046 HostPattern::Exact("api.anthropic.com".into()),
1047 HostPattern::Wildcard("*.anthropic.com".into()),
1048 ],
1049 );
1050 }
1051
1052 #[test]
1053 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1054 fn secret_builder_rejects_empty_allowed_hosts() {
1055 let _ = SecretBuilder::new()
1056 .env("TOKEN")
1057 .value("secret-value")
1058 .build();
1059 }
1060
1061 #[test]
1062 fn secret_builder_source_yields_reference_and_empty_value() {
1063 let secret = SecretBuilder::new()
1064 .env("API_KEY")
1065 .source(SecretSource::Env {
1066 var: "HOST_API_KEY".into(),
1067 })
1068 .allow("api.example.com")
1069 .build();
1070
1071 assert!(secret.value.is_empty());
1072 assert_eq!(
1073 secret.source,
1074 Some(SecretSource::Env {
1075 var: "HOST_API_KEY".into()
1076 })
1077 );
1078
1079 let json = serde_json::to_string(&secret).unwrap();
1081 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1082 }
1083
1084 #[test]
1085 #[should_panic(expected = "exactly one of .value() or .source()")]
1086 fn secret_builder_rejects_both_value_and_source() {
1087 let _ = SecretBuilder::new()
1088 .env("API_KEY")
1089 .value("inline")
1090 .source(SecretSource::Env {
1091 var: "HOST_API_KEY".into(),
1092 })
1093 .allow("api.example.com")
1094 .build();
1095 }
1096
1097 #[test]
1098 fn network_builder_rejects_invalid_secret_config() {
1099 let err = NetworkBuilder::new()
1100 .secret_entry(SecretEntry {
1101 env_var: "API=KEY".into(),
1102 value: Zeroizing::new("secret-value".into()),
1103 source: None,
1104 placeholder: "$MSB_API_KEY".into(),
1105 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1106 substitution: SecretSubstitution::default(),
1107 passthrough_hosts: Vec::new(),
1108 violation_action: None,
1109 require_tls_identity: true,
1110 })
1111 .build()
1112 .unwrap_err();
1113
1114 assert!(err.to_string().contains("env_var must not contain `=`"));
1115 }
1116
1117 #[test]
1118 fn rate_limiter_builder_sets_buckets_and_bursts() {
1119 use microsandbox_utils::size::SizeExt;
1120
1121 let cfg = NetworkBuilder::new()
1122 .rate_limiter(|r| {
1123 r.egress(|r| {
1124 r.bandwidth(1.mib(), Duration::from_secs(1))
1125 .bandwidth_burst(512.kib())
1126 .ops(1_000, Duration::from_secs(1))
1127 .ops_burst(500)
1128 })
1129 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1130 })
1131 .build()
1132 .unwrap();
1133
1134 let rate_limiter = cfg.rate_limiter.unwrap();
1135 let egress = rate_limiter.egress.unwrap();
1136 let bandwidth = egress.bandwidth.unwrap();
1137 assert_eq!(bandwidth.size, 1024 * 1024);
1138 assert_eq!(bandwidth.refill_time_ms, 1000);
1139 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1140 let ops = egress.ops.unwrap();
1141 assert_eq!(ops.size, 1_000);
1142 assert_eq!(ops.refill_time_ms, 1000);
1143 assert_eq!(ops.one_time_burst, 500);
1144
1145 let ingress = rate_limiter.ingress.unwrap();
1146 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1147 assert!(ingress.ops.is_none());
1148 }
1149
1150 #[test]
1151 fn rate_limiters_default_to_unlimited() {
1152 let cfg = NetworkBuilder::new().build().unwrap();
1153 assert!(cfg.rate_limiter.is_none());
1154 }
1155
1156 #[test]
1157 fn rate_limiter_builder_rejects_empty_limiter() {
1158 let err = NetworkBuilder::new()
1159 .rate_limiter(|r| r.egress(|r| r))
1160 .build()
1161 .unwrap_err();
1162 assert_eq!(
1163 err.to_string(),
1164 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1165 );
1166 }
1167
1168 #[test]
1169 fn network_rate_limiter_builder_rejects_missing_directions() {
1170 let err = NetworkBuilder::new()
1171 .rate_limiter(|r| r)
1172 .build()
1173 .unwrap_err();
1174 assert_eq!(
1175 err.to_string(),
1176 "rate limiter must configure at least one of egress or ingress"
1177 );
1178 }
1179
1180 #[test]
1181 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1182 let err = NetworkBuilder::new()
1183 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1184 .build()
1185 .unwrap_err();
1186 assert_eq!(
1187 err.to_string(),
1188 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1189 );
1190
1191 let err = NetworkBuilder::new()
1192 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1193 .build()
1194 .unwrap_err();
1195 assert_eq!(
1196 err.to_string(),
1197 "egress rate limiter: ops refill interval must be at least one millisecond"
1198 );
1199
1200 let err = NetworkBuilder::new()
1201 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1202 .build()
1203 .unwrap_err();
1204 assert_eq!(
1205 err.to_string(),
1206 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1207 );
1208 }
1209
1210 #[test]
1211 fn rate_limiter_builder_rejects_burst_without_bucket() {
1212 use microsandbox_utils::size::SizeExt;
1213
1214 let err = NetworkBuilder::new()
1215 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1216 .build()
1217 .unwrap_err();
1218 assert_eq!(
1219 err.to_string(),
1220 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1221 );
1222
1223 let err = NetworkBuilder::new()
1224 .rate_limiter(|r| {
1225 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1226 })
1227 .build()
1228 .unwrap_err();
1229 assert_eq!(
1230 err.to_string(),
1231 "ingress rate limiter: ops_burst requires the ops bucket"
1232 );
1233 }
1234
1235 #[test]
1236 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1237 let err = NetworkBuilder::new()
1238 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1239 .build()
1240 .unwrap_err();
1241 assert_eq!(
1242 err.to_string(),
1243 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1244 );
1245 }
1246}