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 DnsConfig, InterfaceOverrides, MAX_NETWORK_CONNECTIONS, NetworkConfig, PortProtocol,
19 PublishedPort,
20};
21use crate::dns::Nameserver;
22use crate::policy::{BuildError, NetworkPolicy};
23use crate::secrets::config::{
24 HostPattern, SecretEntry, SecretInjection, SecretSource, ViolationAction,
25};
26
27#[derive(Clone)]
33pub struct NetworkBuilder {
34 config: NetworkConfig,
35 errors: Vec<BuildError>,
36}
37
38pub struct DnsBuilder {
40 config: DnsConfig,
41}
42
43pub struct TlsBuilder {
45 config: TlsConfig,
46}
47
48pub struct SecretBuilder {
58 env_var: Option<String>,
59 value: Option<String>,
60 source: Option<SecretSource>,
61 placeholder: Option<String>,
62 allowed_hosts: Vec<HostPattern>,
63 injection: SecretInjection,
64 on_violation: Option<ViolationAction>,
65 require_tls_identity: bool,
66}
67
68#[derive(Default)]
70pub struct ViolationActionBuilder {
71 action: ViolationAction,
72}
73
74#[derive(Default)]
83pub struct NetworkRateLimiterBuilder {
84 config: NetworkRateLimiterConfig,
85 errors: Vec<BuildError>,
86}
87
88pub struct RateLimiterBuilder {
99 direction: NetworkRateLimitDirection,
100 bandwidth: Option<TokenBucketConfig>,
101 ops: Option<TokenBucketConfig>,
102 bandwidth_burst: Option<u64>,
103 ops_burst: Option<u64>,
104 refill_error: Option<(&'static str, RefillTimeError)>,
106}
107
108#[derive(Clone, Copy, Debug)]
109enum RefillTimeError {
110 TooShort,
111 Precision,
112 TooLong,
113}
114
115impl NetworkBuilder {
120 pub fn new() -> Self {
122 Self {
123 config: NetworkConfig::default(),
124 errors: Vec::new(),
125 }
126 }
127
128 pub fn from_config(config: NetworkConfig) -> Self {
130 Self {
131 config,
132 errors: Vec::new(),
133 }
134 }
135
136 pub fn enabled(mut self, enabled: bool) -> Self {
138 self.config.enabled = enabled;
139 self
140 }
141
142 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
144 self.port_bind(
145 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
146 host_port,
147 guest_port,
148 )
149 }
150
151 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
153 self.port_udp_bind(
154 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
155 host_port,
156 guest_port,
157 )
158 }
159
160 pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
162 self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
163 }
164
165 pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
167 self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
168 }
169
170 fn add_port(
171 mut self,
172 host_bind: IpAddr,
173 host_port: u16,
174 guest_port: u16,
175 protocol: PortProtocol,
176 ) -> Self {
177 self.config.ports.push(PublishedPort {
178 host_port,
179 guest_port,
180 protocol,
181 host_bind,
182 });
183 self
184 }
185
186 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
188 self.config.policy = policy;
189 self
190 }
191
192 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
201 self.config.dns = f(DnsBuilder::new()).build();
202 self
203 }
204
205 #[doc(hidden)]
207 pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
208 self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
209 self
210 }
211
212 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
214 self.config.tls = f(TlsBuilder::new()).build();
215 self
216 }
217
218 #[doc(hidden)]
220 pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
221 self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
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 injection: SecretInjection::default(),
259 on_violation: None,
260 require_tls_identity: true,
261 });
262 self
263 }
264
265 pub fn on_secret_violation(
267 mut self,
268 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
269 ) -> Self {
270 self.config.secrets.on_violation = f(ViolationActionBuilder::default()).build();
271 self
272 }
273
274 pub fn max_connections(mut self, max: usize) -> Self {
276 if max > MAX_NETWORK_CONNECTIONS {
277 self.errors.push(BuildError::MaxConnectionsExceeded {
278 configured: max,
279 limit: MAX_NETWORK_CONNECTIONS,
280 });
281 } else {
282 self.config.max_connections = Some(max);
283 }
284 self
285 }
286
287 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
289 self.config.interface = overrides;
290 self
291 }
292
293 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
297 if pool.prefix() > 30 {
298 self.errors.push(BuildError::InvalidIpv4Pool {
299 raw: pool.to_string(),
300 });
301 } else {
302 self.config.interface.ipv4_pool = Some(pool);
303 }
304 self
305 }
306
307 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
311 if pool.prefix() > 64 {
312 self.errors.push(BuildError::InvalidIpv6Pool {
313 raw: pool.to_string(),
314 });
315 } else {
316 self.config.interface.ipv6_pool = Some(pool);
317 }
318 self
319 }
320
321 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
327 self.config.trust_host_cas = enabled;
328 self
329 }
330
331 pub fn rate_limiter(
342 mut self,
343 f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
344 ) -> Self {
345 match f(NetworkRateLimiterBuilder::new()).build() {
346 Ok(limiter) => self.config.rate_limiter = Some(limiter),
347 Err(err) => self.errors.push(err),
348 }
349 self
350 }
351
352 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
358 if let Some(err) = self.errors.drain(..).next() {
359 return Err(err);
360 }
361 if let Some(max) = self.config.max_connections
362 && max > MAX_NETWORK_CONNECTIONS
363 {
364 return Err(BuildError::MaxConnectionsExceeded {
365 configured: max,
366 limit: MAX_NETWORK_CONNECTIONS,
367 });
368 }
369 if self.config.tls.enabled
370 && (self.config.tls.intercept_ca.cert_path.is_some()
371 != self.config.tls.intercept_ca.key_path.is_some())
372 {
373 return Err(BuildError::IncompleteInterceptCaConfig);
374 }
375 self.config.secrets.validate()?;
376 Ok(self.config)
377 }
378}
379
380impl DnsBuilder {
381 pub fn new() -> Self {
383 Self {
384 config: DnsConfig::default(),
385 }
386 }
387
388 fn from_config(config: DnsConfig) -> Self {
389 Self { config }
390 }
391
392 pub fn rebind_protection(mut self, enabled: bool) -> Self {
394 self.config.rebind_protection = enabled;
395 self
396 }
397
398 pub fn nameservers<I>(mut self, nameservers: I) -> Self
405 where
406 I: IntoIterator,
407 I::Item: Into<Nameserver>,
408 {
409 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
410 self
411 }
412
413 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
415 self.config.query_timeout_ms = ms;
416 self
417 }
418
419 pub fn build(self) -> DnsConfig {
421 self.config
422 }
423}
424
425impl Default for DnsBuilder {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431impl TlsBuilder {
432 pub fn new() -> Self {
434 Self {
435 config: TlsConfig {
436 enabled: true,
437 ..TlsConfig::default()
438 },
439 }
440 }
441
442 fn from_config(config: TlsConfig) -> Self {
443 Self { config }
444 }
445
446 pub fn enabled(mut self, enabled: bool) -> Self {
448 self.config.enabled = enabled;
449 self
450 }
451
452 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
454 self.config.bypass.push(pattern.into());
455 self
456 }
457
458 pub fn verify_upstream(mut self, verify: bool) -> Self {
460 self.config.verify_upstream = verify;
461 self
462 }
463
464 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
470 self.config
471 .scoped_verify_upstream
472 .push(ScopedVerifyUpstream {
473 pattern: pattern.into(),
474 verify,
475 });
476 self
477 }
478
479 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
481 self.config.intercepted_ports = ports;
482 self
483 }
484
485 pub fn block_quic(mut self, block: bool) -> Self {
487 self.config.block_quic_on_intercept = block;
488 self
489 }
490
491 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
496 self.config.upstream_ca_cert.push(path.into());
497 self
498 }
499
500 pub fn upstream_ca_cert_for(
507 mut self,
508 pattern: impl Into<String>,
509 path: impl Into<PathBuf>,
510 ) -> Self {
511 self.config
512 .scoped_upstream_ca_cert
513 .push(ScopedUpstreamCaCert {
514 pattern: pattern.into(),
515 path: path.into(),
516 });
517 self
518 }
519
520 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
522 self.config.intercept_ca.cert_path = Some(path.into());
523 self
524 }
525
526 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
528 self.config.intercept_ca.key_path = Some(path.into());
529 self
530 }
531
532 pub fn build(self) -> TlsConfig {
534 self.config
535 }
536}
537
538impl SecretBuilder {
539 pub fn new() -> Self {
541 Self {
542 env_var: None,
543 value: None,
544 source: None,
545 placeholder: None,
546 allowed_hosts: Vec::new(),
547 injection: SecretInjection::default(),
548 on_violation: None,
549 require_tls_identity: true,
550 }
551 }
552
553 pub fn env(mut self, var: impl Into<String>) -> Self {
558 self.env_var = Some(var.into());
559 self
560 }
561
562 pub fn value(mut self, value: impl Into<String>) -> Self {
568 self.value = Some(value.into());
569 self
570 }
571
572 pub fn source(mut self, source: SecretSource) -> Self {
579 self.source = Some(source);
580 self
581 }
582
583 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
589 self.placeholder = Some(placeholder.into());
590 self
591 }
592
593 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
595 self.allowed_hosts.push(HostPattern::Exact(host.into()));
596 self
597 }
598
599 pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
601 self.allowed_hosts
602 .push(HostPattern::Wildcard(pattern.into()));
603 self
604 }
605
606 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
609 if i_understand_the_risk {
610 self.allowed_hosts.push(HostPattern::Any);
611 }
612 self
613 }
614
615 pub fn on_violation(
617 mut self,
618 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
619 ) -> Self {
620 self.on_violation = Some(f(ViolationActionBuilder::default()).build());
621 self
622 }
623
624 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
626 self.require_tls_identity = enabled;
627 self
628 }
629
630 pub fn inject_headers(mut self, enabled: bool) -> Self {
632 self.injection.headers = enabled;
633 self
634 }
635
636 pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
638 self.injection.basic_auth = enabled;
639 self
640 }
641
642 pub fn inject_query(mut self, enabled: bool) -> Self {
644 self.injection.query_params = enabled;
645 self
646 }
647
648 pub fn inject_body(mut self, enabled: bool) -> Self {
655 self.injection.body = enabled;
656 self
657 }
658
659 pub fn build(self) -> SecretEntry {
669 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
670 assert!(
671 self.value.is_some() ^ self.source.is_some(),
672 "SecretBuilder: exactly one of .value() or .source() is required"
673 );
674 assert!(
675 !self.allowed_hosts.is_empty(),
676 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
677 );
678 let placeholder = self
679 .placeholder
680 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
681
682 SecretEntry {
683 env_var,
684 value: Zeroizing::new(self.value.unwrap_or_default()),
685 source: self.source,
686 placeholder,
687 allowed_hosts: self.allowed_hosts,
688 injection: self.injection,
689 on_violation: self.on_violation,
690 require_tls_identity: self.require_tls_identity,
691 }
692 }
693}
694
695impl NetworkRateLimiterBuilder {
696 fn new() -> Self {
697 Self::default()
698 }
699
700 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
702 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
703 Ok(limiter) => self.config.egress = Some(limiter),
704 Err(err) => self.errors.push(err),
705 }
706 self
707 }
708
709 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
711 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
712 Ok(limiter) => self.config.ingress = Some(limiter),
713 Err(err) => self.errors.push(err),
714 }
715 self
716 }
717
718 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
720 if let Some(error) = self.errors.drain(..).next() {
721 return Err(error);
722 }
723 if self.config.egress.is_none() && self.config.ingress.is_none() {
724 return Err(BuildError::EmptyNetworkRateLimiter);
725 }
726 Ok(self.config)
727 }
728}
729
730impl RateLimiterBuilder {
731 fn new(direction: NetworkRateLimitDirection) -> Self {
732 Self {
733 direction,
734 bandwidth: None,
735 ops: None,
736 bandwidth_burst: None,
737 ops_burst: None,
738 refill_error: None,
739 }
740 }
741
742 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
751 match refill_time_ms(refill_time) {
752 Ok(refill_time_ms) => {
753 self.bandwidth = Some(TokenBucketConfig {
754 size: size.into().as_u64(),
755 refill_time_ms,
756 one_time_burst: 0,
757 });
758 }
759 Err(error) => {
760 self.refill_error.get_or_insert(("bandwidth", error));
761 }
762 }
763 self
764 }
765
766 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
769 self.bandwidth_burst = Some(burst.into().as_u64());
770 self
771 }
772
773 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
782 match refill_time_ms(refill_time) {
783 Ok(refill_time_ms) => {
784 self.ops = Some(TokenBucketConfig {
785 size: count,
786 refill_time_ms,
787 one_time_burst: 0,
788 });
789 }
790 Err(error) => {
791 self.refill_error.get_or_insert(("ops", error));
792 }
793 }
794 self
795 }
796
797 pub fn ops_burst(mut self, count: u64) -> Self {
800 self.ops_burst = Some(count);
801 self
802 }
803
804 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
806 let direction = self.direction;
807 if let Some((bucket, error)) = self.refill_error {
808 return Err(match error {
809 RefillTimeError::TooShort => {
810 BuildError::RateLimitRefillTooShort { direction, bucket }
811 }
812 RefillTimeError::Precision => {
813 BuildError::RateLimitRefillPrecision { direction, bucket }
814 }
815 RefillTimeError::TooLong => {
816 BuildError::RateLimitRefillTooLong { direction, bucket }
817 }
818 });
819 }
820
821 let mut config = RateLimiterConfig {
822 bandwidth: self.bandwidth,
823 ops: self.ops,
824 };
825 if let Some(burst) = self.bandwidth_burst {
826 let Some(bandwidth) = &mut config.bandwidth else {
827 return Err(BuildError::RateLimitBurstWithoutBucket {
828 direction,
829 bucket: "bandwidth",
830 });
831 };
832 bandwidth.one_time_burst = burst;
833 }
834 if let Some(burst) = self.ops_burst {
835 let Some(ops) = &mut config.ops else {
836 return Err(BuildError::RateLimitBurstWithoutBucket {
837 direction,
838 bucket: "ops",
839 });
840 };
841 ops.one_time_burst = burst;
842 }
843
844 config
845 .validate()
846 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
847 Ok(config)
848 }
849}
850
851impl ViolationActionBuilder {
852 pub fn new() -> Self {
854 Self::default()
855 }
856
857 pub fn from_action(action: ViolationAction) -> Self {
859 action.into()
860 }
861
862 pub fn block(mut self) -> Self {
864 self.action = ViolationAction::Block;
865 self
866 }
867
868 pub fn block_and_log(mut self) -> Self {
870 self.action = ViolationAction::BlockAndLog;
871 self
872 }
873
874 pub fn block_and_terminate(mut self) -> Self {
876 self.action = ViolationAction::BlockAndTerminate;
877 self
878 }
879
880 pub fn passthrough_host(mut self, host: impl Into<String>) -> Self {
882 self.push_passthrough_host(HostPattern::Exact(host.into()));
883 self
884 }
885
886 pub fn passthrough_host_pattern(mut self, pattern: impl Into<String>) -> Self {
888 self.push_passthrough_host(HostPattern::Wildcard(pattern.into()));
889 self
890 }
891
892 pub fn passthrough_all_hosts(mut self, i_understand_the_risk: bool) -> Self {
894 if i_understand_the_risk {
895 self.push_passthrough_host(HostPattern::Any);
896 }
897 self
898 }
899
900 fn push_passthrough_host(&mut self, host: HostPattern) {
902 match self.action {
903 ViolationAction::Passthrough(ref mut hosts) => hosts.push(host),
904 _ => self.action = ViolationAction::Passthrough(vec![host]),
905 }
906 }
907
908 pub fn build(self) -> ViolationAction {
910 self.action
911 }
912}
913
914fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
920 if refill_time < Duration::from_millis(1) {
921 return Err(RefillTimeError::TooShort);
922 }
923 let refill_time_ms =
924 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
925 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
926 return Err(RefillTimeError::Precision);
927 }
928 Ok(refill_time_ms)
929}
930
931impl Default for NetworkBuilder {
936 fn default() -> Self {
937 Self::new()
938 }
939}
940
941impl Default for TlsBuilder {
942 fn default() -> Self {
943 Self::new()
944 }
945}
946
947impl Default for SecretBuilder {
948 fn default() -> Self {
949 Self::new()
950 }
951}
952impl From<ViolationAction> for ViolationActionBuilder {
953 fn from(action: ViolationAction) -> Self {
954 Self { action }
955 }
956}
957
958#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[test]
968 fn network_builder_happy_path_returns_config() {
969 let cfg = NetworkBuilder::new()
970 .dns(|d| d.rebind_protection(false))
971 .build()
972 .unwrap();
973 assert!(!cfg.dns.rebind_protection);
974 }
975
976 #[test]
977 fn network_builder_rejects_excessive_max_connections() {
978 let err = NetworkBuilder::new()
979 .max_connections(MAX_NETWORK_CONNECTIONS + 1)
980 .build()
981 .unwrap_err();
982
983 assert!(matches!(
984 err,
985 BuildError::MaxConnectionsExceeded {
986 configured,
987 limit: MAX_NETWORK_CONNECTIONS
988 } if configured == MAX_NETWORK_CONNECTIONS + 1
989 ));
990 }
991
992 #[test]
993 fn network_builder_rejects_incomplete_intercept_ca_config() {
994 let err = NetworkBuilder::new()
995 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
996 .build()
997 .unwrap_err();
998
999 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1000 }
1001
1002 #[test]
1003 fn port_bind_sets_host_bind() {
1004 let bind = "0.0.0.0".parse().unwrap();
1005 let cfg = NetworkBuilder::new()
1006 .port_bind(bind, 8080, 80)
1007 .port_udp_bind(bind, 5353, 53)
1008 .build()
1009 .unwrap();
1010
1011 assert_eq!(cfg.ports[0].host_bind, bind);
1012 assert_eq!(cfg.ports[0].host_port, 8080);
1013 assert_eq!(cfg.ports[0].guest_port, 80);
1014 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1015 assert_eq!(cfg.ports[1].host_bind, bind);
1016 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1017 }
1018
1019 #[test]
1020 fn port_helpers_default_to_loopback() {
1021 let cfg = NetworkBuilder::new()
1022 .port(8080, 80)
1023 .port_udp(5353, 53)
1024 .build()
1025 .unwrap();
1026
1027 assert_eq!(
1028 cfg.ports[0].host_bind,
1029 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1030 );
1031 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1032 assert_eq!(
1033 cfg.ports[1].host_bind,
1034 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1035 );
1036 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1037 }
1038
1039 #[test]
1040 fn outbound_proxy_defaults_to_none() {
1041 let cfg = NetworkBuilder::new().build().unwrap();
1042 assert_eq!(cfg.outbound_proxy, None);
1043 }
1044
1045 #[test]
1046 fn network_builder_sets_global_passthrough_action() {
1047 let cfg = NetworkBuilder::new()
1048 .on_secret_violation(|v| {
1049 v.passthrough_host("api.anthropic.com")
1050 .passthrough_host_pattern("*.anthropic.com")
1051 })
1052 .build()
1053 .unwrap();
1054
1055 assert_eq!(
1056 cfg.secrets.on_violation,
1057 ViolationAction::Passthrough(vec![
1058 HostPattern::Exact("api.anthropic.com".into()),
1059 HostPattern::Wildcard("*.anthropic.com".into()),
1060 ])
1061 );
1062 }
1063
1064 #[test]
1065 fn secret_builder_sets_violation_action() {
1066 let secret = SecretBuilder::new()
1067 .env("TOKEN")
1068 .value("secret-value")
1069 .allow_host("api.github.com")
1070 .on_violation(|v| {
1071 v.passthrough_host("api.anthropic.com")
1072 .passthrough_host_pattern("*.anthropic.com")
1073 })
1074 .build();
1075
1076 assert_eq!(
1077 secret.on_violation,
1078 Some(ViolationAction::Passthrough(vec![
1079 HostPattern::Exact("api.anthropic.com".into()),
1080 HostPattern::Wildcard("*.anthropic.com".into()),
1081 ])),
1082 );
1083 }
1084
1085 #[test]
1086 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1087 fn secret_builder_rejects_empty_allowed_hosts() {
1088 let _ = SecretBuilder::new()
1089 .env("TOKEN")
1090 .value("secret-value")
1091 .build();
1092 }
1093
1094 #[test]
1095 fn secret_builder_source_yields_reference_and_empty_value() {
1096 let secret = SecretBuilder::new()
1097 .env("API_KEY")
1098 .source(SecretSource::Env {
1099 var: "HOST_API_KEY".into(),
1100 })
1101 .allow_host("api.example.com")
1102 .build();
1103
1104 assert!(secret.value.is_empty());
1105 assert_eq!(
1106 secret.source,
1107 Some(SecretSource::Env {
1108 var: "HOST_API_KEY".into()
1109 })
1110 );
1111
1112 let json = serde_json::to_string(&secret).unwrap();
1114 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1115 }
1116
1117 #[test]
1118 #[should_panic(expected = "exactly one of .value() or .source()")]
1119 fn secret_builder_rejects_both_value_and_source() {
1120 let _ = SecretBuilder::new()
1121 .env("API_KEY")
1122 .value("inline")
1123 .source(SecretSource::Env {
1124 var: "HOST_API_KEY".into(),
1125 })
1126 .allow_host("api.example.com")
1127 .build();
1128 }
1129
1130 #[test]
1131 fn network_builder_rejects_invalid_secret_config() {
1132 let err = NetworkBuilder::new()
1133 .secret_entry(SecretEntry {
1134 env_var: "API=KEY".into(),
1135 value: Zeroizing::new("secret-value".into()),
1136 source: None,
1137 placeholder: "$MSB_API_KEY".into(),
1138 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1139 injection: SecretInjection::default(),
1140 on_violation: None,
1141 require_tls_identity: true,
1142 })
1143 .build()
1144 .unwrap_err();
1145
1146 assert!(err.to_string().contains("env_var must not contain `=`"));
1147 }
1148
1149 #[test]
1150 fn violation_action_builder_blocking_call_replaces_passthrough_policy() {
1151 let action = ViolationActionBuilder::default()
1152 .passthrough_host("google.com")
1153 .block_and_terminate()
1154 .passthrough_host("facebook.com")
1155 .build();
1156
1157 assert_eq!(
1158 action,
1159 ViolationAction::Passthrough(vec![HostPattern::Exact("facebook.com".into())])
1160 );
1161 }
1162
1163 #[test]
1164 fn rate_limiter_builder_sets_buckets_and_bursts() {
1165 use microsandbox_utils::size::SizeExt;
1166
1167 let cfg = NetworkBuilder::new()
1168 .rate_limiter(|r| {
1169 r.egress(|r| {
1170 r.bandwidth(1.mib(), Duration::from_secs(1))
1171 .bandwidth_burst(512.kib())
1172 .ops(1_000, Duration::from_secs(1))
1173 .ops_burst(500)
1174 })
1175 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1176 })
1177 .build()
1178 .unwrap();
1179
1180 let rate_limiter = cfg.rate_limiter.unwrap();
1181 let egress = rate_limiter.egress.unwrap();
1182 let bandwidth = egress.bandwidth.unwrap();
1183 assert_eq!(bandwidth.size, 1024 * 1024);
1184 assert_eq!(bandwidth.refill_time_ms, 1000);
1185 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1186 let ops = egress.ops.unwrap();
1187 assert_eq!(ops.size, 1_000);
1188 assert_eq!(ops.refill_time_ms, 1000);
1189 assert_eq!(ops.one_time_burst, 500);
1190
1191 let ingress = rate_limiter.ingress.unwrap();
1192 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1193 assert!(ingress.ops.is_none());
1194 }
1195
1196 #[test]
1197 fn rate_limiters_default_to_unlimited() {
1198 let cfg = NetworkBuilder::new().build().unwrap();
1199 assert!(cfg.rate_limiter.is_none());
1200 }
1201
1202 #[test]
1203 fn rate_limiter_builder_rejects_empty_limiter() {
1204 let err = NetworkBuilder::new()
1205 .rate_limiter(|r| r.egress(|r| r))
1206 .build()
1207 .unwrap_err();
1208 assert_eq!(
1209 err.to_string(),
1210 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1211 );
1212 }
1213
1214 #[test]
1215 fn network_rate_limiter_builder_rejects_missing_directions() {
1216 let err = NetworkBuilder::new()
1217 .rate_limiter(|r| r)
1218 .build()
1219 .unwrap_err();
1220 assert_eq!(
1221 err.to_string(),
1222 "rate limiter must configure at least one of egress or ingress"
1223 );
1224 }
1225
1226 #[test]
1227 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1228 let err = NetworkBuilder::new()
1229 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1230 .build()
1231 .unwrap_err();
1232 assert_eq!(
1233 err.to_string(),
1234 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1235 );
1236
1237 let err = NetworkBuilder::new()
1238 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1239 .build()
1240 .unwrap_err();
1241 assert_eq!(
1242 err.to_string(),
1243 "egress rate limiter: ops refill interval must be at least one millisecond"
1244 );
1245
1246 let err = NetworkBuilder::new()
1247 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1248 .build()
1249 .unwrap_err();
1250 assert_eq!(
1251 err.to_string(),
1252 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1253 );
1254 }
1255
1256 #[test]
1257 fn rate_limiter_builder_rejects_burst_without_bucket() {
1258 use microsandbox_utils::size::SizeExt;
1259
1260 let err = NetworkBuilder::new()
1261 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1262 .build()
1263 .unwrap_err();
1264 assert_eq!(
1265 err.to_string(),
1266 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1267 );
1268
1269 let err = NetworkBuilder::new()
1270 .rate_limiter(|r| {
1271 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1272 })
1273 .build()
1274 .unwrap_err();
1275 assert_eq!(
1276 err.to_string(),
1277 "ingress rate limiter: ops_burst requires the ops bucket"
1278 );
1279 }
1280
1281 #[test]
1282 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1283 let err = NetworkBuilder::new()
1284 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1285 .build()
1286 .unwrap_err();
1287 assert_eq!(
1288 err.to_string(),
1289 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1290 );
1291 }
1292
1293 #[test]
1294 fn violation_action_builder_accumulates_passthrough_hosts() {
1295 let action = ViolationActionBuilder::default()
1296 .block()
1297 .passthrough_host("google.com")
1298 .passthrough_host("facebook.com")
1299 .build();
1300
1301 assert_eq!(
1302 action,
1303 ViolationAction::Passthrough(vec![
1304 HostPattern::Exact("google.com".into()),
1305 HostPattern::Exact("facebook.com".into()),
1306 ]),
1307 );
1308 }
1309}