Skip to main content

microsandbox_network/
builder.rs

1//! Fluent builder API for [`NetworkConfig`].
2//!
3//! Used by `SandboxBuilder::network(|n| n.port(8080, 80).policy(...))`.
4
5use std::net::IpAddr;
6use std::path::PathBuf;
7
8use crate::config::{DnsConfig, InterfaceOverrides, NetworkConfig, PortProtocol, PublishedPort};
9use crate::dns::Nameserver;
10use crate::policy::{BuildError, NetworkPolicy};
11use crate::secrets::config::{HostPattern, SecretEntry, SecretInjection, ViolationAction};
12use crate::tls::TlsConfig;
13
14//--------------------------------------------------------------------------------------------------
15// Types
16//--------------------------------------------------------------------------------------------------
17
18/// Fluent builder for [`NetworkConfig`].
19#[derive(Clone)]
20pub struct NetworkBuilder {
21    config: NetworkConfig,
22    errors: Vec<BuildError>,
23}
24
25/// Fluent builder for [`DnsConfig`].
26pub struct DnsBuilder {
27    config: DnsConfig,
28}
29
30/// Fluent builder for [`TlsConfig`].
31pub struct TlsBuilder {
32    config: TlsConfig,
33}
34
35/// Fluent builder for a single [`SecretEntry`].
36///
37/// ```ignore
38/// SecretBuilder::new()
39///     .env("OPENAI_API_KEY")
40///     .value(api_key)
41///     .allow_host("api.openai.com")
42///     .build()
43/// ```
44pub 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
53//--------------------------------------------------------------------------------------------------
54// Methods
55//--------------------------------------------------------------------------------------------------
56
57impl NetworkBuilder {
58    /// Start building a network configuration with defaults.
59    pub fn new() -> Self {
60        Self {
61            config: NetworkConfig::default(),
62            errors: Vec::new(),
63        }
64    }
65
66    /// Start building from an existing network configuration.
67    pub fn from_config(config: NetworkConfig) -> Self {
68        Self {
69            config,
70            errors: Vec::new(),
71        }
72    }
73
74    /// Enable or disable networking.
75    pub fn enabled(mut self, enabled: bool) -> Self {
76        self.config.enabled = enabled;
77        self
78    }
79
80    /// Publish a TCP port: `host_port` on the host maps to `guest_port` in the guest.
81    pub fn port(self, host_port: u16, guest_port: u16) -> Self {
82        self.add_port(host_port, guest_port, PortProtocol::Tcp)
83    }
84
85    /// Publish a UDP port.
86    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    /// Set the network policy.
101    pub fn policy(mut self, policy: NetworkPolicy) -> Self {
102        self.config.policy = policy;
103        self
104    }
105
106    /// Configure DNS interception via a closure.
107    ///
108    /// ```ignore
109    /// .dns(|d| d
110    ///     .nameservers(["1.1.1.1".parse::<Nameserver>()?])
111    ///     .rebind_protection(false)
112    /// )
113    /// ```
114    pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
115        self.config.dns = f(DnsBuilder::new()).build();
116        self
117    }
118
119    /// Configure TLS interception via a closure.
120    pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
121        self.config.tls = f(TlsBuilder::new()).build();
122        self
123    }
124
125    /// Add a secret via a closure builder.
126    ///
127    /// ```ignore
128    /// .secret(|s| s
129    ///     .env("OPENAI_API_KEY")
130    ///     .value(api_key)
131    ///     .allow_host("api.openai.com")
132    /// )
133    /// ```
134    pub fn secret(mut self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
135        self.config
136            .secrets
137            .secrets
138            .push(f(SecretBuilder::new()).build());
139        self
140    }
141
142    /// Shorthand: add a secret with env var, value, placeholder, and allowed host.
143    pub fn secret_env(
144        mut self,
145        env_var: impl Into<String>,
146        value: impl Into<String>,
147        placeholder: impl Into<String>,
148        allowed_host: impl Into<String>,
149    ) -> Self {
150        self.config.secrets.secrets.push(SecretEntry {
151            env_var: env_var.into(),
152            value: value.into(),
153            placeholder: placeholder.into(),
154            allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
155            injection: SecretInjection::default(),
156            require_tls_identity: true,
157        });
158        self
159    }
160
161    /// Set the violation action for secrets.
162    pub fn on_secret_violation(mut self, action: ViolationAction) -> Self {
163        self.config.secrets.on_violation = action;
164        self
165    }
166
167    /// Set the maximum number of concurrent connections.
168    pub fn max_connections(mut self, max: usize) -> Self {
169        self.config.max_connections = Some(max);
170        self
171    }
172
173    /// Set guest interface overrides.
174    pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
175        self.config.interface = overrides;
176        self
177    }
178
179    /// Whether to ship the host's trusted root CAs into the guest at
180    /// boot. Default: false. Opt in when running behind a corporate
181    /// TLS-inspecting proxy (Cloudflare Warp Zero Trust, Zscaler,
182    /// Netskope, ...) whose gateway CA is trusted on the host but
183    /// unknown to the guest's stock Mozilla bundle.
184    pub fn trust_host_cas(mut self, enabled: bool) -> Self {
185        self.config.trust_host_cas = enabled;
186        self
187    }
188
189    /// Consume the builder and return the configuration.
190    ///
191    /// Surfaces the first [`BuildError`] accumulated by any nested
192    /// builder (currently [`DnsBuilder`]). Errors stored on the
193    /// network builder itself flow through here too.
194    pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
195        if let Some(err) = self.errors.drain(..).next() {
196            return Err(err);
197        }
198        Ok(self.config)
199    }
200}
201
202impl DnsBuilder {
203    /// Start building DNS configuration with defaults.
204    pub fn new() -> Self {
205        Self {
206            config: DnsConfig::default(),
207        }
208    }
209
210    /// Enable or disable DNS rebinding protection. Default: true.
211    pub fn rebind_protection(mut self, enabled: bool) -> Self {
212        self.config.rebind_protection = enabled;
213        self
214    }
215
216    /// Set the upstream nameservers to forward queries to. When one or
217    /// more are set, the interceptor uses these instead of the
218    /// nameservers in the host's `/etc/resolv.conf`. Replaces any
219    /// previously-set nameservers. Each element is any type convertible
220    /// into [`Nameserver`] (`SocketAddr`, `IpAddr`, or a parsed
221    /// string via `"dns.google:53".parse::<Nameserver>()?`).
222    pub fn nameservers<I>(mut self, nameservers: I) -> Self
223    where
224        I: IntoIterator,
225        I::Item: Into<Nameserver>,
226    {
227        self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
228        self
229    }
230
231    /// Set the per-DNS-query timeout in milliseconds. Default: 5000.
232    pub fn query_timeout_ms(mut self, ms: u64) -> Self {
233        self.config.query_timeout_ms = ms;
234        self
235    }
236
237    /// Consume the builder and return the configuration.
238    pub fn build(self) -> DnsConfig {
239        self.config
240    }
241}
242
243impl Default for DnsBuilder {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl TlsBuilder {
250    /// Start building TLS configuration.
251    pub fn new() -> Self {
252        Self {
253            config: TlsConfig {
254                enabled: true,
255                ..TlsConfig::default()
256            },
257        }
258    }
259
260    /// Add a domain to the bypass list (no MITM). Supports `*.suffix` wildcards.
261    pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
262        self.config.bypass.push(pattern.into());
263        self
264    }
265
266    /// Enable or disable upstream server certificate verification.
267    pub fn verify_upstream(mut self, verify: bool) -> Self {
268        self.config.verify_upstream = verify;
269        self
270    }
271
272    /// Set the ports to intercept.
273    pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
274        self.config.intercepted_ports = ports;
275        self
276    }
277
278    /// Enable or disable QUIC blocking on intercepted ports.
279    pub fn block_quic(mut self, block: bool) -> Self {
280        self.config.block_quic_on_intercept = block;
281        self
282    }
283
284    /// Add a CA certificate PEM file to trust for upstream server verification.
285    ///
286    /// Useful when the upstream server uses a self-signed or private CA certificate.
287    /// Can be called multiple times to add several CAs.
288    pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
289        self.config.upstream_ca_cert.push(path.into());
290        self
291    }
292
293    /// Set a custom interception CA certificate PEM file path.
294    pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
295        self.config.intercept_ca.cert_path = Some(path.into());
296        self
297    }
298
299    /// Set a custom interception CA private key PEM file path.
300    pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
301        self.config.intercept_ca.key_path = Some(path.into());
302        self
303    }
304
305    /// Consume the builder and return the configuration.
306    pub fn build(self) -> TlsConfig {
307        self.config
308    }
309}
310
311impl SecretBuilder {
312    /// Start building a secret.
313    pub fn new() -> Self {
314        Self {
315            env_var: None,
316            value: None,
317            placeholder: None,
318            allowed_hosts: Vec::new(),
319            injection: SecretInjection::default(),
320            require_tls_identity: true,
321        }
322    }
323
324    /// Set the environment variable to expose the placeholder as (required).
325    pub fn env(mut self, var: impl Into<String>) -> Self {
326        self.env_var = Some(var.into());
327        self
328    }
329
330    /// Set the secret value (required).
331    pub fn value(mut self, value: impl Into<String>) -> Self {
332        self.value = Some(value.into());
333        self
334    }
335
336    /// Set a custom placeholder string.
337    /// If not set, auto-generated as `$MSB_<env_var>`.
338    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
339        self.placeholder = Some(placeholder.into());
340        self
341    }
342
343    /// Add an allowed host (exact match).
344    pub fn allow_host(mut self, host: impl Into<String>) -> Self {
345        self.allowed_hosts.push(HostPattern::Exact(host.into()));
346        self
347    }
348
349    /// Add an allowed host with wildcard pattern (e.g., `*.openai.com`).
350    pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
351        self.allowed_hosts
352            .push(HostPattern::Wildcard(pattern.into()));
353        self
354    }
355
356    /// Allow for any host. **Dangerous**: secret can be exfiltrated to any
357    /// destination. Requires explicit acknowledgment.
358    pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
359        if i_understand_the_risk {
360            self.allowed_hosts.push(HostPattern::Any);
361        }
362        self
363    }
364
365    /// Require verified TLS identity before substituting (default: true).
366    pub fn require_tls_identity(mut self, enabled: bool) -> Self {
367        self.require_tls_identity = enabled;
368        self
369    }
370
371    /// Configure header injection (default: true).
372    pub fn inject_headers(mut self, enabled: bool) -> Self {
373        self.injection.headers = enabled;
374        self
375    }
376
377    /// Configure Basic Auth injection (default: true).
378    pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
379        self.injection.basic_auth = enabled;
380        self
381    }
382
383    /// Configure query parameter injection (default: false).
384    pub fn inject_query(mut self, enabled: bool) -> Self {
385        self.injection.query_params = enabled;
386        self
387    }
388
389    /// Configure body injection (default: false).
390    pub fn inject_body(mut self, enabled: bool) -> Self {
391        self.injection.body = enabled;
392        self
393    }
394
395    /// Consume the builder and return a [`SecretEntry`].
396    ///
397    /// # Panics
398    /// Panics if `env` or `value` was not set.
399    pub fn build(self) -> SecretEntry {
400        let env_var = self.env_var.expect("SecretBuilder: .env() is required");
401        let value = self.value.expect("SecretBuilder: .value() is required");
402        let placeholder = self
403            .placeholder
404            .unwrap_or_else(|| format!("$MSB_{env_var}"));
405
406        SecretEntry {
407            env_var,
408            value,
409            placeholder,
410            allowed_hosts: self.allowed_hosts,
411            injection: self.injection,
412            require_tls_identity: self.require_tls_identity,
413        }
414    }
415}
416
417//--------------------------------------------------------------------------------------------------
418// Trait Implementations
419//--------------------------------------------------------------------------------------------------
420
421impl Default for NetworkBuilder {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427impl Default for TlsBuilder {
428    fn default() -> Self {
429        Self::new()
430    }
431}
432
433impl Default for SecretBuilder {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439//--------------------------------------------------------------------------------------------------
440// Tests
441//--------------------------------------------------------------------------------------------------
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    /// Network builder happy path returns the config unchanged.
448    #[test]
449    fn network_builder_happy_path_returns_config() {
450        let cfg = NetworkBuilder::new()
451            .dns(|d| d.rebind_protection(false))
452            .build()
453            .unwrap();
454        assert!(!cfg.dns.rebind_protection);
455    }
456}