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 strict(mut self, enabled: bool) -> Self {
227 self.config.strict = enabled;
228 self
229 }
230
231 pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
241 self.secret_entry(f(SecretBuilder::new()).build())
242 }
243
244 pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
246 self.config.secrets.secrets.push(entry);
247 self
248 }
249
250 pub fn secret_env(
252 mut self,
253 env_var: impl Into<String>,
254 value: impl Into<String>,
255 placeholder: impl Into<String>,
256 allowed_host: impl Into<String>,
257 ) -> Self {
258 self.config.secrets.secrets.push(SecretEntry {
259 env_var: env_var.into(),
260 value: Zeroizing::new(value.into()),
261 source: None,
262 placeholder: placeholder.into(),
263 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
264 injection: SecretInjection::default(),
265 on_violation: None,
266 require_tls_identity: true,
267 });
268 self
269 }
270
271 pub fn on_secret_violation(
273 mut self,
274 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
275 ) -> Self {
276 self.config.secrets.on_violation = f(ViolationActionBuilder::default()).build();
277 self
278 }
279
280 pub fn max_connections(mut self, max: usize) -> Self {
282 if max > MAX_NETWORK_CONNECTIONS {
283 self.errors.push(BuildError::MaxConnectionsExceeded {
284 configured: max,
285 limit: MAX_NETWORK_CONNECTIONS,
286 });
287 } else {
288 self.config.max_connections = Some(max);
289 }
290 self
291 }
292
293 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
295 self.config.interface = overrides;
296 self
297 }
298
299 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
303 if pool.prefix() > 30 {
304 self.errors.push(BuildError::InvalidIpv4Pool {
305 raw: pool.to_string(),
306 });
307 } else {
308 self.config.interface.ipv4_pool = Some(pool);
309 }
310 self
311 }
312
313 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
317 if pool.prefix() > 64 {
318 self.errors.push(BuildError::InvalidIpv6Pool {
319 raw: pool.to_string(),
320 });
321 } else {
322 self.config.interface.ipv6_pool = Some(pool);
323 }
324 self
325 }
326
327 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
333 self.config.trust_host_cas = enabled;
334 self
335 }
336
337 pub fn rate_limiter(
348 mut self,
349 f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
350 ) -> Self {
351 match f(NetworkRateLimiterBuilder::new()).build() {
352 Ok(limiter) => self.config.rate_limiter = Some(limiter),
353 Err(err) => self.errors.push(err),
354 }
355 self
356 }
357
358 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
364 if let Some(err) = self.errors.drain(..).next() {
365 return Err(err);
366 }
367 if let Some(max) = self.config.max_connections
368 && max > MAX_NETWORK_CONNECTIONS
369 {
370 return Err(BuildError::MaxConnectionsExceeded {
371 configured: max,
372 limit: MAX_NETWORK_CONNECTIONS,
373 });
374 }
375 if self.config.tls.enabled
376 && (self.config.tls.intercept_ca.cert_path.is_some()
377 != self.config.tls.intercept_ca.key_path.is_some())
378 {
379 return Err(BuildError::IncompleteInterceptCaConfig);
380 }
381 self.config.secrets.validate()?;
382 Ok(self.config)
383 }
384}
385
386impl DnsBuilder {
387 pub fn new() -> Self {
389 Self {
390 config: DnsConfig::default(),
391 }
392 }
393
394 fn from_config(config: DnsConfig) -> Self {
395 Self { config }
396 }
397
398 pub fn rebind_protection(mut self, enabled: bool) -> Self {
400 self.config.rebind_protection = enabled;
401 self
402 }
403
404 pub fn nameservers<I>(mut self, nameservers: I) -> Self
411 where
412 I: IntoIterator,
413 I::Item: Into<Nameserver>,
414 {
415 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
416 self
417 }
418
419 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
421 self.config.query_timeout_ms = ms;
422 self
423 }
424
425 pub fn build(self) -> DnsConfig {
427 self.config
428 }
429}
430
431impl Default for DnsBuilder {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437impl TlsBuilder {
438 pub fn new() -> Self {
440 Self {
441 config: TlsConfig {
442 enabled: true,
443 ..TlsConfig::default()
444 },
445 }
446 }
447
448 fn from_config(config: TlsConfig) -> Self {
449 Self { config }
450 }
451
452 pub fn enabled(mut self, enabled: bool) -> Self {
454 self.config.enabled = enabled;
455 self
456 }
457
458 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
460 self.config.bypass.push(pattern.into());
461 self
462 }
463
464 pub fn verify_upstream(mut self, verify: bool) -> Self {
466 self.config.verify_upstream = verify;
467 self
468 }
469
470 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
476 self.config
477 .scoped_verify_upstream
478 .push(ScopedVerifyUpstream {
479 pattern: pattern.into(),
480 verify,
481 });
482 self
483 }
484
485 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
487 self.config.intercepted_ports = ports;
488 self
489 }
490
491 pub fn block_quic(mut self, block: bool) -> Self {
493 self.config.block_quic_on_intercept = block;
494 self
495 }
496
497 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
502 self.config.upstream_ca_cert.push(path.into());
503 self
504 }
505
506 pub fn upstream_ca_cert_for(
513 mut self,
514 pattern: impl Into<String>,
515 path: impl Into<PathBuf>,
516 ) -> Self {
517 self.config
518 .scoped_upstream_ca_cert
519 .push(ScopedUpstreamCaCert {
520 pattern: pattern.into(),
521 path: path.into(),
522 });
523 self
524 }
525
526 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
528 self.config.intercept_ca.cert_path = Some(path.into());
529 self
530 }
531
532 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
534 self.config.intercept_ca.key_path = Some(path.into());
535 self
536 }
537
538 pub fn build(self) -> TlsConfig {
540 self.config
541 }
542}
543
544impl SecretBuilder {
545 pub fn new() -> Self {
547 Self {
548 env_var: None,
549 value: None,
550 source: None,
551 placeholder: None,
552 allowed_hosts: Vec::new(),
553 injection: SecretInjection::default(),
554 on_violation: None,
555 require_tls_identity: true,
556 }
557 }
558
559 pub fn env(mut self, var: impl Into<String>) -> Self {
564 self.env_var = Some(var.into());
565 self
566 }
567
568 pub fn value(mut self, value: impl Into<String>) -> Self {
574 self.value = Some(value.into());
575 self
576 }
577
578 pub fn source(mut self, source: SecretSource) -> Self {
585 self.source = Some(source);
586 self
587 }
588
589 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
595 self.placeholder = Some(placeholder.into());
596 self
597 }
598
599 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
601 self.allowed_hosts.push(HostPattern::Exact(host.into()));
602 self
603 }
604
605 pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
607 self.allowed_hosts
608 .push(HostPattern::Wildcard(pattern.into()));
609 self
610 }
611
612 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
615 if i_understand_the_risk {
616 self.allowed_hosts.push(HostPattern::Any);
617 }
618 self
619 }
620
621 pub fn on_violation(
623 mut self,
624 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
625 ) -> Self {
626 self.on_violation = Some(f(ViolationActionBuilder::default()).build());
627 self
628 }
629
630 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
632 self.require_tls_identity = enabled;
633 self
634 }
635
636 pub fn inject_headers(mut self, enabled: bool) -> Self {
638 self.injection.headers = enabled;
639 self
640 }
641
642 pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
644 self.injection.basic_auth = enabled;
645 self
646 }
647
648 pub fn inject_query(mut self, enabled: bool) -> Self {
650 self.injection.query_params = enabled;
651 self
652 }
653
654 pub fn inject_body(mut self, enabled: bool) -> Self {
661 self.injection.body = enabled;
662 self
663 }
664
665 pub fn build(self) -> SecretEntry {
675 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
676 assert!(
677 self.value.is_some() ^ self.source.is_some(),
678 "SecretBuilder: exactly one of .value() or .source() is required"
679 );
680 assert!(
681 !self.allowed_hosts.is_empty(),
682 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
683 );
684 let placeholder = self
685 .placeholder
686 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
687
688 SecretEntry {
689 env_var,
690 value: Zeroizing::new(self.value.unwrap_or_default()),
691 source: self.source,
692 placeholder,
693 allowed_hosts: self.allowed_hosts,
694 injection: self.injection,
695 on_violation: self.on_violation,
696 require_tls_identity: self.require_tls_identity,
697 }
698 }
699}
700
701impl NetworkRateLimiterBuilder {
702 fn new() -> Self {
703 Self::default()
704 }
705
706 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
708 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
709 Ok(limiter) => self.config.egress = Some(limiter),
710 Err(err) => self.errors.push(err),
711 }
712 self
713 }
714
715 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
717 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
718 Ok(limiter) => self.config.ingress = Some(limiter),
719 Err(err) => self.errors.push(err),
720 }
721 self
722 }
723
724 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
726 if let Some(error) = self.errors.drain(..).next() {
727 return Err(error);
728 }
729 if self.config.egress.is_none() && self.config.ingress.is_none() {
730 return Err(BuildError::EmptyNetworkRateLimiter);
731 }
732 Ok(self.config)
733 }
734}
735
736impl RateLimiterBuilder {
737 fn new(direction: NetworkRateLimitDirection) -> Self {
738 Self {
739 direction,
740 bandwidth: None,
741 ops: None,
742 bandwidth_burst: None,
743 ops_burst: None,
744 refill_error: None,
745 }
746 }
747
748 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
757 match refill_time_ms(refill_time) {
758 Ok(refill_time_ms) => {
759 self.bandwidth = Some(TokenBucketConfig {
760 size: size.into().as_u64(),
761 refill_time_ms,
762 one_time_burst: 0,
763 });
764 }
765 Err(error) => {
766 self.refill_error.get_or_insert(("bandwidth", error));
767 }
768 }
769 self
770 }
771
772 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
775 self.bandwidth_burst = Some(burst.into().as_u64());
776 self
777 }
778
779 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
788 match refill_time_ms(refill_time) {
789 Ok(refill_time_ms) => {
790 self.ops = Some(TokenBucketConfig {
791 size: count,
792 refill_time_ms,
793 one_time_burst: 0,
794 });
795 }
796 Err(error) => {
797 self.refill_error.get_or_insert(("ops", error));
798 }
799 }
800 self
801 }
802
803 pub fn ops_burst(mut self, count: u64) -> Self {
806 self.ops_burst = Some(count);
807 self
808 }
809
810 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
812 let direction = self.direction;
813 if let Some((bucket, error)) = self.refill_error {
814 return Err(match error {
815 RefillTimeError::TooShort => {
816 BuildError::RateLimitRefillTooShort { direction, bucket }
817 }
818 RefillTimeError::Precision => {
819 BuildError::RateLimitRefillPrecision { direction, bucket }
820 }
821 RefillTimeError::TooLong => {
822 BuildError::RateLimitRefillTooLong { direction, bucket }
823 }
824 });
825 }
826
827 let mut config = RateLimiterConfig {
828 bandwidth: self.bandwidth,
829 ops: self.ops,
830 };
831 if let Some(burst) = self.bandwidth_burst {
832 let Some(bandwidth) = &mut config.bandwidth else {
833 return Err(BuildError::RateLimitBurstWithoutBucket {
834 direction,
835 bucket: "bandwidth",
836 });
837 };
838 bandwidth.one_time_burst = burst;
839 }
840 if let Some(burst) = self.ops_burst {
841 let Some(ops) = &mut config.ops else {
842 return Err(BuildError::RateLimitBurstWithoutBucket {
843 direction,
844 bucket: "ops",
845 });
846 };
847 ops.one_time_burst = burst;
848 }
849
850 config
851 .validate()
852 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
853 Ok(config)
854 }
855}
856
857impl ViolationActionBuilder {
858 pub fn new() -> Self {
860 Self::default()
861 }
862
863 pub fn from_action(action: ViolationAction) -> Self {
865 action.into()
866 }
867
868 pub fn block(mut self) -> Self {
870 self.action = ViolationAction::Block;
871 self
872 }
873
874 pub fn block_and_log(mut self) -> Self {
876 self.action = ViolationAction::BlockAndLog;
877 self
878 }
879
880 pub fn block_and_terminate(mut self) -> Self {
882 self.action = ViolationAction::BlockAndTerminate;
883 self
884 }
885
886 pub fn passthrough_host(mut self, host: impl Into<String>) -> Self {
888 self.push_passthrough_host(HostPattern::Exact(host.into()));
889 self
890 }
891
892 pub fn passthrough_host_pattern(mut self, pattern: impl Into<String>) -> Self {
894 self.push_passthrough_host(HostPattern::Wildcard(pattern.into()));
895 self
896 }
897
898 pub fn passthrough_all_hosts(mut self, i_understand_the_risk: bool) -> Self {
900 if i_understand_the_risk {
901 self.push_passthrough_host(HostPattern::Any);
902 }
903 self
904 }
905
906 fn push_passthrough_host(&mut self, host: HostPattern) {
908 match self.action {
909 ViolationAction::Passthrough(ref mut hosts) => hosts.push(host),
910 _ => self.action = ViolationAction::Passthrough(vec![host]),
911 }
912 }
913
914 pub fn build(self) -> ViolationAction {
916 self.action
917 }
918}
919
920fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
926 if refill_time < Duration::from_millis(1) {
927 return Err(RefillTimeError::TooShort);
928 }
929 let refill_time_ms =
930 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
931 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
932 return Err(RefillTimeError::Precision);
933 }
934 Ok(refill_time_ms)
935}
936
937impl Default for NetworkBuilder {
942 fn default() -> Self {
943 Self::new()
944 }
945}
946
947impl Default for TlsBuilder {
948 fn default() -> Self {
949 Self::new()
950 }
951}
952
953impl Default for SecretBuilder {
954 fn default() -> Self {
955 Self::new()
956 }
957}
958impl From<ViolationAction> for ViolationActionBuilder {
959 fn from(action: ViolationAction) -> Self {
960 Self { action }
961 }
962}
963
964#[cfg(test)]
969mod tests {
970 use super::*;
971
972 #[test]
974 fn network_builder_happy_path_returns_config() {
975 let cfg = NetworkBuilder::new()
976 .dns(|d| d.rebind_protection(false))
977 .build()
978 .unwrap();
979 assert!(!cfg.dns.rebind_protection);
980 }
981
982 #[test]
983 fn network_builder_rejects_excessive_max_connections() {
984 let err = NetworkBuilder::new()
985 .max_connections(MAX_NETWORK_CONNECTIONS + 1)
986 .build()
987 .unwrap_err();
988
989 assert!(matches!(
990 err,
991 BuildError::MaxConnectionsExceeded {
992 configured,
993 limit: MAX_NETWORK_CONNECTIONS
994 } if configured == MAX_NETWORK_CONNECTIONS + 1
995 ));
996 }
997
998 #[test]
999 fn network_builder_rejects_incomplete_intercept_ca_config() {
1000 let err = NetworkBuilder::new()
1001 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1002 .build()
1003 .unwrap_err();
1004
1005 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1006 }
1007
1008 #[test]
1009 fn port_bind_sets_host_bind() {
1010 let bind = "0.0.0.0".parse().unwrap();
1011 let cfg = NetworkBuilder::new()
1012 .port_bind(bind, 8080, 80)
1013 .port_udp_bind(bind, 5353, 53)
1014 .build()
1015 .unwrap();
1016
1017 assert_eq!(cfg.ports[0].host_bind, bind);
1018 assert_eq!(cfg.ports[0].host_port, 8080);
1019 assert_eq!(cfg.ports[0].guest_port, 80);
1020 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1021 assert_eq!(cfg.ports[1].host_bind, bind);
1022 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1023 }
1024
1025 #[test]
1026 fn port_helpers_default_to_loopback() {
1027 let cfg = NetworkBuilder::new()
1028 .port(8080, 80)
1029 .port_udp(5353, 53)
1030 .build()
1031 .unwrap();
1032
1033 assert_eq!(
1034 cfg.ports[0].host_bind,
1035 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1036 );
1037 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1038 assert_eq!(
1039 cfg.ports[1].host_bind,
1040 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1041 );
1042 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1043 }
1044
1045 #[test]
1046 fn outbound_proxy_defaults_to_none() {
1047 let cfg = NetworkBuilder::new().build().unwrap();
1048 assert_eq!(cfg.outbound_proxy, None);
1049 }
1050
1051 #[test]
1052 fn network_builder_sets_strict_mode() {
1053 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1054 assert!(cfg.strict);
1055 }
1056
1057 #[test]
1058 fn network_builder_sets_global_passthrough_action() {
1059 let cfg = NetworkBuilder::new()
1060 .on_secret_violation(|v| {
1061 v.passthrough_host("api.anthropic.com")
1062 .passthrough_host_pattern("*.anthropic.com")
1063 })
1064 .build()
1065 .unwrap();
1066
1067 assert_eq!(
1068 cfg.secrets.on_violation,
1069 ViolationAction::Passthrough(vec![
1070 HostPattern::Exact("api.anthropic.com".into()),
1071 HostPattern::Wildcard("*.anthropic.com".into()),
1072 ])
1073 );
1074 }
1075
1076 #[test]
1077 fn secret_builder_sets_violation_action() {
1078 let secret = SecretBuilder::new()
1079 .env("TOKEN")
1080 .value("secret-value")
1081 .allow_host("api.github.com")
1082 .on_violation(|v| {
1083 v.passthrough_host("api.anthropic.com")
1084 .passthrough_host_pattern("*.anthropic.com")
1085 })
1086 .build();
1087
1088 assert_eq!(
1089 secret.on_violation,
1090 Some(ViolationAction::Passthrough(vec![
1091 HostPattern::Exact("api.anthropic.com".into()),
1092 HostPattern::Wildcard("*.anthropic.com".into()),
1093 ])),
1094 );
1095 }
1096
1097 #[test]
1098 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1099 fn secret_builder_rejects_empty_allowed_hosts() {
1100 let _ = SecretBuilder::new()
1101 .env("TOKEN")
1102 .value("secret-value")
1103 .build();
1104 }
1105
1106 #[test]
1107 fn secret_builder_source_yields_reference_and_empty_value() {
1108 let secret = SecretBuilder::new()
1109 .env("API_KEY")
1110 .source(SecretSource::Env {
1111 var: "HOST_API_KEY".into(),
1112 })
1113 .allow_host("api.example.com")
1114 .build();
1115
1116 assert!(secret.value.is_empty());
1117 assert_eq!(
1118 secret.source,
1119 Some(SecretSource::Env {
1120 var: "HOST_API_KEY".into()
1121 })
1122 );
1123
1124 let json = serde_json::to_string(&secret).unwrap();
1126 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1127 }
1128
1129 #[test]
1130 #[should_panic(expected = "exactly one of .value() or .source()")]
1131 fn secret_builder_rejects_both_value_and_source() {
1132 let _ = SecretBuilder::new()
1133 .env("API_KEY")
1134 .value("inline")
1135 .source(SecretSource::Env {
1136 var: "HOST_API_KEY".into(),
1137 })
1138 .allow_host("api.example.com")
1139 .build();
1140 }
1141
1142 #[test]
1143 fn network_builder_rejects_invalid_secret_config() {
1144 let err = NetworkBuilder::new()
1145 .secret_entry(SecretEntry {
1146 env_var: "API=KEY".into(),
1147 value: Zeroizing::new("secret-value".into()),
1148 source: None,
1149 placeholder: "$MSB_API_KEY".into(),
1150 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1151 injection: SecretInjection::default(),
1152 on_violation: None,
1153 require_tls_identity: true,
1154 })
1155 .build()
1156 .unwrap_err();
1157
1158 assert!(err.to_string().contains("env_var must not contain `=`"));
1159 }
1160
1161 #[test]
1162 fn violation_action_builder_blocking_call_replaces_passthrough_policy() {
1163 let action = ViolationActionBuilder::default()
1164 .passthrough_host("google.com")
1165 .block_and_terminate()
1166 .passthrough_host("facebook.com")
1167 .build();
1168
1169 assert_eq!(
1170 action,
1171 ViolationAction::Passthrough(vec![HostPattern::Exact("facebook.com".into())])
1172 );
1173 }
1174
1175 #[test]
1176 fn rate_limiter_builder_sets_buckets_and_bursts() {
1177 use microsandbox_utils::size::SizeExt;
1178
1179 let cfg = NetworkBuilder::new()
1180 .rate_limiter(|r| {
1181 r.egress(|r| {
1182 r.bandwidth(1.mib(), Duration::from_secs(1))
1183 .bandwidth_burst(512.kib())
1184 .ops(1_000, Duration::from_secs(1))
1185 .ops_burst(500)
1186 })
1187 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1188 })
1189 .build()
1190 .unwrap();
1191
1192 let rate_limiter = cfg.rate_limiter.unwrap();
1193 let egress = rate_limiter.egress.unwrap();
1194 let bandwidth = egress.bandwidth.unwrap();
1195 assert_eq!(bandwidth.size, 1024 * 1024);
1196 assert_eq!(bandwidth.refill_time_ms, 1000);
1197 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1198 let ops = egress.ops.unwrap();
1199 assert_eq!(ops.size, 1_000);
1200 assert_eq!(ops.refill_time_ms, 1000);
1201 assert_eq!(ops.one_time_burst, 500);
1202
1203 let ingress = rate_limiter.ingress.unwrap();
1204 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1205 assert!(ingress.ops.is_none());
1206 }
1207
1208 #[test]
1209 fn rate_limiters_default_to_unlimited() {
1210 let cfg = NetworkBuilder::new().build().unwrap();
1211 assert!(cfg.rate_limiter.is_none());
1212 }
1213
1214 #[test]
1215 fn rate_limiter_builder_rejects_empty_limiter() {
1216 let err = NetworkBuilder::new()
1217 .rate_limiter(|r| r.egress(|r| r))
1218 .build()
1219 .unwrap_err();
1220 assert_eq!(
1221 err.to_string(),
1222 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1223 );
1224 }
1225
1226 #[test]
1227 fn network_rate_limiter_builder_rejects_missing_directions() {
1228 let err = NetworkBuilder::new()
1229 .rate_limiter(|r| r)
1230 .build()
1231 .unwrap_err();
1232 assert_eq!(
1233 err.to_string(),
1234 "rate limiter must configure at least one of egress or ingress"
1235 );
1236 }
1237
1238 #[test]
1239 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1240 let err = NetworkBuilder::new()
1241 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1242 .build()
1243 .unwrap_err();
1244 assert_eq!(
1245 err.to_string(),
1246 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1247 );
1248
1249 let err = NetworkBuilder::new()
1250 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1251 .build()
1252 .unwrap_err();
1253 assert_eq!(
1254 err.to_string(),
1255 "egress rate limiter: ops refill interval must be at least one millisecond"
1256 );
1257
1258 let err = NetworkBuilder::new()
1259 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1260 .build()
1261 .unwrap_err();
1262 assert_eq!(
1263 err.to_string(),
1264 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1265 );
1266 }
1267
1268 #[test]
1269 fn rate_limiter_builder_rejects_burst_without_bucket() {
1270 use microsandbox_utils::size::SizeExt;
1271
1272 let err = NetworkBuilder::new()
1273 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1274 .build()
1275 .unwrap_err();
1276 assert_eq!(
1277 err.to_string(),
1278 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1279 );
1280
1281 let err = NetworkBuilder::new()
1282 .rate_limiter(|r| {
1283 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1284 })
1285 .build()
1286 .unwrap_err();
1287 assert_eq!(
1288 err.to_string(),
1289 "ingress rate limiter: ops_burst requires the ops bucket"
1290 );
1291 }
1292
1293 #[test]
1294 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1295 let err = NetworkBuilder::new()
1296 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1297 .build()
1298 .unwrap_err();
1299 assert_eq!(
1300 err.to_string(),
1301 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1302 );
1303 }
1304
1305 #[test]
1306 fn violation_action_builder_accumulates_passthrough_hosts() {
1307 let action = ViolationActionBuilder::default()
1308 .block()
1309 .passthrough_host("google.com")
1310 .passthrough_host("facebook.com")
1311 .build();
1312
1313 assert_eq!(
1314 action,
1315 ViolationAction::Passthrough(vec![
1316 HostPattern::Exact("google.com".into()),
1317 HostPattern::Exact("facebook.com".into()),
1318 ]),
1319 );
1320 }
1321}