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