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, HttpConfig, InterfaceOverrides, NetworkConfig, PortProtocol,
19 PublishedPort, TcpAcceptQueueSize,
20};
21use crate::dns::Nameserver;
22use crate::policy::{BuildError, NetworkPolicy};
23use crate::secrets::config::{
24 HostPattern, SecretEntry, SecretSource, SecretSubstitution, SecretViolationAction,
25};
26
27#[derive(Clone)]
33pub struct NetworkBuilder {
34 config: NetworkConfig,
35 errors: Vec<BuildError>,
36}
37
38#[derive(Default)]
40pub struct HttpBuilder {
41 config: HttpConfig,
42}
43
44pub struct DnsBuilder {
46 config: DnsConfig,
47}
48
49pub struct TlsBuilder {
51 config: TlsConfig,
52}
53
54pub struct SecretBuilder {
64 env_var: Option<String>,
65 value: Option<String>,
66 source: Option<SecretSource>,
67 placeholder: Option<String>,
68 allowed_hosts: Vec<HostPattern>,
69 substitution: SecretSubstitution,
70 passthrough_hosts: Vec<HostPattern>,
71 violation_action: Option<SecretViolationAction>,
72 require_tls_identity: bool,
73}
74
75#[derive(Default)]
84pub struct NetworkRateLimiterBuilder {
85 config: NetworkRateLimiterConfig,
86 errors: Vec<BuildError>,
87}
88
89pub struct RateLimiterBuilder {
100 direction: NetworkRateLimitDirection,
101 bandwidth: Option<TokenBucketConfig>,
102 ops: Option<TokenBucketConfig>,
103 bandwidth_burst: Option<u64>,
104 ops_burst: Option<u64>,
105 refill_error: Option<(&'static str, RefillTimeError)>,
107}
108
109#[derive(Clone, Copy, Debug)]
110enum RefillTimeError {
111 TooShort,
112 Precision,
113 TooLong,
114}
115
116impl HttpBuilder {
121 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn deny_response(mut self, enabled: bool) -> Self {
128 self.config.deny_response = enabled;
129 self
130 }
131
132 pub fn deny_message(mut self, message: impl Into<String>) -> Self {
136 self.config.deny_message = Some(message.into());
137 self
138 }
139
140 pub fn build(self) -> HttpConfig {
142 self.config
143 }
144}
145
146impl NetworkBuilder {
147 pub fn new() -> Self {
149 Self {
150 config: NetworkConfig::default(),
151 errors: Vec::new(),
152 }
153 }
154
155 pub fn from_config(config: NetworkConfig) -> Self {
157 Self {
158 config,
159 errors: Vec::new(),
160 }
161 }
162
163 pub fn enabled(mut self, enabled: bool) -> Self {
165 self.config.enabled = enabled;
166 self
167 }
168
169 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
171 self.port_bind(
172 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
173 host_port,
174 guest_port,
175 )
176 }
177
178 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
180 self.port_udp_bind(
181 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
182 host_port,
183 guest_port,
184 )
185 }
186
187 pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
189 self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
190 }
191
192 pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
194 self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
195 }
196
197 fn add_port(
198 mut self,
199 host_bind: IpAddr,
200 host_port: u16,
201 guest_port: u16,
202 protocol: PortProtocol,
203 ) -> Self {
204 self.config.ports.push(PublishedPort {
205 host_port,
206 guest_port,
207 protocol,
208 host_bind,
209 });
210 self
211 }
212
213 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
215 self.config.policy = policy;
216 self
217 }
218
219 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
228 self.config.dns = f(DnsBuilder::new()).build();
229 self
230 }
231
232 #[doc(hidden)]
234 pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
235 self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
236 self
237 }
238
239 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
241 self.config.tls = f(TlsBuilder::new()).build();
242 self
243 }
244
245 #[doc(hidden)]
247 pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
248 self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
249 self
250 }
251
252 pub fn strict(mut self, enabled: bool) -> Self {
254 self.config.strict = enabled;
255 self
256 }
257
258 pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
268 self.secret_entry(f(SecretBuilder::new()).build())
269 }
270
271 pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
273 self.config.secrets.secrets.push(entry);
274 self
275 }
276
277 pub fn secret_env(
279 mut self,
280 env_var: impl Into<String>,
281 value: impl Into<String>,
282 placeholder: impl Into<String>,
283 allowed_host: impl Into<String>,
284 ) -> Self {
285 self.config.secrets.secrets.push(SecretEntry {
286 env_var: env_var.into(),
287 value: Zeroizing::new(value.into()),
288 source: None,
289 placeholder: placeholder.into(),
290 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
291 substitution: SecretSubstitution::default(),
292 passthrough_hosts: Vec::new(),
293 violation_action: None,
294 require_tls_identity: true,
295 });
296 self
297 }
298
299 pub fn secret_violation_action(mut self, action: SecretViolationAction) -> Self {
301 self.config.secrets.violation_action = action;
302 self
303 }
304
305 #[deprecated(note = "use max_tcp_connections instead")]
307 pub fn max_connections(self, max: usize) -> Self {
308 self.max_tcp_connections(max)
309 }
310
311 pub fn max_tcp_connections(mut self, max: usize) -> Self {
313 self.config.max_tcp_connections = Some(ConnectionLimit::from(max));
314 self
315 }
316
317 pub fn max_udp_connections(mut self, max: usize) -> Self {
319 self.config.max_udp_connections = Some(ConnectionLimit::from(max));
320 self
321 }
322
323 pub fn tcp_accept_queue_size(mut self, size: u32) -> Self {
329 match TcpAcceptQueueSize::try_from(size) {
330 Ok(size) => self.config.tcp_accept_queue_size = Some(size),
331 Err(err) => self.errors.push(err.into()),
332 }
333 self
334 }
335
336 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
338 self.config.interface = overrides;
339 self
340 }
341
342 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
346 if pool.prefix() > 30 {
347 self.errors.push(BuildError::InvalidIpv4Pool {
348 raw: pool.to_string(),
349 });
350 } else {
351 self.config.interface.ipv4_pool = Some(pool);
352 }
353 self
354 }
355
356 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
360 if pool.prefix() > 64 {
361 self.errors.push(BuildError::InvalidIpv6Pool {
362 raw: pool.to_string(),
363 });
364 } else {
365 self.config.interface.ipv6_pool = Some(pool);
366 }
367 self
368 }
369
370 pub fn nat64_prefix(mut self, prefix: Ipv6Network) -> Self {
376 if prefix.prefix() != 96 {
377 self.errors.push(BuildError::InvalidNat64Prefix {
378 raw: prefix.to_string(),
379 });
380 } else if !self.config.nat64_prefixes.contains(&prefix) {
381 self.config.nat64_prefixes.push(prefix);
382 }
383
384 self
385 }
386
387 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
393 self.config.trust_host_cas = enabled;
394 self
395 }
396
397 pub fn http(mut self, configure: impl FnOnce(HttpBuilder) -> HttpBuilder) -> Self {
399 self.config.http = configure(HttpBuilder {
400 config: self.config.http,
401 })
402 .build();
403 self
404 }
405
406 pub fn rate_limiter(
417 mut self,
418 f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
419 ) -> Self {
420 match f(NetworkRateLimiterBuilder::new()).build() {
421 Ok(limiter) => self.config.rate_limiter = Some(limiter),
422 Err(err) => self.errors.push(err),
423 }
424 self
425 }
426
427 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
433 if let Some(err) = self.errors.drain(..).next() {
434 return Err(err);
435 }
436 if let Some(prefix) = self
437 .config
438 .nat64_prefixes
439 .iter()
440 .find(|prefix| prefix.prefix() != 96)
441 {
442 return Err(BuildError::InvalidNat64Prefix {
443 raw: prefix.to_string(),
444 });
445 }
446 if self.config.tls.enabled
447 && (self.config.tls.intercept_ca.cert_path.is_some()
448 != self.config.tls.intercept_ca.key_path.is_some())
449 {
450 return Err(BuildError::IncompleteInterceptCaConfig);
451 }
452 self.config.secrets.validate()?;
453 Ok(self.config)
454 }
455}
456
457impl DnsBuilder {
458 pub fn new() -> Self {
460 Self {
461 config: DnsConfig::default(),
462 }
463 }
464
465 fn from_config(config: DnsConfig) -> Self {
466 Self { config }
467 }
468
469 pub fn rebind_protection(mut self, enabled: bool) -> Self {
471 self.config.rebind_protection = enabled;
472 self
473 }
474
475 pub fn nameservers<I>(mut self, nameservers: I) -> Self
482 where
483 I: IntoIterator,
484 I::Item: Into<Nameserver>,
485 {
486 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
487 self
488 }
489
490 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
492 self.config.query_timeout_ms = ms;
493 self
494 }
495
496 pub fn build(self) -> DnsConfig {
498 self.config
499 }
500}
501
502impl Default for DnsBuilder {
503 fn default() -> Self {
504 Self::new()
505 }
506}
507
508impl TlsBuilder {
509 pub fn new() -> Self {
511 Self {
512 config: TlsConfig {
513 enabled: true,
514 ..TlsConfig::default()
515 },
516 }
517 }
518
519 fn from_config(config: TlsConfig) -> Self {
520 Self { config }
521 }
522
523 pub fn enabled(mut self, enabled: bool) -> Self {
525 self.config.enabled = enabled;
526 self
527 }
528
529 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
531 self.config.bypass.push(pattern.into());
532 self
533 }
534
535 pub fn verify_upstream(mut self, verify: bool) -> Self {
537 self.config.verify_upstream = verify;
538 self
539 }
540
541 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
547 self.config
548 .scoped_verify_upstream
549 .push(ScopedVerifyUpstream {
550 pattern: pattern.into(),
551 verify,
552 });
553 self
554 }
555
556 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
558 self.config.intercepted_ports = ports;
559 self
560 }
561
562 pub fn block_quic(mut self, block: bool) -> Self {
564 self.config.block_quic_on_intercept = block;
565 self
566 }
567
568 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
573 self.config.upstream_ca_cert.push(path.into());
574 self
575 }
576
577 pub fn upstream_ca_cert_for(
584 mut self,
585 pattern: impl Into<String>,
586 path: impl Into<PathBuf>,
587 ) -> Self {
588 self.config
589 .scoped_upstream_ca_cert
590 .push(ScopedUpstreamCaCert {
591 pattern: pattern.into(),
592 path: path.into(),
593 });
594 self
595 }
596
597 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
599 self.config.intercept_ca.cert_path = Some(path.into());
600 self
601 }
602
603 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
605 self.config.intercept_ca.key_path = Some(path.into());
606 self
607 }
608
609 pub fn build(self) -> TlsConfig {
611 self.config
612 }
613}
614
615impl SecretBuilder {
616 pub fn new() -> Self {
618 Self {
619 env_var: None,
620 value: None,
621 source: None,
622 placeholder: None,
623 allowed_hosts: Vec::new(),
624 substitution: SecretSubstitution::default(),
625 passthrough_hosts: Vec::new(),
626 violation_action: None,
627 require_tls_identity: true,
628 }
629 }
630
631 pub fn env(mut self, var: impl Into<String>) -> Self {
636 self.env_var = Some(var.into());
637 self
638 }
639
640 pub fn value(mut self, value: impl Into<String>) -> Self {
646 self.value = Some(value.into());
647 self
648 }
649
650 pub fn source(mut self, source: SecretSource) -> Self {
657 self.source = Some(source);
658 self
659 }
660
661 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
667 self.placeholder = Some(placeholder.into());
668 self
669 }
670
671 pub fn allow(mut self, host: impl AsRef<str>) -> Self {
679 let host = host.as_ref();
680 assert!(
681 host != "*",
682 "SecretBuilder: use .allow_any_host_dangerous(true) for an explicit any-host secret"
683 );
684 self.allowed_hosts.push(HostPattern::parse(host));
685 self
686 }
687
688 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
691 if i_understand_the_risk {
692 self.allowed_hosts.push(HostPattern::Any);
693 }
694 self
695 }
696
697 pub fn allow_placeholder_for(mut self, host: impl AsRef<str>) -> Self {
702 self.passthrough_hosts
703 .push(HostPattern::parse(host.as_ref()));
704 self
705 }
706
707 #[deprecated(note = "use allow_placeholder_for instead")]
709 pub fn allow_passthrough_for(self, host: impl AsRef<str>) -> Self {
710 self.allow_placeholder_for(host)
711 }
712
713 pub fn violation_action(mut self, action: SecretViolationAction) -> Self {
715 self.violation_action = Some(action);
716 self
717 }
718
719 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
721 self.require_tls_identity = enabled;
722 self
723 }
724
725 pub fn substitute_in_headers(mut self, enabled: bool) -> Self {
727 self.substitution.headers = enabled;
728 self
729 }
730
731 pub fn substitute_in_query(mut self, enabled: bool) -> Self {
733 self.substitution.query = enabled;
734 self
735 }
736
737 pub fn substitute_in_body(mut self, enabled: bool) -> Self {
744 self.substitution.body = enabled;
745 self
746 }
747
748 pub fn build(self) -> SecretEntry {
758 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
759 assert!(
760 self.value.is_some() ^ self.source.is_some(),
761 "SecretBuilder: exactly one of .value() or .source() is required"
762 );
763 assert!(
764 !self.allowed_hosts.is_empty(),
765 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
766 );
767 let placeholder = self
768 .placeholder
769 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
770
771 SecretEntry {
772 env_var,
773 value: Zeroizing::new(self.value.unwrap_or_default()),
774 source: self.source,
775 placeholder,
776 allowed_hosts: self.allowed_hosts,
777 substitution: self.substitution,
778 passthrough_hosts: self.passthrough_hosts,
779 violation_action: self.violation_action,
780 require_tls_identity: self.require_tls_identity,
781 }
782 }
783}
784
785impl NetworkRateLimiterBuilder {
786 fn new() -> Self {
787 Self::default()
788 }
789
790 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
792 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
793 Ok(limiter) => self.config.egress = Some(limiter),
794 Err(err) => self.errors.push(err),
795 }
796 self
797 }
798
799 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
801 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
802 Ok(limiter) => self.config.ingress = Some(limiter),
803 Err(err) => self.errors.push(err),
804 }
805 self
806 }
807
808 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
810 if let Some(error) = self.errors.drain(..).next() {
811 return Err(error);
812 }
813 if self.config.egress.is_none() && self.config.ingress.is_none() {
814 return Err(BuildError::EmptyNetworkRateLimiter);
815 }
816 Ok(self.config)
817 }
818}
819
820impl RateLimiterBuilder {
821 fn new(direction: NetworkRateLimitDirection) -> Self {
822 Self {
823 direction,
824 bandwidth: None,
825 ops: None,
826 bandwidth_burst: None,
827 ops_burst: None,
828 refill_error: None,
829 }
830 }
831
832 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
841 match refill_time_ms(refill_time) {
842 Ok(refill_time_ms) => {
843 self.bandwidth = Some(TokenBucketConfig {
844 size: size.into().as_u64(),
845 refill_time_ms,
846 one_time_burst: 0,
847 });
848 }
849 Err(error) => {
850 self.refill_error.get_or_insert(("bandwidth", error));
851 }
852 }
853 self
854 }
855
856 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
859 self.bandwidth_burst = Some(burst.into().as_u64());
860 self
861 }
862
863 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
872 match refill_time_ms(refill_time) {
873 Ok(refill_time_ms) => {
874 self.ops = Some(TokenBucketConfig {
875 size: count,
876 refill_time_ms,
877 one_time_burst: 0,
878 });
879 }
880 Err(error) => {
881 self.refill_error.get_or_insert(("ops", error));
882 }
883 }
884 self
885 }
886
887 pub fn ops_burst(mut self, count: u64) -> Self {
890 self.ops_burst = Some(count);
891 self
892 }
893
894 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
896 let direction = self.direction;
897 if let Some((bucket, error)) = self.refill_error {
898 return Err(match error {
899 RefillTimeError::TooShort => {
900 BuildError::RateLimitRefillTooShort { direction, bucket }
901 }
902 RefillTimeError::Precision => {
903 BuildError::RateLimitRefillPrecision { direction, bucket }
904 }
905 RefillTimeError::TooLong => {
906 BuildError::RateLimitRefillTooLong { direction, bucket }
907 }
908 });
909 }
910
911 let mut config = RateLimiterConfig {
912 bandwidth: self.bandwidth,
913 ops: self.ops,
914 };
915 if let Some(burst) = self.bandwidth_burst {
916 let Some(bandwidth) = &mut config.bandwidth else {
917 return Err(BuildError::RateLimitBurstWithoutBucket {
918 direction,
919 bucket: "bandwidth",
920 });
921 };
922 bandwidth.one_time_burst = burst;
923 }
924 if let Some(burst) = self.ops_burst {
925 let Some(ops) = &mut config.ops else {
926 return Err(BuildError::RateLimitBurstWithoutBucket {
927 direction,
928 bucket: "ops",
929 });
930 };
931 ops.one_time_burst = burst;
932 }
933
934 config
935 .validate()
936 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
937 Ok(config)
938 }
939}
940
941fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
947 if refill_time < Duration::from_millis(1) {
948 return Err(RefillTimeError::TooShort);
949 }
950 let refill_time_ms =
951 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
952 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
953 return Err(RefillTimeError::Precision);
954 }
955 Ok(refill_time_ms)
956}
957
958impl Default for NetworkBuilder {
963 fn default() -> Self {
964 Self::new()
965 }
966}
967
968impl Default for TlsBuilder {
969 fn default() -> Self {
970 Self::new()
971 }
972}
973
974impl Default for SecretBuilder {
975 fn default() -> Self {
976 Self::new()
977 }
978}
979#[cfg(test)]
984mod tests {
985 use super::*;
986
987 #[test]
988 #[allow(deprecated)]
989 fn deprecated_tcp_builder_delegates_to_the_new_name() {
990 let canonical_last = NetworkBuilder::new()
991 .max_connections(0)
992 .max_tcp_connections(64)
993 .build()
994 .unwrap();
995 assert_eq!(
996 canonical_last.max_tcp_connections,
997 Some(ConnectionLimit::from(64))
998 );
999 let legacy_last = NetworkBuilder::new()
1000 .max_tcp_connections(64)
1001 .max_connections(0)
1002 .build()
1003 .unwrap();
1004 assert_eq!(
1005 legacy_last.max_tcp_connections,
1006 Some(ConnectionLimit::Unlimited)
1007 );
1008 let config = NetworkBuilder::new()
1009 .max_connections(0)
1010 .max_udp_connections(7)
1011 .build()
1012 .unwrap();
1013 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
1014 assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
1015 assert!(
1016 NetworkBuilder::new()
1017 .max_tcp_connections(1)
1018 .max_tcp_connections(2)
1019 .build()
1020 .is_ok()
1021 );
1022 }
1023
1024 #[test]
1025 fn tcp_accept_queue_size_is_unset_by_default_and_rejects_out_of_range_values() {
1026 assert_eq!(
1027 NetworkBuilder::new().build().unwrap().tcp_accept_queue_size,
1028 None
1029 );
1030 let config = NetworkBuilder::new()
1031 .tcp_accept_queue_size(4096)
1032 .build()
1033 .unwrap();
1034 assert_eq!(
1035 config.tcp_accept_queue_size.map(TcpAcceptQueueSize::get),
1036 Some(4096)
1037 );
1038
1039 for invalid in [0, TcpAcceptQueueSize::MAX + 1] {
1040 let err = NetworkBuilder::new()
1041 .tcp_accept_queue_size(invalid)
1042 .build()
1043 .unwrap_err();
1044 assert!(
1045 matches!(
1046 err,
1047 BuildError::InvalidTcpAcceptQueueSize { source } if source.value == invalid
1048 ),
1049 "{invalid}: {err}"
1050 );
1051 }
1052 }
1053
1054 #[test]
1056 fn network_builder_happy_path_returns_config() {
1057 let cfg = NetworkBuilder::new()
1058 .dns(|d| d.rebind_protection(false))
1059 .build()
1060 .unwrap();
1061 assert!(!cfg.dns.rebind_protection);
1062 }
1063
1064 #[test]
1065 fn network_builder_preserves_explicit_large_caps() {
1066 for limit in [10000, usize::MAX] {
1067 let config = NetworkBuilder::new()
1068 .max_tcp_connections(limit)
1069 .build()
1070 .unwrap();
1071 assert_eq!(
1072 config.max_tcp_connections,
1073 Some(ConnectionLimit::from(limit))
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn network_builder_rejects_incomplete_intercept_ca_config() {
1080 let err = NetworkBuilder::new()
1081 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1082 .build()
1083 .unwrap_err();
1084
1085 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1086 }
1087
1088 #[test]
1089 fn network_builder_rejects_non_96_nat64_prefix() {
1090 let err = NetworkBuilder::new()
1091 .nat64_prefix("64:ff9b::/64".parse().unwrap())
1092 .build()
1093 .unwrap_err();
1094
1095 assert!(matches!(err, BuildError::InvalidNat64Prefix { .. }));
1096 }
1097
1098 #[test]
1099 fn port_bind_sets_host_bind() {
1100 let bind = "0.0.0.0".parse().unwrap();
1101 let cfg = NetworkBuilder::new()
1102 .port_bind(bind, 8080, 80)
1103 .port_udp_bind(bind, 5353, 53)
1104 .build()
1105 .unwrap();
1106
1107 assert_eq!(cfg.ports[0].host_bind, bind);
1108 assert_eq!(cfg.ports[0].host_port, 8080);
1109 assert_eq!(cfg.ports[0].guest_port, 80);
1110 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1111 assert_eq!(cfg.ports[1].host_bind, bind);
1112 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1113 }
1114
1115 #[test]
1116 fn port_helpers_default_to_loopback() {
1117 let cfg = NetworkBuilder::new()
1118 .port(8080, 80)
1119 .port_udp(5353, 53)
1120 .build()
1121 .unwrap();
1122
1123 assert_eq!(
1124 cfg.ports[0].host_bind,
1125 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1126 );
1127 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1128 assert_eq!(
1129 cfg.ports[1].host_bind,
1130 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1131 );
1132 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1133 }
1134
1135 #[test]
1136 fn outbound_proxy_defaults_to_none() {
1137 let cfg = NetworkBuilder::new().build().unwrap();
1138 assert_eq!(cfg.outbound_proxy, None);
1139 }
1140
1141 #[test]
1142 fn network_builder_sets_strict_mode() {
1143 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1144 assert!(cfg.strict);
1145 }
1146
1147 #[test]
1148 fn network_builder_sets_global_violation_action() {
1149 let cfg = NetworkBuilder::new()
1150 .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1151 .build()
1152 .unwrap();
1153
1154 assert_eq!(
1155 cfg.secrets.violation_action,
1156 SecretViolationAction::BlockAndTerminate
1157 );
1158 }
1159
1160 #[test]
1161 #[allow(deprecated)] fn secret_builder_sets_passthrough_and_violation_policies() {
1163 let secret = SecretBuilder::new()
1164 .env("TOKEN")
1165 .value("secret-value")
1166 .allow("api.github.com")
1167 .allow_placeholder_for("api.anthropic.com")
1168 .allow_passthrough_for("*.anthropic.com")
1169 .violation_action(SecretViolationAction::BlockAndTerminate)
1170 .build();
1171
1172 assert_eq!(
1173 secret.violation_action,
1174 Some(SecretViolationAction::BlockAndTerminate),
1175 );
1176 assert_eq!(
1177 secret.passthrough_hosts,
1178 vec![
1179 HostPattern::Exact("api.anthropic.com".into()),
1180 HostPattern::Wildcard("*.anthropic.com".into()),
1181 ],
1182 );
1183 }
1184
1185 #[test]
1186 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1187 fn secret_builder_rejects_empty_allowed_hosts() {
1188 let _ = SecretBuilder::new()
1189 .env("TOKEN")
1190 .value("secret-value")
1191 .build();
1192 }
1193
1194 #[test]
1195 fn secret_builder_source_yields_reference_and_empty_value() {
1196 let secret = SecretBuilder::new()
1197 .env("API_KEY")
1198 .source(SecretSource::Env {
1199 var: "HOST_API_KEY".into(),
1200 })
1201 .allow("api.example.com")
1202 .build();
1203
1204 assert!(secret.value.is_empty());
1205 assert_eq!(
1206 secret.source,
1207 Some(SecretSource::Env {
1208 var: "HOST_API_KEY".into()
1209 })
1210 );
1211
1212 let json = serde_json::to_string(&secret).unwrap();
1214 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1215 }
1216
1217 #[test]
1218 #[should_panic(expected = "exactly one of .value() or .source()")]
1219 fn secret_builder_rejects_both_value_and_source() {
1220 let _ = SecretBuilder::new()
1221 .env("API_KEY")
1222 .value("inline")
1223 .source(SecretSource::Env {
1224 var: "HOST_API_KEY".into(),
1225 })
1226 .allow("api.example.com")
1227 .build();
1228 }
1229
1230 #[test]
1231 fn network_builder_rejects_invalid_secret_config() {
1232 let err = NetworkBuilder::new()
1233 .secret_entry(SecretEntry {
1234 env_var: "API=KEY".into(),
1235 value: Zeroizing::new("secret-value".into()),
1236 source: None,
1237 placeholder: "$MSB_API_KEY".into(),
1238 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1239 substitution: SecretSubstitution::default(),
1240 passthrough_hosts: Vec::new(),
1241 violation_action: None,
1242 require_tls_identity: true,
1243 })
1244 .build()
1245 .unwrap_err();
1246
1247 assert!(err.to_string().contains("env_var must not contain `=`"));
1248 }
1249
1250 #[test]
1251 fn rate_limiter_builder_sets_buckets_and_bursts() {
1252 use microsandbox_utils::size::SizeExt;
1253
1254 let cfg = NetworkBuilder::new()
1255 .rate_limiter(|r| {
1256 r.egress(|r| {
1257 r.bandwidth(1.mib(), Duration::from_secs(1))
1258 .bandwidth_burst(512.kib())
1259 .ops(1_000, Duration::from_secs(1))
1260 .ops_burst(500)
1261 })
1262 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1263 })
1264 .build()
1265 .unwrap();
1266
1267 let rate_limiter = cfg.rate_limiter.unwrap();
1268 let egress = rate_limiter.egress.unwrap();
1269 let bandwidth = egress.bandwidth.unwrap();
1270 assert_eq!(bandwidth.size, 1024 * 1024);
1271 assert_eq!(bandwidth.refill_time_ms, 1000);
1272 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1273 let ops = egress.ops.unwrap();
1274 assert_eq!(ops.size, 1_000);
1275 assert_eq!(ops.refill_time_ms, 1000);
1276 assert_eq!(ops.one_time_burst, 500);
1277
1278 let ingress = rate_limiter.ingress.unwrap();
1279 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1280 assert!(ingress.ops.is_none());
1281 }
1282
1283 #[test]
1284 fn rate_limiters_default_to_unlimited() {
1285 let cfg = NetworkBuilder::new().build().unwrap();
1286 assert!(cfg.rate_limiter.is_none());
1287 }
1288
1289 #[test]
1290 fn rate_limiter_builder_rejects_empty_limiter() {
1291 let err = NetworkBuilder::new()
1292 .rate_limiter(|r| r.egress(|r| r))
1293 .build()
1294 .unwrap_err();
1295 assert_eq!(
1296 err.to_string(),
1297 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1298 );
1299 }
1300
1301 #[test]
1302 fn network_rate_limiter_builder_rejects_missing_directions() {
1303 let err = NetworkBuilder::new()
1304 .rate_limiter(|r| r)
1305 .build()
1306 .unwrap_err();
1307 assert_eq!(
1308 err.to_string(),
1309 "rate limiter must configure at least one of egress or ingress"
1310 );
1311 }
1312
1313 #[test]
1314 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1315 let err = NetworkBuilder::new()
1316 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1317 .build()
1318 .unwrap_err();
1319 assert_eq!(
1320 err.to_string(),
1321 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1322 );
1323
1324 let err = NetworkBuilder::new()
1325 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1326 .build()
1327 .unwrap_err();
1328 assert_eq!(
1329 err.to_string(),
1330 "egress rate limiter: ops refill interval must be at least one millisecond"
1331 );
1332
1333 let err = NetworkBuilder::new()
1334 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1335 .build()
1336 .unwrap_err();
1337 assert_eq!(
1338 err.to_string(),
1339 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1340 );
1341 }
1342
1343 #[test]
1344 fn rate_limiter_builder_rejects_burst_without_bucket() {
1345 use microsandbox_utils::size::SizeExt;
1346
1347 let err = NetworkBuilder::new()
1348 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1349 .build()
1350 .unwrap_err();
1351 assert_eq!(
1352 err.to_string(),
1353 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1354 );
1355
1356 let err = NetworkBuilder::new()
1357 .rate_limiter(|r| {
1358 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1359 })
1360 .build()
1361 .unwrap_err();
1362 assert_eq!(
1363 err.to_string(),
1364 "ingress rate limiter: ops_burst requires the ops bucket"
1365 );
1366 }
1367
1368 #[test]
1369 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1370 let err = NetworkBuilder::new()
1371 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1372 .build()
1373 .unwrap_err();
1374 assert_eq!(
1375 err.to_string(),
1376 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1377 );
1378 }
1379}