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_header_fields(
743 mut self,
744 fields: impl IntoIterator<Item = impl Into<String>>,
745 ) -> Self {
746 self.substitution.headers = true;
747 self.substitution.header_fields = fields.into_iter().map(Into::into).collect();
748 self
749 }
750
751 pub fn substitute_in_query(mut self, enabled: bool) -> Self {
753 self.substitution.query = enabled;
754 self
755 }
756
757 pub fn substitute_in_body(mut self, enabled: bool) -> Self {
764 self.substitution.body = enabled;
765 self
766 }
767
768 pub fn build(self) -> SecretEntry {
778 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
779 assert!(
780 self.value.is_some() ^ self.source.is_some(),
781 "SecretBuilder: exactly one of .value() or .source() is required"
782 );
783 assert!(
784 !self.allowed_hosts.is_empty(),
785 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
786 );
787 let placeholder = self
788 .placeholder
789 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
790
791 SecretEntry {
792 env_var,
793 value: Zeroizing::new(self.value.unwrap_or_default()),
794 source: self.source,
795 placeholder,
796 allowed_hosts: self.allowed_hosts,
797 substitution: self.substitution,
798 passthrough_hosts: self.passthrough_hosts,
799 violation_action: self.violation_action,
800 require_tls_identity: self.require_tls_identity,
801 }
802 }
803}
804
805impl NetworkRateLimiterBuilder {
806 fn new() -> Self {
807 Self::default()
808 }
809
810 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
812 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
813 Ok(limiter) => self.config.egress = Some(limiter),
814 Err(err) => self.errors.push(err),
815 }
816 self
817 }
818
819 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
821 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
822 Ok(limiter) => self.config.ingress = Some(limiter),
823 Err(err) => self.errors.push(err),
824 }
825 self
826 }
827
828 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
830 if let Some(error) = self.errors.drain(..).next() {
831 return Err(error);
832 }
833 if self.config.egress.is_none() && self.config.ingress.is_none() {
834 return Err(BuildError::EmptyNetworkRateLimiter);
835 }
836 Ok(self.config)
837 }
838}
839
840impl RateLimiterBuilder {
841 fn new(direction: NetworkRateLimitDirection) -> Self {
842 Self {
843 direction,
844 bandwidth: None,
845 ops: None,
846 bandwidth_burst: None,
847 ops_burst: None,
848 refill_error: None,
849 }
850 }
851
852 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
861 match refill_time_ms(refill_time) {
862 Ok(refill_time_ms) => {
863 self.bandwidth = Some(TokenBucketConfig {
864 size: size.into().as_u64(),
865 refill_time_ms,
866 one_time_burst: 0,
867 });
868 }
869 Err(error) => {
870 self.refill_error.get_or_insert(("bandwidth", error));
871 }
872 }
873 self
874 }
875
876 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
879 self.bandwidth_burst = Some(burst.into().as_u64());
880 self
881 }
882
883 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
892 match refill_time_ms(refill_time) {
893 Ok(refill_time_ms) => {
894 self.ops = Some(TokenBucketConfig {
895 size: count,
896 refill_time_ms,
897 one_time_burst: 0,
898 });
899 }
900 Err(error) => {
901 self.refill_error.get_or_insert(("ops", error));
902 }
903 }
904 self
905 }
906
907 pub fn ops_burst(mut self, count: u64) -> Self {
910 self.ops_burst = Some(count);
911 self
912 }
913
914 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
916 let direction = self.direction;
917 if let Some((bucket, error)) = self.refill_error {
918 return Err(match error {
919 RefillTimeError::TooShort => {
920 BuildError::RateLimitRefillTooShort { direction, bucket }
921 }
922 RefillTimeError::Precision => {
923 BuildError::RateLimitRefillPrecision { direction, bucket }
924 }
925 RefillTimeError::TooLong => {
926 BuildError::RateLimitRefillTooLong { direction, bucket }
927 }
928 });
929 }
930
931 let mut config = RateLimiterConfig {
932 bandwidth: self.bandwidth,
933 ops: self.ops,
934 };
935 if let Some(burst) = self.bandwidth_burst {
936 let Some(bandwidth) = &mut config.bandwidth else {
937 return Err(BuildError::RateLimitBurstWithoutBucket {
938 direction,
939 bucket: "bandwidth",
940 });
941 };
942 bandwidth.one_time_burst = burst;
943 }
944 if let Some(burst) = self.ops_burst {
945 let Some(ops) = &mut config.ops else {
946 return Err(BuildError::RateLimitBurstWithoutBucket {
947 direction,
948 bucket: "ops",
949 });
950 };
951 ops.one_time_burst = burst;
952 }
953
954 config
955 .validate()
956 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
957 Ok(config)
958 }
959}
960
961fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
967 if refill_time < Duration::from_millis(1) {
968 return Err(RefillTimeError::TooShort);
969 }
970 let refill_time_ms =
971 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
972 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
973 return Err(RefillTimeError::Precision);
974 }
975 Ok(refill_time_ms)
976}
977
978impl Default for NetworkBuilder {
983 fn default() -> Self {
984 Self::new()
985 }
986}
987
988impl Default for TlsBuilder {
989 fn default() -> Self {
990 Self::new()
991 }
992}
993
994impl Default for SecretBuilder {
995 fn default() -> Self {
996 Self::new()
997 }
998}
999#[cfg(test)]
1004mod tests {
1005 use super::*;
1006
1007 #[test]
1008 #[allow(deprecated)]
1009 fn deprecated_tcp_builder_delegates_to_the_new_name() {
1010 let canonical_last = NetworkBuilder::new()
1011 .max_connections(0)
1012 .max_tcp_connections(64)
1013 .build()
1014 .unwrap();
1015 assert_eq!(
1016 canonical_last.max_tcp_connections,
1017 Some(ConnectionLimit::from(64))
1018 );
1019 let legacy_last = NetworkBuilder::new()
1020 .max_tcp_connections(64)
1021 .max_connections(0)
1022 .build()
1023 .unwrap();
1024 assert_eq!(
1025 legacy_last.max_tcp_connections,
1026 Some(ConnectionLimit::Unlimited)
1027 );
1028 let config = NetworkBuilder::new()
1029 .max_connections(0)
1030 .max_udp_connections(7)
1031 .build()
1032 .unwrap();
1033 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
1034 assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
1035 assert!(
1036 NetworkBuilder::new()
1037 .max_tcp_connections(1)
1038 .max_tcp_connections(2)
1039 .build()
1040 .is_ok()
1041 );
1042 }
1043
1044 #[test]
1045 fn tcp_accept_queue_size_is_unset_by_default_and_rejects_out_of_range_values() {
1046 assert_eq!(
1047 NetworkBuilder::new().build().unwrap().tcp_accept_queue_size,
1048 None
1049 );
1050 let config = NetworkBuilder::new()
1051 .tcp_accept_queue_size(4096)
1052 .build()
1053 .unwrap();
1054 assert_eq!(
1055 config.tcp_accept_queue_size.map(TcpAcceptQueueSize::get),
1056 Some(4096)
1057 );
1058
1059 for invalid in [0, TcpAcceptQueueSize::MAX + 1] {
1060 let err = NetworkBuilder::new()
1061 .tcp_accept_queue_size(invalid)
1062 .build()
1063 .unwrap_err();
1064 assert!(
1065 matches!(
1066 err,
1067 BuildError::InvalidTcpAcceptQueueSize { source } if source.value == invalid
1068 ),
1069 "{invalid}: {err}"
1070 );
1071 }
1072 }
1073
1074 #[test]
1076 fn network_builder_happy_path_returns_config() {
1077 let cfg = NetworkBuilder::new()
1078 .dns(|d| d.rebind_protection(false))
1079 .build()
1080 .unwrap();
1081 assert!(!cfg.dns.rebind_protection);
1082 }
1083
1084 #[test]
1085 fn network_builder_preserves_explicit_large_caps() {
1086 for limit in [10000, usize::MAX] {
1087 let config = NetworkBuilder::new()
1088 .max_tcp_connections(limit)
1089 .build()
1090 .unwrap();
1091 assert_eq!(
1092 config.max_tcp_connections,
1093 Some(ConnectionLimit::from(limit))
1094 );
1095 }
1096 }
1097
1098 #[test]
1099 fn network_builder_rejects_incomplete_intercept_ca_config() {
1100 let err = NetworkBuilder::new()
1101 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1102 .build()
1103 .unwrap_err();
1104
1105 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1106 }
1107
1108 #[test]
1109 fn network_builder_rejects_non_96_nat64_prefix() {
1110 let err = NetworkBuilder::new()
1111 .nat64_prefix("64:ff9b::/64".parse().unwrap())
1112 .build()
1113 .unwrap_err();
1114
1115 assert!(matches!(err, BuildError::InvalidNat64Prefix { .. }));
1116 }
1117
1118 #[test]
1119 fn port_bind_sets_host_bind() {
1120 let bind = "0.0.0.0".parse().unwrap();
1121 let cfg = NetworkBuilder::new()
1122 .port_bind(bind, 8080, 80)
1123 .port_udp_bind(bind, 5353, 53)
1124 .build()
1125 .unwrap();
1126
1127 assert_eq!(cfg.ports[0].host_bind, bind);
1128 assert_eq!(cfg.ports[0].host_port, 8080);
1129 assert_eq!(cfg.ports[0].guest_port, 80);
1130 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1131 assert_eq!(cfg.ports[1].host_bind, bind);
1132 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1133 }
1134
1135 #[test]
1136 fn port_helpers_default_to_loopback() {
1137 let cfg = NetworkBuilder::new()
1138 .port(8080, 80)
1139 .port_udp(5353, 53)
1140 .build()
1141 .unwrap();
1142
1143 assert_eq!(
1144 cfg.ports[0].host_bind,
1145 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1146 );
1147 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1148 assert_eq!(
1149 cfg.ports[1].host_bind,
1150 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1151 );
1152 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1153 }
1154
1155 #[test]
1156 fn outbound_proxy_defaults_to_none() {
1157 let cfg = NetworkBuilder::new().build().unwrap();
1158 assert_eq!(cfg.outbound_proxy, None);
1159 }
1160
1161 #[test]
1162 fn network_builder_sets_strict_mode() {
1163 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1164 assert!(cfg.strict);
1165 }
1166
1167 #[test]
1168 fn network_builder_sets_global_violation_action() {
1169 let cfg = NetworkBuilder::new()
1170 .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1171 .build()
1172 .unwrap();
1173
1174 assert_eq!(
1175 cfg.secrets.violation_action,
1176 SecretViolationAction::BlockAndTerminate
1177 );
1178 }
1179
1180 #[test]
1181 #[allow(deprecated)] fn secret_builder_sets_passthrough_and_violation_policies() {
1183 let secret = SecretBuilder::new()
1184 .env("TOKEN")
1185 .value("secret-value")
1186 .allow("api.github.com")
1187 .allow_placeholder_for("api.anthropic.com")
1188 .allow_passthrough_for("*.anthropic.com")
1189 .violation_action(SecretViolationAction::BlockAndTerminate)
1190 .build();
1191
1192 assert_eq!(
1193 secret.violation_action,
1194 Some(SecretViolationAction::BlockAndTerminate),
1195 );
1196 assert_eq!(
1197 secret.passthrough_hosts,
1198 vec![
1199 HostPattern::Exact("api.anthropic.com".into()),
1200 HostPattern::Wildcard("*.anthropic.com".into()),
1201 ],
1202 );
1203 }
1204
1205 #[test]
1206 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1207 fn secret_builder_rejects_empty_allowed_hosts() {
1208 let _ = SecretBuilder::new()
1209 .env("TOKEN")
1210 .value("secret-value")
1211 .build();
1212 }
1213
1214 #[test]
1215 fn secret_builder_source_yields_reference_and_empty_value() {
1216 let secret = SecretBuilder::new()
1217 .env("API_KEY")
1218 .source(SecretSource::Env {
1219 var: "HOST_API_KEY".into(),
1220 })
1221 .allow("api.example.com")
1222 .build();
1223
1224 assert!(secret.value.is_empty());
1225 assert_eq!(
1226 secret.source,
1227 Some(SecretSource::Env {
1228 var: "HOST_API_KEY".into()
1229 })
1230 );
1231
1232 let json = serde_json::to_string(&secret).unwrap();
1234 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1235 }
1236
1237 #[test]
1238 #[should_panic(expected = "exactly one of .value() or .source()")]
1239 fn secret_builder_rejects_both_value_and_source() {
1240 let _ = SecretBuilder::new()
1241 .env("API_KEY")
1242 .value("inline")
1243 .source(SecretSource::Env {
1244 var: "HOST_API_KEY".into(),
1245 })
1246 .allow("api.example.com")
1247 .build();
1248 }
1249
1250 #[test]
1251 fn network_builder_rejects_invalid_secret_config() {
1252 let err = NetworkBuilder::new()
1253 .secret_entry(SecretEntry {
1254 env_var: "API=KEY".into(),
1255 value: Zeroizing::new("secret-value".into()),
1256 source: None,
1257 placeholder: "$MSB_API_KEY".into(),
1258 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1259 substitution: SecretSubstitution::default(),
1260 passthrough_hosts: Vec::new(),
1261 violation_action: None,
1262 require_tls_identity: true,
1263 })
1264 .build()
1265 .unwrap_err();
1266
1267 assert!(err.to_string().contains("env_var must not contain `=`"));
1268 }
1269
1270 #[test]
1271 fn rate_limiter_builder_sets_buckets_and_bursts() {
1272 use microsandbox_utils::size::SizeExt;
1273
1274 let cfg = NetworkBuilder::new()
1275 .rate_limiter(|r| {
1276 r.egress(|r| {
1277 r.bandwidth(1.mib(), Duration::from_secs(1))
1278 .bandwidth_burst(512.kib())
1279 .ops(1_000, Duration::from_secs(1))
1280 .ops_burst(500)
1281 })
1282 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1283 })
1284 .build()
1285 .unwrap();
1286
1287 let rate_limiter = cfg.rate_limiter.unwrap();
1288 let egress = rate_limiter.egress.unwrap();
1289 let bandwidth = egress.bandwidth.unwrap();
1290 assert_eq!(bandwidth.size, 1024 * 1024);
1291 assert_eq!(bandwidth.refill_time_ms, 1000);
1292 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1293 let ops = egress.ops.unwrap();
1294 assert_eq!(ops.size, 1_000);
1295 assert_eq!(ops.refill_time_ms, 1000);
1296 assert_eq!(ops.one_time_burst, 500);
1297
1298 let ingress = rate_limiter.ingress.unwrap();
1299 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1300 assert!(ingress.ops.is_none());
1301 }
1302
1303 #[test]
1304 fn rate_limiters_default_to_unlimited() {
1305 let cfg = NetworkBuilder::new().build().unwrap();
1306 assert!(cfg.rate_limiter.is_none());
1307 }
1308
1309 #[test]
1310 fn rate_limiter_builder_rejects_empty_limiter() {
1311 let err = NetworkBuilder::new()
1312 .rate_limiter(|r| r.egress(|r| r))
1313 .build()
1314 .unwrap_err();
1315 assert_eq!(
1316 err.to_string(),
1317 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1318 );
1319 }
1320
1321 #[test]
1322 fn network_rate_limiter_builder_rejects_missing_directions() {
1323 let err = NetworkBuilder::new()
1324 .rate_limiter(|r| r)
1325 .build()
1326 .unwrap_err();
1327 assert_eq!(
1328 err.to_string(),
1329 "rate limiter must configure at least one of egress or ingress"
1330 );
1331 }
1332
1333 #[test]
1334 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1335 let err = NetworkBuilder::new()
1336 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1337 .build()
1338 .unwrap_err();
1339 assert_eq!(
1340 err.to_string(),
1341 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1342 );
1343
1344 let err = NetworkBuilder::new()
1345 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1346 .build()
1347 .unwrap_err();
1348 assert_eq!(
1349 err.to_string(),
1350 "egress rate limiter: ops refill interval must be at least one millisecond"
1351 );
1352
1353 let err = NetworkBuilder::new()
1354 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1355 .build()
1356 .unwrap_err();
1357 assert_eq!(
1358 err.to_string(),
1359 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1360 );
1361 }
1362
1363 #[test]
1364 fn rate_limiter_builder_rejects_burst_without_bucket() {
1365 use microsandbox_utils::size::SizeExt;
1366
1367 let err = NetworkBuilder::new()
1368 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1369 .build()
1370 .unwrap_err();
1371 assert_eq!(
1372 err.to_string(),
1373 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1374 );
1375
1376 let err = NetworkBuilder::new()
1377 .rate_limiter(|r| {
1378 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1379 })
1380 .build()
1381 .unwrap_err();
1382 assert_eq!(
1383 err.to_string(),
1384 "ingress rate limiter: ops_burst requires the ops bucket"
1385 );
1386 }
1387
1388 #[test]
1389 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1390 let err = NetworkBuilder::new()
1391 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1392 .build()
1393 .unwrap_err();
1394 assert_eq!(
1395 err.to_string(),
1396 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1397 );
1398 }
1399}