1use std::net::IpAddr;
6use std::path::PathBuf;
7
8use crate::config::{DnsConfig, InterfaceOverrides, NetworkConfig, PortProtocol, PublishedPort};
9use crate::dns::Nameserver;
10use crate::policy::{BuildError, DomainName, NetworkPolicy};
11use crate::secrets::config::{HostPattern, SecretEntry, SecretInjection, ViolationAction};
12use crate::tls::TlsConfig;
13
14#[derive(Clone)]
20pub struct NetworkBuilder {
21 config: NetworkConfig,
22 errors: Vec<BuildError>,
23}
24
25pub struct DnsBuilder {
27 config: DnsConfig,
28 errors: Vec<BuildError>,
29}
30
31pub struct TlsBuilder {
33 config: TlsConfig,
34}
35
36pub struct SecretBuilder {
46 env_var: Option<String>,
47 value: Option<String>,
48 placeholder: Option<String>,
49 allowed_hosts: Vec<HostPattern>,
50 injection: SecretInjection,
51 require_tls_identity: bool,
52}
53
54impl NetworkBuilder {
59 pub fn new() -> Self {
61 Self {
62 config: NetworkConfig::default(),
63 errors: Vec::new(),
64 }
65 }
66
67 pub fn from_config(config: NetworkConfig) -> Self {
69 Self {
70 config,
71 errors: Vec::new(),
72 }
73 }
74
75 pub fn enabled(mut self, enabled: bool) -> Self {
77 self.config.enabled = enabled;
78 self
79 }
80
81 pub fn port(self, host_port: u16, guest_port: u16) -> Self {
83 self.add_port(host_port, guest_port, PortProtocol::Tcp)
84 }
85
86 pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
88 self.add_port(host_port, guest_port, PortProtocol::Udp)
89 }
90
91 fn add_port(mut self, host_port: u16, guest_port: u16, protocol: PortProtocol) -> Self {
92 self.config.ports.push(PublishedPort {
93 host_port,
94 guest_port,
95 protocol,
96 host_bind: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
97 });
98 self
99 }
100
101 pub fn policy(mut self, policy: NetworkPolicy) -> Self {
103 self.config.policy = policy;
104 self
105 }
106
107 pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
117 let dns_builder = f(DnsBuilder::new());
118 match dns_builder.build() {
119 Ok(config) => self.config.dns = config,
120 Err(err) => self.errors.push(err),
121 }
122 self
123 }
124
125 pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
127 self.config.tls = f(TlsBuilder::new()).build();
128 self
129 }
130
131 pub fn secret(mut self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
141 self.config
142 .secrets
143 .secrets
144 .push(f(SecretBuilder::new()).build());
145 self
146 }
147
148 pub fn secret_env(
150 mut self,
151 env_var: impl Into<String>,
152 value: impl Into<String>,
153 placeholder: impl Into<String>,
154 allowed_host: impl Into<String>,
155 ) -> Self {
156 self.config.secrets.secrets.push(SecretEntry {
157 env_var: env_var.into(),
158 value: value.into(),
159 placeholder: placeholder.into(),
160 allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
161 injection: SecretInjection::default(),
162 require_tls_identity: true,
163 });
164 self
165 }
166
167 pub fn on_secret_violation(mut self, action: ViolationAction) -> Self {
169 self.config.secrets.on_violation = action;
170 self
171 }
172
173 pub fn max_connections(mut self, max: usize) -> Self {
175 self.config.max_connections = Some(max);
176 self
177 }
178
179 pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
181 self.config.interface = overrides;
182 self
183 }
184
185 pub fn trust_host_cas(mut self, enabled: bool) -> Self {
191 self.config.trust_host_cas = enabled;
192 self
193 }
194
195 pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
201 if let Some(err) = self.errors.drain(..).next() {
202 return Err(err);
203 }
204 Ok(self.config)
205 }
206}
207
208impl DnsBuilder {
209 pub fn new() -> Self {
211 Self {
212 config: DnsConfig::default(),
213 errors: Vec::new(),
214 }
215 }
216
217 pub fn block_domain(mut self, domain: impl Into<String>) -> Self {
226 let raw: String = domain.into();
227 match raw.parse::<DomainName>() {
228 Ok(name) => self.config.blocked_domains.push(name.into()),
229 Err(source) => self
230 .errors
231 .push(BuildError::InvalidBlockedDomain { raw, source }),
232 }
233 self
234 }
235
236 pub fn block_domain_suffix(mut self, suffix: impl Into<String>) -> Self {
242 let raw: String = suffix.into();
243 match raw.parse::<DomainName>() {
244 Ok(name) => self.config.blocked_suffixes.push(name.into()),
245 Err(source) => self
246 .errors
247 .push(BuildError::InvalidBlockedDomainSuffix { raw, source }),
248 }
249 self
250 }
251
252 pub fn rebind_protection(mut self, enabled: bool) -> Self {
254 self.config.rebind_protection = enabled;
255 self
256 }
257
258 pub fn nameservers<I>(mut self, nameservers: I) -> Self
265 where
266 I: IntoIterator,
267 I::Item: Into<Nameserver>,
268 {
269 self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
270 self
271 }
272
273 pub fn query_timeout_ms(mut self, ms: u64) -> Self {
275 self.config.query_timeout_ms = ms;
276 self
277 }
278
279 pub fn build(mut self) -> Result<DnsConfig, BuildError> {
285 if let Some(err) = self.errors.drain(..).next() {
286 return Err(err);
287 }
288 Ok(self.config)
289 }
290}
291
292impl Default for DnsBuilder {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298impl TlsBuilder {
299 pub fn new() -> Self {
301 Self {
302 config: TlsConfig {
303 enabled: true,
304 ..TlsConfig::default()
305 },
306 }
307 }
308
309 pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
311 self.config.bypass.push(pattern.into());
312 self
313 }
314
315 pub fn verify_upstream(mut self, verify: bool) -> Self {
317 self.config.verify_upstream = verify;
318 self
319 }
320
321 pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
323 self.config.intercepted_ports = ports;
324 self
325 }
326
327 pub fn block_quic(mut self, block: bool) -> Self {
329 self.config.block_quic_on_intercept = block;
330 self
331 }
332
333 pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
338 self.config.upstream_ca_cert.push(path.into());
339 self
340 }
341
342 pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
344 self.config.intercept_ca.cert_path = Some(path.into());
345 self
346 }
347
348 pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
350 self.config.intercept_ca.key_path = Some(path.into());
351 self
352 }
353
354 pub fn build(self) -> TlsConfig {
356 self.config
357 }
358}
359
360impl SecretBuilder {
361 pub fn new() -> Self {
363 Self {
364 env_var: None,
365 value: None,
366 placeholder: None,
367 allowed_hosts: Vec::new(),
368 injection: SecretInjection::default(),
369 require_tls_identity: true,
370 }
371 }
372
373 pub fn env(mut self, var: impl Into<String>) -> Self {
375 self.env_var = Some(var.into());
376 self
377 }
378
379 pub fn value(mut self, value: impl Into<String>) -> Self {
381 self.value = Some(value.into());
382 self
383 }
384
385 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
388 self.placeholder = Some(placeholder.into());
389 self
390 }
391
392 pub fn allow_host(mut self, host: impl Into<String>) -> Self {
394 self.allowed_hosts.push(HostPattern::Exact(host.into()));
395 self
396 }
397
398 pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
400 self.allowed_hosts
401 .push(HostPattern::Wildcard(pattern.into()));
402 self
403 }
404
405 pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
408 if i_understand_the_risk {
409 self.allowed_hosts.push(HostPattern::Any);
410 }
411 self
412 }
413
414 pub fn require_tls_identity(mut self, enabled: bool) -> Self {
416 self.require_tls_identity = enabled;
417 self
418 }
419
420 pub fn inject_headers(mut self, enabled: bool) -> Self {
422 self.injection.headers = enabled;
423 self
424 }
425
426 pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
428 self.injection.basic_auth = enabled;
429 self
430 }
431
432 pub fn inject_query(mut self, enabled: bool) -> Self {
434 self.injection.query_params = enabled;
435 self
436 }
437
438 pub fn inject_body(mut self, enabled: bool) -> Self {
440 self.injection.body = enabled;
441 self
442 }
443
444 pub fn build(self) -> SecretEntry {
449 let env_var = self.env_var.expect("SecretBuilder: .env() is required");
450 let value = self.value.expect("SecretBuilder: .value() is required");
451 let placeholder = self
452 .placeholder
453 .unwrap_or_else(|| format!("$MSB_{env_var}"));
454
455 SecretEntry {
456 env_var,
457 value,
458 placeholder,
459 allowed_hosts: self.allowed_hosts,
460 injection: self.injection,
461 require_tls_identity: self.require_tls_identity,
462 }
463 }
464}
465
466impl Default for NetworkBuilder {
471 fn default() -> Self {
472 Self::new()
473 }
474}
475
476impl Default for TlsBuilder {
477 fn default() -> Self {
478 Self::new()
479 }
480}
481
482impl Default for SecretBuilder {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
499 fn block_domain_happy_path() {
500 let cfg = DnsBuilder::new()
501 .block_domain("evil.com")
502 .block_domain_suffix(".tracking.example")
503 .build()
504 .unwrap();
505 assert_eq!(cfg.blocked_domains.len(), 1);
506 assert_eq!(cfg.blocked_suffixes.len(), 1);
507 }
508
509 #[test]
512 fn block_domain_invalid_surfaces_at_build() {
513 let result = DnsBuilder::new().block_domain("not a domain!").build();
514 match result {
515 Err(BuildError::InvalidBlockedDomain { raw, .. }) => {
516 assert_eq!(raw, "not a domain!");
517 }
518 other => panic!("expected InvalidBlockedDomain, got {other:?}"),
519 }
520 }
521
522 #[test]
525 fn block_domain_suffix_invalid_surfaces_at_build() {
526 let result = DnsBuilder::new()
527 .block_domain_suffix("...invalid!!!")
528 .build();
529 match result {
530 Err(BuildError::InvalidBlockedDomainSuffix { raw, .. }) => {
531 assert_eq!(raw, "...invalid!!!");
532 }
533 other => panic!("expected InvalidBlockedDomainSuffix, got {other:?}"),
534 }
535 }
536
537 #[test]
540 fn block_domain_first_error_wins() {
541 let result = DnsBuilder::new()
542 .block_domain("first bad!")
543 .block_domain("second bad!")
544 .build();
545 match result {
546 Err(BuildError::InvalidBlockedDomain { raw, .. }) => {
547 assert_eq!(raw, "first bad!");
548 }
549 other => panic!("expected first-error InvalidBlockedDomain, got {other:?}"),
550 }
551 }
552
553 #[test]
557 fn dns_error_cascades_through_network_builder() {
558 let result = NetworkBuilder::new()
559 .dns(|d| d.block_domain("not a domain!"))
560 .build();
561 match result {
562 Err(BuildError::InvalidBlockedDomain { raw, .. }) => {
563 assert_eq!(raw, "not a domain!");
564 }
565 other => panic!("expected cascaded InvalidBlockedDomain, got {other:?}"),
566 }
567 }
568
569 #[test]
571 fn network_builder_happy_path_returns_config() {
572 let cfg = NetworkBuilder::new()
573 .dns(|d| d.block_domain("evil.com"))
574 .build()
575 .unwrap();
576 assert_eq!(cfg.dns.blocked_domains.len(), 1);
577 }
578}