1use std::net::IpAddr;
6use std::path::PathBuf;
7
8use ipnetwork::{Ipv4Network, Ipv6Network};
9
10use crate::config::{DnsConfig, InterfaceOverrides, NetworkConfig, PortProtocol, PublishedPort};
11use crate::dns::Nameserver;
12use crate::policy::{BuildError, NetworkPolicy};
13use zeroize::Zeroizing;
14
15use crate::secrets::config::{
16 HostPattern, SecretEntry, SecretInjection, SecretSource, ViolationAction,
17};
18use crate::tls::{ScopedUpstreamCaCert, ScopedVerifyUpstream, TlsConfig};
19
20#[derive(Clone)]
26pub struct NetworkBuilder {
27 config: NetworkConfig,
28 errors: Vec<BuildError>,
29}
30
31pub struct DnsBuilder {
33 config: DnsConfig,
34}
35
36pub struct TlsBuilder {
38 config: TlsConfig,
39}
40
41pub struct SecretBuilder {
51 env_var: Option<String>,
52 value: Option<String>,
53 source: Option<SecretSource>,
54 placeholder: Option<String>,
55 allowed_hosts: Vec<HostPattern>,
56 injection: SecretInjection,
57 on_violation: Option<ViolationAction>,
58 require_tls_identity: bool,
59}
60
61#[derive(Default)]
63pub struct ViolationActionBuilder {
64 action: ViolationAction,
65}
66
67impl NetworkBuilder {
72 pub fn new() -> Self {
74 Self {
75 config: NetworkConfig::default(),
76 errors: Vec::new(),
77 }
78 }
79
80 pub fn from_config(config: NetworkConfig) -> Self {
82 Self {
83 config,
84 errors: Vec::new(),
85 }
86 }
87
88 pub fn enabled(mut self, enabled: bool) -> Self {
90 self.config.enabled = enabled;
91 self
92 }
93
94 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
96 self.port_bind(
97 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
98 host_port,
99 guest_port,
100 )
101 }
102
103 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
105 self.port_udp_bind(
106 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
107 host_port,
108 guest_port,
109 )
110 }
111
112 pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
114 self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
115 }
116
117 pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
119 self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
120 }
121
122 fn add_port(
123 mut self,
124 host_bind: IpAddr,
125 host_port: u16,
126 guest_port: u16,
127 protocol: PortProtocol,
128 ) -> Self {
129 self.config.ports.push(PublishedPort {
130 host_port,
131 guest_port,
132 protocol,
133 host_bind,
134 });
135 self
136 }
137
138 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
140 self.config.policy = policy;
141 self
142 }
143
144 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
153 self.config.dns = f(DnsBuilder::new()).build();
154 self
155 }
156
157 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
159 self.config.tls = f(TlsBuilder::new()).build();
160 self
161 }
162
163 pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
173 self.secret_entry(f(SecretBuilder::new()).build())
174 }
175
176 pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
178 self.config.secrets.secrets.push(entry);
179 self
180 }
181
182 pub fn secret_env(
184 mut self,
185 env_var: impl Into<String>,
186 value: impl Into<String>,
187 placeholder: impl Into<String>,
188 allowed_host: impl Into<String>,
189 ) -> Self {
190 self.config.secrets.secrets.push(SecretEntry {
191 env_var: env_var.into(),
192 value: Zeroizing::new(value.into()),
193 source: None,
194 placeholder: placeholder.into(),
195 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
196 injection: SecretInjection::default(),
197 on_violation: None,
198 require_tls_identity: true,
199 });
200 self
201 }
202
203 pub fn on_secret_violation(
205 mut self,
206 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
207 ) -> Self {
208 self.config.secrets.on_violation = f(ViolationActionBuilder::default()).build();
209 self
210 }
211
212 pub fn max_connections(mut self, max: usize) -> Self {
214 self.config.max_connections = Some(max);
215 self
216 }
217
218 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
220 self.config.interface = overrides;
221 self
222 }
223
224 pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
228 if pool.prefix() > 30 {
229 self.errors.push(BuildError::InvalidIpv4Pool {
230 raw: pool.to_string(),
231 });
232 } else {
233 self.config.interface.ipv4_pool = Some(pool);
234 }
235 self
236 }
237
238 pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
242 if pool.prefix() > 64 {
243 self.errors.push(BuildError::InvalidIpv6Pool {
244 raw: pool.to_string(),
245 });
246 } else {
247 self.config.interface.ipv6_pool = Some(pool);
248 }
249 self
250 }
251
252 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
258 self.config.trust_host_cas = enabled;
259 self
260 }
261
262 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
268 if let Some(err) = self.errors.drain(..).next() {
269 return Err(err);
270 }
271 self.config.secrets.validate()?;
272 Ok(self.config)
273 }
274}
275
276impl DnsBuilder {
277 pub fn new() -> Self {
279 Self {
280 config: DnsConfig::default(),
281 }
282 }
283
284 pub fn rebind_protection(mut self, enabled: bool) -> Self {
286 self.config.rebind_protection = enabled;
287 self
288 }
289
290 pub fn nameservers<I>(mut self, nameservers: I) -> Self
297 where
298 I: IntoIterator,
299 I::Item: Into<Nameserver>,
300 {
301 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
302 self
303 }
304
305 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
307 self.config.query_timeout_ms = ms;
308 self
309 }
310
311 pub fn build(self) -> DnsConfig {
313 self.config
314 }
315}
316
317impl Default for DnsBuilder {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323impl TlsBuilder {
324 pub fn new() -> Self {
326 Self {
327 config: TlsConfig {
328 enabled: true,
329 ..TlsConfig::default()
330 },
331 }
332 }
333
334 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
336 self.config.bypass.push(pattern.into());
337 self
338 }
339
340 pub fn verify_upstream(mut self, verify: bool) -> Self {
342 self.config.verify_upstream = verify;
343 self
344 }
345
346 pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
352 self.config
353 .scoped_verify_upstream
354 .push(ScopedVerifyUpstream {
355 pattern: pattern.into(),
356 verify,
357 });
358 self
359 }
360
361 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
363 self.config.intercepted_ports = ports;
364 self
365 }
366
367 pub fn block_quic(mut self, block: bool) -> Self {
369 self.config.block_quic_on_intercept = block;
370 self
371 }
372
373 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
378 self.config.upstream_ca_cert.push(path.into());
379 self
380 }
381
382 pub fn upstream_ca_cert_for(
389 mut self,
390 pattern: impl Into<String>,
391 path: impl Into<PathBuf>,
392 ) -> Self {
393 self.config
394 .scoped_upstream_ca_cert
395 .push(ScopedUpstreamCaCert {
396 pattern: pattern.into(),
397 path: path.into(),
398 });
399 self
400 }
401
402 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
404 self.config.intercept_ca.cert_path = Some(path.into());
405 self
406 }
407
408 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
410 self.config.intercept_ca.key_path = Some(path.into());
411 self
412 }
413
414 pub fn build(self) -> TlsConfig {
416 self.config
417 }
418}
419
420impl SecretBuilder {
421 pub fn new() -> Self {
423 Self {
424 env_var: None,
425 value: None,
426 source: None,
427 placeholder: None,
428 allowed_hosts: Vec::new(),
429 injection: SecretInjection::default(),
430 on_violation: None,
431 require_tls_identity: true,
432 }
433 }
434
435 pub fn env(mut self, var: impl Into<String>) -> Self {
440 self.env_var = Some(var.into());
441 self
442 }
443
444 pub fn value(mut self, value: impl Into<String>) -> Self {
450 self.value = Some(value.into());
451 self
452 }
453
454 pub fn source(mut self, source: SecretSource) -> Self {
461 self.source = Some(source);
462 self
463 }
464
465 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
471 self.placeholder = Some(placeholder.into());
472 self
473 }
474
475 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
477 self.allowed_hosts.push(HostPattern::Exact(host.into()));
478 self
479 }
480
481 pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
483 self.allowed_hosts
484 .push(HostPattern::Wildcard(pattern.into()));
485 self
486 }
487
488 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
491 if i_understand_the_risk {
492 self.allowed_hosts.push(HostPattern::Any);
493 }
494 self
495 }
496
497 pub fn on_violation(
499 mut self,
500 f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
501 ) -> Self {
502 self.on_violation = Some(f(ViolationActionBuilder::default()).build());
503 self
504 }
505
506 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
508 self.require_tls_identity = enabled;
509 self
510 }
511
512 pub fn inject_headers(mut self, enabled: bool) -> Self {
514 self.injection.headers = enabled;
515 self
516 }
517
518 pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
520 self.injection.basic_auth = enabled;
521 self
522 }
523
524 pub fn inject_query(mut self, enabled: bool) -> Self {
526 self.injection.query_params = enabled;
527 self
528 }
529
530 pub fn inject_body(mut self, enabled: bool) -> Self {
537 self.injection.body = enabled;
538 self
539 }
540
541 pub fn build(self) -> SecretEntry {
551 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
552 assert!(
553 self.value.is_some() ^ self.source.is_some(),
554 "SecretBuilder: exactly one of .value() or .source() is required"
555 );
556 assert!(
557 !self.allowed_hosts.is_empty(),
558 "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
559 );
560 let placeholder = self
561 .placeholder
562 .unwrap_or_else(|| format!("$MSB_{env_var}"));
563
564 SecretEntry {
565 env_var,
566 value: Zeroizing::new(self.value.unwrap_or_default()),
567 source: self.source,
568 placeholder,
569 allowed_hosts: self.allowed_hosts,
570 injection: self.injection,
571 on_violation: self.on_violation,
572 require_tls_identity: self.require_tls_identity,
573 }
574 }
575}
576
577impl ViolationActionBuilder {
578 pub fn new() -> Self {
580 Self::default()
581 }
582
583 pub fn from_action(action: ViolationAction) -> Self {
585 action.into()
586 }
587
588 pub fn block(mut self) -> Self {
590 self.action = ViolationAction::Block;
591 self
592 }
593
594 pub fn block_and_log(mut self) -> Self {
596 self.action = ViolationAction::BlockAndLog;
597 self
598 }
599
600 pub fn block_and_terminate(mut self) -> Self {
602 self.action = ViolationAction::BlockAndTerminate;
603 self
604 }
605
606 pub fn passthrough_host(mut self, host: impl Into<String>) -> Self {
608 self.push_passthrough_host(HostPattern::Exact(host.into()));
609 self
610 }
611
612 pub fn passthrough_host_pattern(mut self, pattern: impl Into<String>) -> Self {
614 self.push_passthrough_host(HostPattern::Wildcard(pattern.into()));
615 self
616 }
617
618 pub fn passthrough_all_hosts(mut self, i_understand_the_risk: bool) -> Self {
620 if i_understand_the_risk {
621 self.push_passthrough_host(HostPattern::Any);
622 }
623 self
624 }
625
626 fn push_passthrough_host(&mut self, host: HostPattern) {
628 match self.action {
629 ViolationAction::Passthrough(ref mut hosts) => hosts.push(host),
630 _ => self.action = ViolationAction::Passthrough(vec![host]),
631 }
632 }
633
634 pub fn build(self) -> ViolationAction {
636 self.action
637 }
638}
639
640impl Default for NetworkBuilder {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650impl Default for TlsBuilder {
651 fn default() -> Self {
652 Self::new()
653 }
654}
655
656impl Default for SecretBuilder {
657 fn default() -> Self {
658 Self::new()
659 }
660}
661impl From<ViolationAction> for ViolationActionBuilder {
662 fn from(action: ViolationAction) -> Self {
663 Self { action }
664 }
665}
666
667#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
677 fn network_builder_happy_path_returns_config() {
678 let cfg = NetworkBuilder::new()
679 .dns(|d| d.rebind_protection(false))
680 .build()
681 .unwrap();
682 assert!(!cfg.dns.rebind_protection);
683 }
684
685 #[test]
686 fn port_bind_sets_host_bind() {
687 let bind = "0.0.0.0".parse().unwrap();
688 let cfg = NetworkBuilder::new()
689 .port_bind(bind, 8080, 80)
690 .port_udp_bind(bind, 5353, 53)
691 .build()
692 .unwrap();
693
694 assert_eq!(cfg.ports[0].host_bind, bind);
695 assert_eq!(cfg.ports[0].host_port, 8080);
696 assert_eq!(cfg.ports[0].guest_port, 80);
697 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
698 assert_eq!(cfg.ports[1].host_bind, bind);
699 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
700 }
701
702 #[test]
703 fn port_helpers_default_to_loopback() {
704 let cfg = NetworkBuilder::new()
705 .port(8080, 80)
706 .port_udp(5353, 53)
707 .build()
708 .unwrap();
709
710 assert_eq!(
711 cfg.ports[0].host_bind,
712 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
713 );
714 assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
715 assert_eq!(
716 cfg.ports[1].host_bind,
717 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
718 );
719 assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
720 }
721
722 #[test]
723 fn network_builder_sets_global_passthrough_action() {
724 let cfg = NetworkBuilder::new()
725 .on_secret_violation(|v| {
726 v.passthrough_host("api.anthropic.com")
727 .passthrough_host_pattern("*.anthropic.com")
728 })
729 .build()
730 .unwrap();
731
732 assert_eq!(
733 cfg.secrets.on_violation,
734 ViolationAction::Passthrough(vec![
735 HostPattern::Exact("api.anthropic.com".into()),
736 HostPattern::Wildcard("*.anthropic.com".into()),
737 ])
738 );
739 }
740
741 #[test]
742 fn secret_builder_sets_violation_action() {
743 let secret = SecretBuilder::new()
744 .env("TOKEN")
745 .value("secret-value")
746 .allow_host("api.github.com")
747 .on_violation(|v| {
748 v.passthrough_host("api.anthropic.com")
749 .passthrough_host_pattern("*.anthropic.com")
750 })
751 .build();
752
753 assert_eq!(
754 secret.on_violation,
755 Some(ViolationAction::Passthrough(vec![
756 HostPattern::Exact("api.anthropic.com".into()),
757 HostPattern::Wildcard("*.anthropic.com".into()),
758 ])),
759 );
760 }
761
762 #[test]
763 #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
764 fn secret_builder_rejects_empty_allowed_hosts() {
765 let _ = SecretBuilder::new()
766 .env("TOKEN")
767 .value("secret-value")
768 .build();
769 }
770
771 #[test]
772 fn secret_builder_source_yields_reference_and_empty_value() {
773 let secret = SecretBuilder::new()
774 .env("API_KEY")
775 .source(SecretSource::Env {
776 var: "HOST_API_KEY".into(),
777 })
778 .allow_host("api.example.com")
779 .build();
780
781 assert!(secret.value.is_empty());
782 assert_eq!(
783 secret.source,
784 Some(SecretSource::Env {
785 var: "HOST_API_KEY".into()
786 })
787 );
788
789 let json = serde_json::to_string(&secret).unwrap();
791 assert!(json.contains("\"var\":\"HOST_API_KEY\""));
792 }
793
794 #[test]
795 #[should_panic(expected = "exactly one of .value() or .source()")]
796 fn secret_builder_rejects_both_value_and_source() {
797 let _ = SecretBuilder::new()
798 .env("API_KEY")
799 .value("inline")
800 .source(SecretSource::Env {
801 var: "HOST_API_KEY".into(),
802 })
803 .allow_host("api.example.com")
804 .build();
805 }
806
807 #[test]
808 fn network_builder_rejects_invalid_secret_config() {
809 let err = NetworkBuilder::new()
810 .secret_entry(SecretEntry {
811 env_var: "API=KEY".into(),
812 value: Zeroizing::new("secret-value".into()),
813 source: None,
814 placeholder: "$MSB_API_KEY".into(),
815 allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
816 injection: SecretInjection::default(),
817 on_violation: None,
818 require_tls_identity: true,
819 })
820 .build()
821 .unwrap_err();
822
823 assert!(err.to_string().contains("env_var must not contain `=`"));
824 }
825
826 #[test]
827 fn violation_action_builder_blocking_call_replaces_passthrough_policy() {
828 let action = ViolationActionBuilder::default()
829 .passthrough_host("google.com")
830 .block_and_terminate()
831 .passthrough_host("facebook.com")
832 .build();
833
834 assert_eq!(
835 action,
836 ViolationAction::Passthrough(vec![HostPattern::Exact("facebook.com".into())])
837 );
838 }
839
840 #[test]
841 fn violation_action_builder_accumulates_passthrough_hosts() {
842 let action = ViolationActionBuilder::default()
843 .block()
844 .passthrough_host("google.com")
845 .passthrough_host("facebook.com")
846 .build();
847
848 assert_eq!(
849 action,
850 ViolationAction::Passthrough(vec![
851 HostPattern::Exact("google.com".into()),
852 HostPattern::Exact("facebook.com".into()),
853 ]),
854 );
855 }
856}