1use std::net::IpAddr;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use ipnetwork::{Ipv4Network, Ipv6Network};
10use microsandbox_types::{
11 HttpDenyResponseFormat, NetworkRateLimitDirection, NetworkRateLimiterConfig, RateLimiterConfig,
12 ScopedUpstreamCaCert, 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_response_format(mut self, format: HttpDenyResponseFormat) -> Self {
134 self.config.deny_response_format = format;
135 self
136 }
137
138 pub fn deny_message(mut self, message: impl Into<String>) -> Self {
141 self.config.deny_message = Some(message.into());
142 self
143 }
144
145 pub fn network_deny_message(mut self, message: impl Into<String>) -> Self {
149 self.config.network_deny_message = Some(message.into());
150 self
151 }
152
153 pub fn secret_deny_message(mut self, message: impl Into<String>) -> Self {
156 self.config.secret_deny_message = Some(message.into());
157 self
158 }
159
160 pub fn build(self) -> HttpConfig {
162 self.config
163 }
164}
165
166impl NetworkBuilder {
167 pub fn new() -> Self {
169 Self {
170 config: NetworkConfig::default(),
171 errors: Vec::new(),
172 }
173 }
174
175 pub fn from_config(config: NetworkConfig) -> Self {
177 Self {
178 config,
179 errors: Vec::new(),
180 }
181 }
182
183 pub fn enabled(mut self, enabled: bool) -> Self {
185 self.config.enabled = enabled;
186 self
187 }
188
189 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
191 self.port_bind(
192 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
193 host_port,
194 guest_port,
195 )
196 }
197
198 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
200 self.port_udp_bind(
201 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
202 host_port,
203 guest_port,
204 )
205 }
206
207 pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
209 self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
210 }
211
212 pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
214 self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
215 }
216
217 fn add_port(
218 mut self,
219 host_bind: IpAddr,
220 host_port: u16,
221 guest_port: u16,
222 protocol: PortProtocol,
223 ) -> Self {
224 self.config.ports.push(PublishedPort {
225 host_port,
226 guest_port,
227 protocol,
228 host_bind,
229 });
230 self
231 }
232
233 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
235 self.config.policy = policy;
236 self
237 }
238
239 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
248 self.config.dns = f(DnsBuilder::new()).build();
249 self
250 }
251
252 #[doc(hidden)]
254 pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
255 self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
256 self
257 }
258
259 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
261 self.config.tls = f(TlsBuilder::new()).build();
262 self
263 }
264
265 #[doc(hidden)]
267 pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
268 self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
269 self
270 }
271
272 pub fn strict(mut self, enabled: bool) -> Self {
274 self.config.strict = enabled;
275 self
276 }
277
278 pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
288 self.secret_entry(f(SecretBuilder::new()).build())
289 }
290
291 pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
293 self.config.secrets.secrets.push(entry);
294 self
295 }
296
297 pub fn secret_env(
299 mut self,
300 env_var: impl Into<String>,
301 value: impl Into<String>,
302 placeholder: impl Into<String>,
303 allowed_host: impl Into<String>,
304 ) -> Self {
305 self.config.secrets.secrets.push(SecretEntry {
306 env_var: env_var.into(),
307 value: Zeroizing::new(value.into()),
308 source: None,
309 placeholder: placeholder.into(),
310 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
311 substitution: SecretSubstitution::default(),
312 passthrough_hosts: Vec::new(),
313 violation_action: None,
314 require_tls_identity: true,
315 });
316 self
317 }
318
319 pub fn secret_violation_action(mut self, action: SecretViolationAction) -> Self {
321 self.config.secrets.violation_action = action;
322 self
323 }
324
325 #[deprecated(note = "use max_tcp_connections instead")]
327 pub fn max_connections(self, max: usize) -> Self {
328 self.max_tcp_connections(max)
329 }
330
331 pub fn max_tcp_connections(mut self, max: usize) -> Self {
333 self.config.max_tcp_connections = Some(ConnectionLimit::from(max));
334 self
335 }
336
337 pub fn max_inbound_tcp_connections(mut self, max: usize) -> Self {
339 self.config.max_inbound_tcp_connections = Some(ConnectionLimit::from(max));
340 self
341 }
342
343 pub fn max_udp_connections(mut self, max: usize) -> Self {
345 self.config.max_udp_connections = Some(ConnectionLimit::from(max));
346 self
347 }
348
349 pub fn tcp_accept_queue_size(mut self, size: u32) -> Self {
355 match TcpAcceptQueueSize::try_from(size) {
356 Ok(size) => self.config.tcp_accept_queue_size = Some(size),
357 Err(err) => self.errors.push(err.into()),
358 }
359 self
360 }
361
362 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
364 self.config.interface = overrides;
365 self
366 }
367
368 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
372 if pool.prefix() > 30 {
373 self.errors.push(BuildError::InvalidIpv4Pool {
374 raw: pool.to_string(),
375 });
376 } else {
377 self.config.interface.ipv4_pool = Some(pool);
378 }
379 self
380 }
381
382 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
386 if pool.prefix() > 64 {
387 self.errors.push(BuildError::InvalidIpv6Pool {
388 raw: pool.to_string(),
389 });
390 } else {
391 self.config.interface.ipv6_pool = Some(pool);
392 }
393 self
394 }
395
396 pub fn nat64_prefix(mut self, prefix: Ipv6Network) -> Self {
402 if prefix.prefix() != 96 {
403 self.errors.push(BuildError::InvalidNat64Prefix {
404 raw: prefix.to_string(),
405 });
406 } else if !self.config.nat64_prefixes.contains(&prefix) {
407 self.config.nat64_prefixes.push(prefix);
408 }
409
410 self
411 }
412
413 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
419 self.config.trust_host_cas = enabled;
420 self
421 }
422
423 pub fn http(mut self, configure: impl FnOnce(HttpBuilder) -> HttpBuilder) -> Self {
425 self.config.http = configure(HttpBuilder {
426 config: self.config.http,
427 })
428 .build();
429 self
430 }
431
432 pub fn rate_limiter(
443 mut self,
444 f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
445 ) -> Self {
446 match f(NetworkRateLimiterBuilder::new()).build() {
447 Ok(limiter) => self.config.rate_limiter = Some(limiter),
448 Err(err) => self.errors.push(err),
449 }
450 self
451 }
452
453 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
459 if let Some(err) = self.errors.drain(..).next() {
460 return Err(err);
461 }
462 if let Some(prefix) = self
463 .config
464 .nat64_prefixes
465 .iter()
466 .find(|prefix| prefix.prefix() != 96)
467 {
468 return Err(BuildError::InvalidNat64Prefix {
469 raw: prefix.to_string(),
470 });
471 }
472 if self.config.tls.enabled
473 && (self.config.tls.intercept_ca.cert_path.is_some()
474 != self.config.tls.intercept_ca.key_path.is_some())
475 {
476 return Err(BuildError::IncompleteInterceptCaConfig);
477 }
478 self.config.secrets.validate()?;
479 Ok(self.config)
480 }
481}
482
483impl DnsBuilder {
484 pub fn new() -> Self {
486 Self {
487 config: DnsConfig::default(),
488 }
489 }
490
491 fn from_config(config: DnsConfig) -> Self {
492 Self { config }
493 }
494
495 pub fn rebind_protection(mut self, enabled: bool) -> Self {
497 self.config.rebind_protection = enabled;
498 self
499 }
500
501 pub fn nameservers<I>(mut self, nameservers: I) -> Self
508 where
509 I: IntoIterator,
510 I::Item: Into<Nameserver>,
511 {
512 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
513 self
514 }
515
516 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
518 self.config.query_timeout_ms = ms;
519 self
520 }
521
522 pub fn build(self) -> DnsConfig {
524 self.config
525 }
526}
527
528impl Default for DnsBuilder {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534impl TlsBuilder {
535 pub fn new() -> Self {
537 Self {
538 config: TlsConfig {
539 enabled: true,
540 ..TlsConfig::default()
541 },
542 }
543 }
544
545 fn from_config(config: TlsConfig) -> Self {
546 Self { config }
547 }
548
549 pub fn enabled(mut self, enabled: bool) -> Self {
551 self.config.enabled = enabled;
552 self
553 }
554
555 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
557 self.config.bypass.push(pattern.into());
558 self
559 }
560
561 pub fn verify_upstream(mut self, verify: bool) -> Self {
563 self.config.verify_upstream = verify;
564 self
565 }
566
567 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
573 self.config
574 .scoped_verify_upstream
575 .push(ScopedVerifyUpstream {
576 pattern: pattern.into(),
577 verify,
578 });
579 self
580 }
581
582 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
584 self.config.intercepted_ports = ports;
585 self
586 }
587
588 pub fn block_quic(mut self, block: bool) -> Self {
590 self.config.block_quic_on_intercept = block;
591 self
592 }
593
594 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
599 self.config.upstream_ca_cert.push(path.into());
600 self
601 }
602
603 pub fn upstream_ca_cert_for(
610 mut self,
611 pattern: impl Into<String>,
612 path: impl Into<PathBuf>,
613 ) -> Self {
614 self.config
615 .scoped_upstream_ca_cert
616 .push(ScopedUpstreamCaCert {
617 pattern: pattern.into(),
618 path: path.into(),
619 });
620 self
621 }
622
623 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
625 self.config.intercept_ca.cert_path = Some(path.into());
626 self
627 }
628
629 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
631 self.config.intercept_ca.key_path = Some(path.into());
632 self
633 }
634
635 pub fn build(self) -> TlsConfig {
637 self.config
638 }
639}
640
641impl SecretBuilder {
642 pub fn new() -> Self {
644 Self {
645 env_var: None,
646 value: None,
647 source: None,
648 placeholder: None,
649 allowed_hosts: Vec::new(),
650 substitution: SecretSubstitution::default(),
651 passthrough_hosts: Vec::new(),
652 violation_action: None,
653 require_tls_identity: true,
654 }
655 }
656
657 pub fn env(mut self, var: impl Into<String>) -> Self {
662 self.env_var = Some(var.into());
663 self
664 }
665
666 pub fn value(mut self, value: impl Into<String>) -> Self {
672 self.value = Some(value.into());
673 self
674 }
675
676 pub fn source(mut self, source: SecretSource) -> Self {
683 self.source = Some(source);
684 self
685 }
686
687 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
693 self.placeholder = Some(placeholder.into());
694 self
695 }
696
697 pub fn allow(mut self, host: impl AsRef<str>) -> Self {
705 let host = host.as_ref();
706 assert!(
707 host != "*",
708 "SecretBuilder: use .allow_any_host_dangerous(true) for an explicit any-host secret"
709 );
710 self.allowed_hosts.push(HostPattern::parse(host));
711 self
712 }
713
714 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
717 if i_understand_the_risk {
718 self.allowed_hosts.push(HostPattern::Any);
719 }
720 self
721 }
722
723 pub fn allow_placeholder_for(mut self, host: impl AsRef<str>) -> Self {
728 self.passthrough_hosts
729 .push(HostPattern::parse(host.as_ref()));
730 self
731 }
732
733 #[deprecated(note = "use allow_placeholder_for instead")]
735 pub fn allow_passthrough_for(self, host: impl AsRef<str>) -> Self {
736 self.allow_placeholder_for(host)
737 }
738
739 pub fn violation_action(mut self, action: SecretViolationAction) -> Self {
741 self.violation_action = Some(action);
742 self
743 }
744
745 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
747 self.require_tls_identity = enabled;
748 self
749 }
750
751 pub fn substitute_in_headers(mut self, enabled: bool) -> Self {
753 self.substitution.headers = enabled;
754 self
755 }
756
757 pub fn substitute_in_header_fields(
769 mut self,
770 fields: impl IntoIterator<Item = impl Into<String>>,
771 ) -> Self {
772 self.substitution.headers = true;
773 self.substitution.header_fields = fields.into_iter().map(Into::into).collect();
774 self
775 }
776
777 pub fn substitute_in_query(mut self, enabled: bool) -> Self {
779 self.substitution.query = enabled;
780 self
781 }
782
783 pub fn substitute_in_body(mut self, enabled: bool) -> Self {
790 self.substitution.body = enabled;
791 self
792 }
793
794 pub fn build(self) -> SecretEntry {
804 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
805 assert!(
806 self.value.is_some() ^ self.source.is_some(),
807 "SecretBuilder: exactly one of .value() or .source() is required"
808 );
809 assert!(
810 !self.allowed_hosts.is_empty(),
811 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
812 );
813 let placeholder = self
814 .placeholder
815 .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
816
817 SecretEntry {
818 env_var,
819 value: Zeroizing::new(self.value.unwrap_or_default()),
820 source: self.source,
821 placeholder,
822 allowed_hosts: self.allowed_hosts,
823 substitution: self.substitution,
824 passthrough_hosts: self.passthrough_hosts,
825 violation_action: self.violation_action,
826 require_tls_identity: self.require_tls_identity,
827 }
828 }
829}
830
831impl NetworkRateLimiterBuilder {
832 fn new() -> Self {
833 Self::default()
834 }
835
836 pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
838 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
839 Ok(limiter) => self.config.egress = Some(limiter),
840 Err(err) => self.errors.push(err),
841 }
842 self
843 }
844
845 pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
847 match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
848 Ok(limiter) => self.config.ingress = Some(limiter),
849 Err(err) => self.errors.push(err),
850 }
851 self
852 }
853
854 pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
856 if let Some(error) = self.errors.drain(..).next() {
857 return Err(error);
858 }
859 if self.config.egress.is_none() && self.config.ingress.is_none() {
860 return Err(BuildError::EmptyNetworkRateLimiter);
861 }
862 Ok(self.config)
863 }
864}
865
866impl RateLimiterBuilder {
867 fn new(direction: NetworkRateLimitDirection) -> Self {
868 Self {
869 direction,
870 bandwidth: None,
871 ops: None,
872 bandwidth_burst: None,
873 ops_burst: None,
874 refill_error: None,
875 }
876 }
877
878 pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
887 match refill_time_ms(refill_time) {
888 Ok(refill_time_ms) => {
889 self.bandwidth = Some(TokenBucketConfig {
890 size: size.into().as_u64(),
891 refill_time_ms,
892 one_time_burst: 0,
893 });
894 }
895 Err(error) => {
896 self.refill_error.get_or_insert(("bandwidth", error));
897 }
898 }
899 self
900 }
901
902 pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
905 self.bandwidth_burst = Some(burst.into().as_u64());
906 self
907 }
908
909 pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
918 match refill_time_ms(refill_time) {
919 Ok(refill_time_ms) => {
920 self.ops = Some(TokenBucketConfig {
921 size: count,
922 refill_time_ms,
923 one_time_burst: 0,
924 });
925 }
926 Err(error) => {
927 self.refill_error.get_or_insert(("ops", error));
928 }
929 }
930 self
931 }
932
933 pub fn ops_burst(mut self, count: u64) -> Self {
936 self.ops_burst = Some(count);
937 self
938 }
939
940 pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
942 let direction = self.direction;
943 if let Some((bucket, error)) = self.refill_error {
944 return Err(match error {
945 RefillTimeError::TooShort => {
946 BuildError::RateLimitRefillTooShort { direction, bucket }
947 }
948 RefillTimeError::Precision => {
949 BuildError::RateLimitRefillPrecision { direction, bucket }
950 }
951 RefillTimeError::TooLong => {
952 BuildError::RateLimitRefillTooLong { direction, bucket }
953 }
954 });
955 }
956
957 let mut config = RateLimiterConfig {
958 bandwidth: self.bandwidth,
959 ops: self.ops,
960 };
961 if let Some(burst) = self.bandwidth_burst {
962 let Some(bandwidth) = &mut config.bandwidth else {
963 return Err(BuildError::RateLimitBurstWithoutBucket {
964 direction,
965 bucket: "bandwidth",
966 });
967 };
968 bandwidth.one_time_burst = burst;
969 }
970 if let Some(burst) = self.ops_burst {
971 let Some(ops) = &mut config.ops else {
972 return Err(BuildError::RateLimitBurstWithoutBucket {
973 direction,
974 bucket: "ops",
975 });
976 };
977 ops.one_time_burst = burst;
978 }
979
980 config
981 .validate()
982 .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
983 Ok(config)
984 }
985}
986
987fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
993 if refill_time < Duration::from_millis(1) {
994 return Err(RefillTimeError::TooShort);
995 }
996 let refill_time_ms =
997 u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
998 if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
999 return Err(RefillTimeError::Precision);
1000 }
1001 Ok(refill_time_ms)
1002}
1003
1004impl Default for NetworkBuilder {
1009 fn default() -> Self {
1010 Self::new()
1011 }
1012}
1013
1014impl Default for TlsBuilder {
1015 fn default() -> Self {
1016 Self::new()
1017 }
1018}
1019
1020impl Default for SecretBuilder {
1021 fn default() -> Self {
1022 Self::new()
1023 }
1024}
1025#[cfg(test)]
1030mod tests {
1031 use super::*;
1032
1033 #[test]
1034 #[allow(deprecated)]
1035 fn deprecated_tcp_builder_delegates_to_the_new_name() {
1036 let canonical_last = NetworkBuilder::new()
1037 .max_connections(0)
1038 .max_tcp_connections(64)
1039 .build()
1040 .unwrap();
1041 assert_eq!(
1042 canonical_last.max_tcp_connections,
1043 Some(ConnectionLimit::from(64))
1044 );
1045 let legacy_last = NetworkBuilder::new()
1046 .max_tcp_connections(64)
1047 .max_connections(0)
1048 .build()
1049 .unwrap();
1050 assert_eq!(
1051 legacy_last.max_tcp_connections,
1052 Some(ConnectionLimit::Unlimited)
1053 );
1054 let config = NetworkBuilder::new()
1055 .max_connections(0)
1056 .max_udp_connections(7)
1057 .build()
1058 .unwrap();
1059 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
1060 assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
1061 assert!(
1062 NetworkBuilder::new()
1063 .max_tcp_connections(1)
1064 .max_tcp_connections(2)
1065 .build()
1066 .is_ok()
1067 );
1068 }
1069
1070 #[test]
1071 fn tcp_accept_queue_size_is_unset_by_default_and_rejects_out_of_range_values() {
1072 assert_eq!(
1073 NetworkBuilder::new().build().unwrap().tcp_accept_queue_size,
1074 None
1075 );
1076 let config = NetworkBuilder::new()
1077 .tcp_accept_queue_size(4096)
1078 .build()
1079 .unwrap();
1080 assert_eq!(
1081 config.tcp_accept_queue_size.map(TcpAcceptQueueSize::get),
1082 Some(4096)
1083 );
1084
1085 for invalid in [0, TcpAcceptQueueSize::MAX + 1] {
1086 let err = NetworkBuilder::new()
1087 .tcp_accept_queue_size(invalid)
1088 .build()
1089 .unwrap_err();
1090 assert!(
1091 matches!(
1092 err,
1093 BuildError::InvalidTcpAcceptQueueSize { source } if source.value == invalid
1094 ),
1095 "{invalid}: {err}"
1096 );
1097 }
1098 }
1099
1100 #[test]
1102 fn network_builder_happy_path_returns_config() {
1103 let cfg = NetworkBuilder::new()
1104 .dns(|d| d.rebind_protection(false))
1105 .build()
1106 .unwrap();
1107 assert!(!cfg.dns.rebind_protection);
1108 }
1109
1110 #[test]
1111 fn network_builder_preserves_explicit_large_caps() {
1112 for limit in [10000, usize::MAX] {
1113 let config = NetworkBuilder::new()
1114 .max_tcp_connections(limit)
1115 .build()
1116 .unwrap();
1117 assert_eq!(
1118 config.max_tcp_connections,
1119 Some(ConnectionLimit::from(limit))
1120 );
1121 }
1122 }
1123
1124 #[test]
1125 fn network_builder_rejects_incomplete_intercept_ca_config() {
1126 let err = NetworkBuilder::new()
1127 .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1128 .build()
1129 .unwrap_err();
1130
1131 assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1132 }
1133
1134 #[test]
1135 fn network_builder_rejects_non_96_nat64_prefix() {
1136 let err = NetworkBuilder::new()
1137 .nat64_prefix("64:ff9b::/64".parse().unwrap())
1138 .build()
1139 .unwrap_err();
1140
1141 assert!(matches!(err, BuildError::InvalidNat64Prefix { .. }));
1142 }
1143
1144 #[test]
1145 fn port_bind_sets_host_bind() {
1146 let bind = "0.0.0.0".parse().unwrap();
1147 let cfg = NetworkBuilder::new()
1148 .port_bind(bind, 8080, 80)
1149 .port_udp_bind(bind, 5353, 53)
1150 .build()
1151 .unwrap();
1152
1153 assert_eq!(cfg.ports[0].host_bind, bind);
1154 assert_eq!(cfg.ports[0].host_port, 8080);
1155 assert_eq!(cfg.ports[0].guest_port, 80);
1156 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1157 assert_eq!(cfg.ports[1].host_bind, bind);
1158 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1159 }
1160
1161 #[test]
1162 fn port_helpers_default_to_loopback() {
1163 let cfg = NetworkBuilder::new()
1164 .port(8080, 80)
1165 .port_udp(5353, 53)
1166 .build()
1167 .unwrap();
1168
1169 assert_eq!(
1170 cfg.ports[0].host_bind,
1171 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1172 );
1173 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1174 assert_eq!(
1175 cfg.ports[1].host_bind,
1176 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1177 );
1178 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1179 }
1180
1181 #[test]
1182 fn outbound_proxy_defaults_to_none() {
1183 let cfg = NetworkBuilder::new().build().unwrap();
1184 assert_eq!(cfg.outbound_proxy, None);
1185 }
1186
1187 #[test]
1188 fn network_builder_sets_strict_mode() {
1189 let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1190 assert!(cfg.strict);
1191 }
1192
1193 #[test]
1194 fn network_builder_sets_global_violation_action() {
1195 let cfg = NetworkBuilder::new()
1196 .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1197 .build()
1198 .unwrap();
1199
1200 assert_eq!(
1201 cfg.secrets.violation_action,
1202 SecretViolationAction::BlockAndTerminate
1203 );
1204 }
1205
1206 #[test]
1207 #[allow(deprecated)] fn secret_builder_sets_passthrough_and_violation_policies() {
1209 let secret = SecretBuilder::new()
1210 .env("TOKEN")
1211 .value("secret-value")
1212 .allow("api.github.com")
1213 .allow_placeholder_for("api.anthropic.com")
1214 .allow_passthrough_for("*.anthropic.com")
1215 .violation_action(SecretViolationAction::BlockAndTerminate)
1216 .build();
1217
1218 assert_eq!(
1219 secret.violation_action,
1220 Some(SecretViolationAction::BlockAndTerminate),
1221 );
1222 assert_eq!(
1223 secret.passthrough_hosts,
1224 vec![
1225 HostPattern::Exact("api.anthropic.com".into()),
1226 HostPattern::Wildcard("*.anthropic.com".into()),
1227 ],
1228 );
1229 }
1230
1231 #[test]
1232 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1233 fn secret_builder_rejects_empty_allowed_hosts() {
1234 let _ = SecretBuilder::new()
1235 .env("TOKEN")
1236 .value("secret-value")
1237 .build();
1238 }
1239
1240 #[test]
1241 fn secret_builder_source_yields_reference_and_empty_value() {
1242 let secret = SecretBuilder::new()
1243 .env("API_KEY")
1244 .source(SecretSource::Env {
1245 var: "HOST_API_KEY".into(),
1246 })
1247 .allow("api.example.com")
1248 .build();
1249
1250 assert!(secret.value.is_empty());
1251 assert_eq!(
1252 secret.source,
1253 Some(SecretSource::Env {
1254 var: "HOST_API_KEY".into()
1255 })
1256 );
1257
1258 let json = serde_json::to_string(&secret).unwrap();
1260 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1261 }
1262
1263 #[test]
1264 #[should_panic(expected = "exactly one of .value() or .source()")]
1265 fn secret_builder_rejects_both_value_and_source() {
1266 let _ = SecretBuilder::new()
1267 .env("API_KEY")
1268 .value("inline")
1269 .source(SecretSource::Env {
1270 var: "HOST_API_KEY".into(),
1271 })
1272 .allow("api.example.com")
1273 .build();
1274 }
1275
1276 #[test]
1277 fn network_builder_rejects_invalid_secret_config() {
1278 let err = NetworkBuilder::new()
1279 .secret_entry(SecretEntry {
1280 env_var: "API=KEY".into(),
1281 value: Zeroizing::new("secret-value".into()),
1282 source: None,
1283 placeholder: "$MSB_API_KEY".into(),
1284 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1285 substitution: SecretSubstitution::default(),
1286 passthrough_hosts: Vec::new(),
1287 violation_action: None,
1288 require_tls_identity: true,
1289 })
1290 .build()
1291 .unwrap_err();
1292
1293 assert!(err.to_string().contains("env_var must not contain `=`"));
1294 }
1295
1296 #[test]
1297 fn rate_limiter_builder_sets_buckets_and_bursts() {
1298 use microsandbox_utils::size::SizeExt;
1299
1300 let cfg = NetworkBuilder::new()
1301 .rate_limiter(|r| {
1302 r.egress(|r| {
1303 r.bandwidth(1.mib(), Duration::from_secs(1))
1304 .bandwidth_burst(512.kib())
1305 .ops(1_000, Duration::from_secs(1))
1306 .ops_burst(500)
1307 })
1308 .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1309 })
1310 .build()
1311 .unwrap();
1312
1313 let rate_limiter = cfg.rate_limiter.unwrap();
1314 let egress = rate_limiter.egress.unwrap();
1315 let bandwidth = egress.bandwidth.unwrap();
1316 assert_eq!(bandwidth.size, 1024 * 1024);
1317 assert_eq!(bandwidth.refill_time_ms, 1000);
1318 assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1319 let ops = egress.ops.unwrap();
1320 assert_eq!(ops.size, 1_000);
1321 assert_eq!(ops.refill_time_ms, 1000);
1322 assert_eq!(ops.one_time_burst, 500);
1323
1324 let ingress = rate_limiter.ingress.unwrap();
1325 assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1326 assert!(ingress.ops.is_none());
1327 }
1328
1329 #[test]
1330 fn rate_limiters_default_to_unlimited() {
1331 let cfg = NetworkBuilder::new().build().unwrap();
1332 assert!(cfg.rate_limiter.is_none());
1333 }
1334
1335 #[test]
1336 fn rate_limiter_builder_rejects_empty_limiter() {
1337 let err = NetworkBuilder::new()
1338 .rate_limiter(|r| r.egress(|r| r))
1339 .build()
1340 .unwrap_err();
1341 assert_eq!(
1342 err.to_string(),
1343 "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1344 );
1345 }
1346
1347 #[test]
1348 fn network_rate_limiter_builder_rejects_missing_directions() {
1349 let err = NetworkBuilder::new()
1350 .rate_limiter(|r| r)
1351 .build()
1352 .unwrap_err();
1353 assert_eq!(
1354 err.to_string(),
1355 "rate limiter must configure at least one of egress or ingress"
1356 );
1357 }
1358
1359 #[test]
1360 fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1361 let err = NetworkBuilder::new()
1362 .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1363 .build()
1364 .unwrap_err();
1365 assert_eq!(
1366 err.to_string(),
1367 "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1368 );
1369
1370 let err = NetworkBuilder::new()
1371 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1372 .build()
1373 .unwrap_err();
1374 assert_eq!(
1375 err.to_string(),
1376 "egress rate limiter: ops refill interval must be at least one millisecond"
1377 );
1378
1379 let err = NetworkBuilder::new()
1380 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1381 .build()
1382 .unwrap_err();
1383 assert_eq!(
1384 err.to_string(),
1385 "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1386 );
1387 }
1388
1389 #[test]
1390 fn rate_limiter_builder_rejects_burst_without_bucket() {
1391 use microsandbox_utils::size::SizeExt;
1392
1393 let err = NetworkBuilder::new()
1394 .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1395 .build()
1396 .unwrap_err();
1397 assert_eq!(
1398 err.to_string(),
1399 "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1400 );
1401
1402 let err = NetworkBuilder::new()
1403 .rate_limiter(|r| {
1404 r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1405 })
1406 .build()
1407 .unwrap_err();
1408 assert_eq!(
1409 err.to_string(),
1410 "ingress rate limiter: ops_burst requires the ops bucket"
1411 );
1412 }
1413
1414 #[test]
1415 fn rate_limiter_builder_rejects_refill_interval_overflow() {
1416 let err = NetworkBuilder::new()
1417 .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1418 .build()
1419 .unwrap_err();
1420 assert_eq!(
1421 err.to_string(),
1422 "egress rate limiter: ops refill interval overflows u64 milliseconds"
1423 );
1424 }
1425}