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 network_builder_sets_global_passthrough_action() {
1041 let cfg = NetworkBuilder::new()
1042 .on_secret_violation(|v| {
1043 v.passthrough_host("api.anthropic.com")
1044 .passthrough_host_pattern("*.anthropic.com")
1045 })
1046 .build()
1047 .unwrap();
1048
1049 assert_eq!(
1050 cfg.secrets.on_violation,
1051 ViolationAction::Passthrough(vec![
1052 HostPattern::Exact("api.anthropic.com".into()),
1053 HostPattern::Wildcard("*.anthropic.com".into()),
1054 ])
1055 );
1056 }
1057
1058 #[test]
1059 fn secret_builder_sets_violation_action() {
1060 let secret = SecretBuilder::new()
1061 .env("TOKEN")
1062 .value("secret-value")
1063 .allow_host("api.github.com")
1064 .on_violation(|v| {
1065 v.passthrough_host("api.anthropic.com")
1066 .passthrough_host_pattern("*.anthropic.com")
1067 })
1068 .build();
1069
1070 assert_eq!(
1071 secret.on_violation,
1072 Some(ViolationAction::Passthrough(vec![
1073 HostPattern::Exact("api.anthropic.com".into()),
1074 HostPattern::Wildcard("*.anthropic.com".into()),
1075 ])),
1076 );
1077 }
1078
1079 #[test]
1080 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1081 fn secret_builder_rejects_empty_allowed_hosts() {
1082 let _ = SecretBuilder::new()
1083 .env("TOKEN")
1084 .value("secret-value")
1085 .build();
1086 }
1087
1088 #[test]
1089 fn secret_builder_source_yields_reference_and_empty_value() {
1090 let secret = SecretBuilder::new()
1091 .env("API_KEY")
1092 .source(SecretSource::Env {
1093 var: "HOST_API_KEY".into(),
1094 })
1095 .allow_host("api.example.com")
1096 .build();
1097
1098 assert!(secret.value.is_empty());
1099 assert_eq!(
1100 secret.source,
1101 Some(SecretSource::Env {
1102 var: "HOST_API_KEY".into()
1103 })
1104 );
1105
1106 let json = serde_json::to_string(&secret).unwrap();
1108 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1109 }
1110
1111 #[test]
1112 #[should_panic(expected = "exactly one of .value() or .source()")]
1113 fn secret_builder_rejects_both_value_and_source() {
1114 let _ = SecretBuilder::new()
1115 .env("API_KEY")
1116 .value("inline")
1117 .source(SecretSource::Env {
1118 var: "HOST_API_KEY".into(),
1119 })
1120 .allow_host("api.example.com")
1121 .build();
1122 }
1123
1124 #[test]
1125 fn network_builder_rejects_invalid_secret_config() {
1126 let err = NetworkBuilder::new()
1127 .secret_entry(SecretEntry {
1128 env_var: "API=KEY".into(),
1129 value: Zeroizing::new("secret-value".into()),
1130 source: None,
1131 placeholder: "$MSB_API_KEY".into(),
1132 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1133 injection: SecretInjection::default(),
1134 on_violation: None,
1135 require_tls_identity: true,
1136 })
1137 .build()
1138 .unwrap_err();
1139
1140 assert!(err.to_string().contains("env_var must not contain `=`"));
1141 }
1142
1143 #[test]
1144 fn violation_action_builder_blocking_call_replaces_passthrough_policy() {
1145 let action = ViolationActionBuilder::default()
1146 .passthrough_host("google.com")
1147 .block_and_terminate()
1148 .passthrough_host("facebook.com")
1149 .build();
1150
1151 assert_eq!(
1152 action,
1153 ViolationAction::Passthrough(vec![HostPattern::Exact("facebook.com".into())])
1154 );
1155 }
1156
1157 #[test]
1158 fn rate_limiter_builder_sets_buckets_and_bursts() {
1159 use microsandbox_utils::size::SizeExt;
1160
1161 let cfg = NetworkBuilder::new()
1162 .rate_limiter(|r| {
1163 r.egress(|r| {
1164 r.bandwidth(1.mib(), Duration::from_secs(1))
1165 .bandwidth_burst(512.kib())
1166 .ops(1_000, Duration::from_secs(1))
1167 .ops_burst(500)
1168 })
1169 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1170 })
1171 .build()
1172 .unwrap();
1173
1174 let rate_limiter = cfg.rate_limiter.unwrap();
1175 let egress = rate_limiter.egress.unwrap();
1176 let bandwidth = egress.bandwidth.unwrap();
1177 assert_eq!(bandwidth.size, 1024 * 1024);
1178 assert_eq!(bandwidth.refill_time_ms, 1000);
1179 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1180 let ops = egress.ops.unwrap();
1181 assert_eq!(ops.size, 1_000);
1182 assert_eq!(ops.refill_time_ms, 1000);
1183 assert_eq!(ops.one_time_burst, 500);
1184
1185 let ingress = rate_limiter.ingress.unwrap();
1186 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1187 assert!(ingress.ops.is_none());
1188 }
1189
1190 #[test]
1191 fn rate_limiters_default_to_unlimited() {
1192 let cfg = NetworkBuilder::new().build().unwrap();
1193 assert!(cfg.rate_limiter.is_none());
1194 }
1195
1196 #[test]
1197 fn rate_limiter_builder_rejects_empty_limiter() {
1198 let err = NetworkBuilder::new()
1199 .rate_limiter(|r| r.egress(|r| r))
1200 .build()
1201 .unwrap_err();
1202 assert_eq!(
1203 err.to_string(),
1204 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1205 );
1206 }
1207
1208 #[test]
1209 fn network_rate_limiter_builder_rejects_missing_directions() {
1210 let err = NetworkBuilder::new()
1211 .rate_limiter(|r| r)
1212 .build()
1213 .unwrap_err();
1214 assert_eq!(
1215 err.to_string(),
1216 "rate limiter must configure at least one of egress or ingress"
1217 );
1218 }
1219
1220 #[test]
1221 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1222 let err = NetworkBuilder::new()
1223 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1224 .build()
1225 .unwrap_err();
1226 assert_eq!(
1227 err.to_string(),
1228 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1229 );
1230
1231 let err = NetworkBuilder::new()
1232 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1233 .build()
1234 .unwrap_err();
1235 assert_eq!(
1236 err.to_string(),
1237 "egress rate limiter: ops refill interval must be at least one millisecond"
1238 );
1239
1240 let err = NetworkBuilder::new()
1241 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1242 .build()
1243 .unwrap_err();
1244 assert_eq!(
1245 err.to_string(),
1246 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1247 );
1248 }
1249
1250 #[test]
1251 fn rate_limiter_builder_rejects_burst_without_bucket() {
1252 use microsandbox_utils::size::SizeExt;
1253
1254 let err = NetworkBuilder::new()
1255 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1256 .build()
1257 .unwrap_err();
1258 assert_eq!(
1259 err.to_string(),
1260 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1261 );
1262
1263 let err = NetworkBuilder::new()
1264 .rate_limiter(|r| {
1265 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1266 })
1267 .build()
1268 .unwrap_err();
1269 assert_eq!(
1270 err.to_string(),
1271 "ingress rate limiter: ops_burst requires the ops bucket"
1272 );
1273 }
1274
1275 #[test]
1276 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1277 let err = NetworkBuilder::new()
1278 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1279 .build()
1280 .unwrap_err();
1281 assert_eq!(
1282 err.to_string(),
1283 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1284 );
1285 }
1286
1287 #[test]
1288 fn violation_action_builder_accumulates_passthrough_hosts() {
1289 let action = ViolationActionBuilder::default()
1290 .block()
1291 .passthrough_host("google.com")
1292 .passthrough_host("facebook.com")
1293 .build();
1294
1295 assert_eq!(
1296 action,
1297 ViolationAction::Passthrough(vec![
1298 HostPattern::Exact("google.com".into()),
1299 HostPattern::Exact("facebook.com".into()),
1300 ]),
1301 );
1302 }
1303}