Skip to main content

microsandbox_network/model/config/
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;
7use std::time::Duration;
8
9use ipnetwork::{Ipv4Network, Ipv6Network};
10use microsandbox_types::{
11    NetworkRateLimitDirection, NetworkRateLimiterConfig, RateLimiterConfig, ScopedUpstreamCaCert,
12    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//--------------------------------------------------------------------------------------------------
28// Types
29//--------------------------------------------------------------------------------------------------
30
31/// Fluent builder for [`NetworkConfig`].
32#[derive(Clone)]
33pub struct NetworkBuilder {
34    config: NetworkConfig,
35    errors: Vec<BuildError>,
36}
37
38/// Fluent builder for HTTP denial responses.
39#[derive(Default)]
40pub struct HttpBuilder {
41    config: HttpConfig,
42}
43
44/// Fluent builder for [`DnsConfig`].
45pub struct DnsBuilder {
46    config: DnsConfig,
47}
48
49/// Fluent builder for [`TlsConfig`].
50pub struct TlsBuilder {
51    config: TlsConfig,
52}
53
54/// Fluent builder for a single [`SecretEntry`].
55///
56/// ```ignore
57/// SecretBuilder::new()
58///     .env("OPENAI_API_KEY")
59///     .value(api_key)
60///     .allow("api.openai.com")
61///     .build()
62/// ```
63pub 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/// Fluent builder for both directions of a [`NetworkRateLimiterConfig`].
76///
77/// ```ignore
78/// .rate_limiter(|r| r
79///     .egress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)))
80///     .ingress(|r| r.ops(1_000, Duration::from_secs(1)))
81/// )
82/// ```
83#[derive(Default)]
84pub struct NetworkRateLimiterBuilder {
85    config: NetworkRateLimiterConfig,
86    errors: Vec<BuildError>,
87}
88
89/// Fluent builder for one direction's [`RateLimiterConfig`].
90///
91/// ```ignore
92/// .egress(|r| r
93///     .bandwidth(1.mib(), Duration::from_secs(1))
94///     .bandwidth_burst(512.kib())
95///     .ops(1_000, Duration::from_secs(1))
96///     .ops_burst(500)
97/// )
98/// ```
99pub struct RateLimiterBuilder {
100    direction: NetworkRateLimitDirection,
101    bandwidth: Option<TokenBucketConfig>,
102    ops: Option<TokenBucketConfig>,
103    bandwidth_burst: Option<u64>,
104    ops_burst: Option<u64>,
105    /// First bucket whose refill interval cannot be represented on the wire.
106    refill_error: Option<(&'static str, RefillTimeError)>,
107}
108
109#[derive(Clone, Copy, Debug)]
110enum RefillTimeError {
111    TooShort,
112    Precision,
113    TooLong,
114}
115
116//--------------------------------------------------------------------------------------------------
117// Methods
118//--------------------------------------------------------------------------------------------------
119
120impl HttpBuilder {
121    /// Create HTTP settings with denial responses disabled.
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Enable readable HTTP denial responses. Disabled by default.
127    pub fn deny_response(mut self, enabled: bool) -> Self {
128        self.config.deny_response = enabled;
129        self
130    }
131
132    /// Set the denied HTTP/HTTPS response body. `{host}` names the blocked host.
133    /// Requires `deny_response(true)`. An empty message produces an empty body;
134    /// omission uses the default.
135    pub fn deny_message(mut self, message: impl Into<String>) -> Self {
136        self.config.deny_message = Some(message.into());
137        self
138    }
139
140    /// Return the HTTP configuration.
141    pub fn build(self) -> HttpConfig {
142        self.config
143    }
144}
145
146impl NetworkBuilder {
147    /// Start building a network configuration with defaults.
148    pub fn new() -> Self {
149        Self {
150            config: NetworkConfig::default(),
151            errors: Vec::new(),
152        }
153    }
154
155    /// Start building from an existing network configuration.
156    pub fn from_config(config: NetworkConfig) -> Self {
157        Self {
158            config,
159            errors: Vec::new(),
160        }
161    }
162
163    /// Enable or disable networking.
164    pub fn enabled(mut self, enabled: bool) -> Self {
165        self.config.enabled = enabled;
166        self
167    }
168
169    /// Publish a TCP port: `host_port` on the host maps to `guest_port` in the guest.
170    pub fn port(self, host_port: u16, guest_port: u16) -> Self {
171        self.port_bind(
172            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
173            host_port,
174            guest_port,
175        )
176    }
177
178    /// Publish a UDP port.
179    pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
180        self.port_udp_bind(
181            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
182            host_port,
183            guest_port,
184        )
185    }
186
187    /// Publish a TCP port on a specific host bind address.
188    pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
189        self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
190    }
191
192    /// Publish a UDP port on a specific host bind address.
193    pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
194        self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
195    }
196
197    fn add_port(
198        mut self,
199        host_bind: IpAddr,
200        host_port: u16,
201        guest_port: u16,
202        protocol: PortProtocol,
203    ) -> Self {
204        self.config.ports.push(PublishedPort {
205            host_port,
206            guest_port,
207            protocol,
208            host_bind,
209        });
210        self
211    }
212
213    /// Set the network policy.
214    pub fn policy(mut self, policy: NetworkPolicy) -> Self {
215        self.config.policy = policy;
216        self
217    }
218
219    /// Configure DNS interception via a closure.
220    ///
221    /// ```ignore
222    /// .dns(|d| d
223    ///     .nameservers(["1.1.1.1".parse::<Nameserver>()?])
224    ///     .rebind_protection(false)
225    /// )
226    /// ```
227    pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
228        self.config.dns = f(DnsBuilder::new()).build();
229        self
230    }
231
232    /// Configure DNS starting from the current values instead of defaults.
233    #[doc(hidden)]
234    pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
235        self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
236        self
237    }
238
239    /// Configure TLS interception via a closure.
240    pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
241        self.config.tls = f(TlsBuilder::new()).build();
242        self
243    }
244
245    /// Configure TLS interception starting from the current values instead of defaults.
246    #[doc(hidden)]
247    pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
248        self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
249        self
250    }
251
252    /// Enable or disable strict hostname-policy enforcement.
253    pub fn strict(mut self, enabled: bool) -> Self {
254        self.config.strict = enabled;
255        self
256    }
257
258    /// Add a secret via a closure builder.
259    ///
260    /// ```ignore
261    /// .secret(|s| s
262    ///     .env("OPENAI_API_KEY")
263    ///     .value(api_key)
264    ///     .allow("api.openai.com")
265    /// )
266    /// ```
267    pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
268        self.secret_entry(f(SecretBuilder::new()).build())
269    }
270
271    /// Add a materialized secret entry.
272    pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
273        self.config.secrets.secrets.push(entry);
274        self
275    }
276
277    /// Shorthand: add a secret with env var, value, placeholder, and allowed host.
278    pub fn secret_env(
279        mut self,
280        env_var: impl Into<String>,
281        value: impl Into<String>,
282        placeholder: impl Into<String>,
283        allowed_host: impl Into<String>,
284    ) -> Self {
285        self.config.secrets.secrets.push(SecretEntry {
286            env_var: env_var.into(),
287            value: Zeroizing::new(value.into()),
288            source: None,
289            placeholder: placeholder.into(),
290            allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
291            substitution: SecretSubstitution::default(),
292            passthrough_hosts: Vec::new(),
293            violation_action: None,
294            require_tls_identity: true,
295        });
296        self
297    }
298
299    /// Set the default action for blocked secret placeholders.
300    pub fn secret_violation_action(mut self, action: SecretViolationAction) -> Self {
301        self.config.secrets.violation_action = action;
302        self
303    }
304
305    /// Deprecated alias for [`Self::max_tcp_connections`].
306    #[deprecated(note = "use max_tcp_connections instead")]
307    pub fn max_connections(self, max: usize) -> Self {
308        self.max_tcp_connections(max)
309    }
310
311    /// Set the TCP connection cap; zero explicitly selects unlimited.
312    pub fn max_tcp_connections(mut self, max: usize) -> Self {
313        self.config.max_tcp_connections = Some(ConnectionLimit::from(max));
314        self
315    }
316
317    /// Set the UDP relay session limit; zero selects unlimited. Defaults to unlimited for single-tenant and 1024 for multi-tenant.
318    pub fn max_udp_connections(mut self, max: usize) -> Self {
319        self.config.max_udp_connections = Some(ConnectionLimit::from(max));
320        self
321    }
322
323    /// Set the accept-queue depth for published TCP port listeners. Defaults to 1024.
324    ///
325    /// Valid values are `1..=i32::MAX`; anything else records
326    /// [`BuildError::InvalidTcpAcceptQueueSize`]. The host kernel clamps the request to its own
327    /// ceiling (`net.core.somaxconn` on Linux, `kern.ipc.somaxconn` on macOS).
328    pub fn tcp_accept_queue_size(mut self, size: u32) -> Self {
329        match TcpAcceptQueueSize::try_from(size) {
330            Ok(size) => self.config.tcp_accept_queue_size = Some(size),
331            Err(err) => self.errors.push(err.into()),
332        }
333        self
334    }
335
336    /// Set guest interface overrides.
337    pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
338        self.config.interface = overrides;
339        self
340    }
341
342    /// Set the IPv4 pool used to derive per-sandbox `/30` guest subnets.
343    ///
344    /// The default is `172.16.0.0/12`. Pools must be at least `/30`.
345    pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
346        if pool.prefix() > 30 {
347            self.errors.push(BuildError::InvalidIpv4Pool {
348                raw: pool.to_string(),
349            });
350        } else {
351            self.config.interface.ipv4_pool = Some(pool);
352        }
353        self
354    }
355
356    /// Set the IPv6 pool used to derive per-sandbox `/64` guest prefixes.
357    ///
358    /// The default is `fd42:6d73:62::/48`. Pools must be at least `/64`.
359    pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
360        if pool.prefix() > 64 {
361            self.errors.push(BuildError::InvalidIpv6Pool {
362                raw: pool.to_string(),
363            });
364        } else {
365            self.config.interface.ipv6_pool = Some(pool);
366        }
367        self
368    }
369
370    /// Add a NAT64 `/96` prefix.
371    ///
372    /// Destinations inside NAT64 prefixes are evaluated against both
373    /// their IPv6 address and the embedded IPv4 address. The well-known
374    /// `64:ff9b::/96` prefix is configured by default.
375    pub fn nat64_prefix(mut self, prefix: Ipv6Network) -> Self {
376        if prefix.prefix() != 96 {
377            self.errors.push(BuildError::InvalidNat64Prefix {
378                raw: prefix.to_string(),
379            });
380        } else if !self.config.nat64_prefixes.contains(&prefix) {
381            self.config.nat64_prefixes.push(prefix);
382        }
383
384        self
385    }
386
387    /// Whether to ship the host's trusted root CAs into the guest at
388    /// boot. Default: false. Opt in when running behind a corporate
389    /// TLS-inspecting proxy (Cloudflare Warp Zero Trust, Zscaler,
390    /// Netskope, ...) whose gateway CA is trusted on the host but
391    /// unknown to the guest's stock Mozilla bundle.
392    pub fn trust_host_cas(mut self, enabled: bool) -> Self {
393        self.config.trust_host_cas = enabled;
394        self
395    }
396
397    /// Configure HTTP responses to denied requests.
398    pub fn http(mut self, configure: impl FnOnce(HttpBuilder) -> HttpBuilder) -> Self {
399        self.config.http = configure(HttpBuilder {
400            config: self.config.http,
401        })
402        .build();
403        self
404    }
405
406    /// Configure egress and ingress traffic rate limits. Applies on the next
407    /// sandbox start.
408    ///
409    /// ```ignore
410    /// .rate_limiter(|r| r
411    ///     .egress(|r| r
412    ///         .bandwidth(1.mib(), Duration::from_secs(1))
413    ///         .ops(1_000, Duration::from_secs(1)))
414    /// )
415    /// ```
416    pub fn rate_limiter(
417        mut self,
418        f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
419    ) -> Self {
420        match f(NetworkRateLimiterBuilder::new()).build() {
421            Ok(limiter) => self.config.rate_limiter = Some(limiter),
422            Err(err) => self.errors.push(err),
423        }
424        self
425    }
426
427    /// Consume the builder and return the configuration.
428    ///
429    /// Surfaces the first [`BuildError`] accumulated by any nested
430    /// builder (currently [`DnsBuilder`]). Errors stored on the
431    /// network builder itself flow through here too.
432    pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
433        if let Some(err) = self.errors.drain(..).next() {
434            return Err(err);
435        }
436        if let Some(prefix) = self
437            .config
438            .nat64_prefixes
439            .iter()
440            .find(|prefix| prefix.prefix() != 96)
441        {
442            return Err(BuildError::InvalidNat64Prefix {
443                raw: prefix.to_string(),
444            });
445        }
446        if self.config.tls.enabled
447            && (self.config.tls.intercept_ca.cert_path.is_some()
448                != self.config.tls.intercept_ca.key_path.is_some())
449        {
450            return Err(BuildError::IncompleteInterceptCaConfig);
451        }
452        self.config.secrets.validate()?;
453        Ok(self.config)
454    }
455}
456
457impl DnsBuilder {
458    /// Start building DNS configuration with defaults.
459    pub fn new() -> Self {
460        Self {
461            config: DnsConfig::default(),
462        }
463    }
464
465    fn from_config(config: DnsConfig) -> Self {
466        Self { config }
467    }
468
469    /// Enable or disable DNS rebinding protection. Default: true.
470    pub fn rebind_protection(mut self, enabled: bool) -> Self {
471        self.config.rebind_protection = enabled;
472        self
473    }
474
475    /// Set the upstream nameservers to forward queries to. When one or
476    /// more are set, the interceptor uses these instead of the
477    /// nameservers in the host's `/etc/resolv.conf`. Replaces any
478    /// previously-set nameservers. Each element is any type convertible
479    /// into [`Nameserver`] (`SocketAddr`, `IpAddr`, or a parsed
480    /// string via `"dns.google:53".parse::<Nameserver>()?`).
481    pub fn nameservers<I>(mut self, nameservers: I) -> Self
482    where
483        I: IntoIterator,
484        I::Item: Into<Nameserver>,
485    {
486        self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
487        self
488    }
489
490    /// Set the per-DNS-query timeout in milliseconds. Default: 5000.
491    pub fn query_timeout_ms(mut self, ms: u64) -> Self {
492        self.config.query_timeout_ms = ms;
493        self
494    }
495
496    /// Consume the builder and return the configuration.
497    pub fn build(self) -> DnsConfig {
498        self.config
499    }
500}
501
502impl Default for DnsBuilder {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508impl TlsBuilder {
509    /// Start building TLS configuration.
510    pub fn new() -> Self {
511        Self {
512            config: TlsConfig {
513                enabled: true,
514                ..TlsConfig::default()
515            },
516        }
517    }
518
519    fn from_config(config: TlsConfig) -> Self {
520        Self { config }
521    }
522
523    /// Enable or disable TLS interception while retaining the remaining TLS settings.
524    pub fn enabled(mut self, enabled: bool) -> Self {
525        self.config.enabled = enabled;
526        self
527    }
528
529    /// Add a domain to the bypass list (no MITM). Supports `*.suffix` wildcards.
530    pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
531        self.config.bypass.push(pattern.into());
532        self
533    }
534
535    /// Enable or disable upstream server certificate verification.
536    pub fn verify_upstream(mut self, verify: bool) -> Self {
537        self.config.verify_upstream = verify;
538        self
539    }
540
541    /// Enable or disable upstream server certificate verification only
542    /// when the upstream SNI matches `pattern`.
543    ///
544    /// Pattern syntax matches [`Self::bypass`]: exact hosts and `*.suffix`
545    /// wildcards are supported.
546    pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
547        self.config
548            .scoped_verify_upstream
549            .push(ScopedVerifyUpstream {
550                pattern: pattern.into(),
551                verify,
552            });
553        self
554    }
555
556    /// Set the ports to intercept.
557    pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
558        self.config.intercepted_ports = ports;
559        self
560    }
561
562    /// Enable or disable QUIC blocking on intercepted ports.
563    pub fn block_quic(mut self, block: bool) -> Self {
564        self.config.block_quic_on_intercept = block;
565        self
566    }
567
568    /// Add a CA certificate PEM file to trust for upstream server verification.
569    ///
570    /// Useful when the upstream server uses a self-signed or private CA certificate.
571    /// Can be called multiple times to add several CAs.
572    pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
573        self.config.upstream_ca_cert.push(path.into());
574        self
575    }
576
577    /// Add a CA certificate PEM file to trust for upstream server verification
578    /// only when the upstream SNI matches `pattern`.
579    ///
580    /// Pattern syntax matches [`Self::bypass`]: exact hosts and `*.suffix`
581    /// wildcards are supported. Can be called multiple times to add several
582    /// CAs for the same host pattern.
583    pub fn upstream_ca_cert_for(
584        mut self,
585        pattern: impl Into<String>,
586        path: impl Into<PathBuf>,
587    ) -> Self {
588        self.config
589            .scoped_upstream_ca_cert
590            .push(ScopedUpstreamCaCert {
591                pattern: pattern.into(),
592                path: path.into(),
593            });
594        self
595    }
596
597    /// Set a custom interception CA certificate PEM file path.
598    pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
599        self.config.intercept_ca.cert_path = Some(path.into());
600        self
601    }
602
603    /// Set a custom interception CA private key PEM file path.
604    pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
605        self.config.intercept_ca.key_path = Some(path.into());
606        self
607    }
608
609    /// Consume the builder and return the configuration.
610    pub fn build(self) -> TlsConfig {
611        self.config
612    }
613}
614
615impl SecretBuilder {
616    /// Start building a secret.
617    pub fn new() -> Self {
618        Self {
619            env_var: None,
620            value: None,
621            source: None,
622            placeholder: None,
623            allowed_hosts: Vec::new(),
624            substitution: SecretSubstitution::default(),
625            passthrough_hosts: Vec::new(),
626            violation_action: None,
627            require_tls_identity: true,
628        }
629    }
630
631    /// Set the environment variable to expose the placeholder as (required).
632    ///
633    /// Names must be non-empty and must not contain `=` or NUL. They are
634    /// not restricted to shell-identifier syntax.
635    pub fn env(mut self, var: impl Into<String>) -> Self {
636        self.env_var = Some(var.into());
637        self
638    }
639
640    /// Set the secret value inline (mutually exclusive with [`source`](Self::source)).
641    ///
642    /// Prefer [`source`](Self::source) for durable configs: an inline value is
643    /// persisted verbatim in the sandbox spec, whereas a source reference is
644    /// resolved host-side at spawn time and never stored at rest.
645    pub fn value(mut self, value: impl Into<String>) -> Self {
646        self.value = Some(value.into());
647        self
648    }
649
650    /// Resolve the value from a host-side source reference at spawn time
651    /// (mutually exclusive with [`value`](Self::value)).
652    ///
653    /// The durable config records only the reference; the plaintext is read
654    /// from the host environment when the sandbox starts, so it never lands
655    /// in the database.
656    pub fn source(mut self, source: SecretSource) -> Self {
657        self.source = Some(source);
658        self
659    }
660
661    /// Set a custom placeholder string.
662    ///
663    /// Placeholders must be non-empty, at most 1024 bytes, and must not
664    /// contain NUL, CR, or LF.
665    /// If not set, auto-generated as `$MSB_<env_var>`.
666    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
667        self.placeholder = Some(placeholder.into());
668        self
669    }
670
671    /// Add a host allowed to receive the substituted secret value.
672    ///
673    /// Once the secret's host and TLS identity checks pass, this host may also
674    /// receive unchanged placeholders outside enabled substitution locations.
675    ///
676    /// `*.example.com` matches the domain and its subdomains. Use
677    /// [`allow_any_host_dangerous`](Self::allow_any_host_dangerous) for `*`.
678    pub fn allow(mut self, host: impl AsRef<str>) -> Self {
679        let host = host.as_ref();
680        assert!(
681            host != "*",
682            "SecretBuilder: use .allow_any_host_dangerous(true) for an explicit any-host secret"
683        );
684        self.allowed_hosts.push(HostPattern::parse(host));
685        self
686    }
687
688    /// Allow for any host. **Dangerous**: secret can be exfiltrated to any
689    /// destination. Requires explicit acknowledgment.
690    pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
691        if i_understand_the_risk {
692            self.allowed_hosts.push(HostPattern::Any);
693        }
694        self
695    }
696
697    /// Allow a host to receive the unchanged placeholder where substitution does not apply.
698    ///
699    /// Exact hosts, `*.example.com`, and `*` are accepted. Repeated calls are additive.
700    /// Enabled substitution locations still receive the real secret on allowed hosts.
701    pub fn allow_placeholder_for(mut self, host: impl AsRef<str>) -> Self {
702        self.passthrough_hosts
703            .push(HostPattern::parse(host.as_ref()));
704        self
705    }
706
707    /// Deprecated alias for [`allow_placeholder_for`](Self::allow_placeholder_for).
708    #[deprecated(note = "use allow_placeholder_for instead")]
709    pub fn allow_passthrough_for(self, host: impl AsRef<str>) -> Self {
710        self.allow_placeholder_for(host)
711    }
712
713    /// Set the blocking action for this secret.
714    pub fn violation_action(mut self, action: SecretViolationAction) -> Self {
715        self.violation_action = Some(action);
716        self
717    }
718
719    /// Require verified TLS identity before substituting (default: true).
720    pub fn require_tls_identity(mut self, enabled: bool) -> Self {
721        self.require_tls_identity = enabled;
722        self
723    }
724
725    /// Configure header substitution (default: true).
726    pub fn substitute_in_headers(mut self, enabled: bool) -> Self {
727        self.substitution.headers = enabled;
728        self
729    }
730
731    /// Restrict header substitution to specific header field names.
732    ///
733    /// Enables header substitution and substitutes the placeholder only in the
734    /// named fields (matched ASCII case-insensitively). Passing an empty list
735    /// restores the default of substituting in every header field. Names must
736    /// be valid HTTP field names.
737    ///
738    /// Prefer restricting to the intended credential header (for example
739    /// `authorization`). Substituting in every header lets an untrusted guest
740    /// put the placeholder in a header the upstream host echoes back, which
741    /// can leak the real secret to the guest through that response.
742    pub fn substitute_in_header_fields(
743        mut self,
744        fields: impl IntoIterator<Item = impl Into<String>>,
745    ) -> Self {
746        self.substitution.headers = true;
747        self.substitution.header_fields = fields.into_iter().map(Into::into).collect();
748        self
749    }
750
751    /// Configure query parameter substitution (default: false).
752    pub fn substitute_in_query(mut self, enabled: bool) -> Self {
753        self.substitution.query = enabled;
754        self
755    }
756
757    /// Configure HTTP/1 body substitution (default: false).
758    ///
759    /// Fixed-length bodies up to 16 MiB update `Content-Length`; larger
760    /// fixed-length bodies are blocked. Chunked bodies are decoded and
761    /// re-encoded with fresh chunk sizes. Encoded bodies pass through
762    /// unchanged.
763    pub fn substitute_in_body(mut self, enabled: bool) -> Self {
764        self.substitution.body = enabled;
765        self
766    }
767
768    /// Consume the builder and return a [`SecretEntry`].
769    ///
770    /// Exactly one of [`value`](Self::value) or [`source`](Self::source) must
771    /// be set. A source-backed entry carries an empty durable value; it is
772    /// resolved host-side at spawn time.
773    ///
774    /// # Panics
775    /// Panics if `env` or at least one allowed host was not set, or if neither
776    /// (or both) of `value`/`source` was set.
777    pub fn build(self) -> SecretEntry {
778        let env_var = self.env_var.expect("SecretBuilder: .env() is required");
779        assert!(
780            self.value.is_some() ^ self.source.is_some(),
781            "SecretBuilder: exactly one of .value() or .source() is required"
782        );
783        assert!(
784            !self.allowed_hosts.is_empty(),
785            "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
786        );
787        let placeholder = self
788            .placeholder
789            .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
790
791        SecretEntry {
792            env_var,
793            value: Zeroizing::new(self.value.unwrap_or_default()),
794            source: self.source,
795            placeholder,
796            allowed_hosts: self.allowed_hosts,
797            substitution: self.substitution,
798            passthrough_hosts: self.passthrough_hosts,
799            violation_action: self.violation_action,
800            require_tls_identity: self.require_tls_identity,
801        }
802    }
803}
804
805impl NetworkRateLimiterBuilder {
806    fn new() -> Self {
807        Self::default()
808    }
809
810    /// Limit guest-to-runtime (egress) traffic.
811    pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
812        match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
813            Ok(limiter) => self.config.egress = Some(limiter),
814            Err(err) => self.errors.push(err),
815        }
816        self
817    }
818
819    /// Limit runtime-to-guest (ingress) traffic.
820    pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
821        match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
822            Ok(limiter) => self.config.ingress = Some(limiter),
823            Err(err) => self.errors.push(err),
824        }
825        self
826    }
827
828    /// Consume the builder and return both configured directions.
829    pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
830        if let Some(error) = self.errors.drain(..).next() {
831            return Err(error);
832        }
833        if self.config.egress.is_none() && self.config.ingress.is_none() {
834            return Err(BuildError::EmptyNetworkRateLimiter);
835        }
836        Ok(self.config)
837    }
838}
839
840impl RateLimiterBuilder {
841    fn new(direction: NetworkRateLimitDirection) -> Self {
842        Self {
843            direction,
844            bandwidth: None,
845            ops: None,
846            bandwidth_burst: None,
847            ops_burst: None,
848            refill_error: None,
849        }
850    }
851
852    /// Cap bandwidth at `size` bytes per `refill_time`.
853    ///
854    /// `refill_time` must be at least one millisecond and exactly representable
855    /// as a whole number of milliseconds.
856    ///
857    /// ```ignore
858    /// .bandwidth(1.mib(), Duration::from_secs(1))
859    /// ```
860    pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
861        match refill_time_ms(refill_time) {
862            Ok(refill_time_ms) => {
863                self.bandwidth = Some(TokenBucketConfig {
864                    size: size.into().as_u64(),
865                    refill_time_ms,
866                    one_time_burst: 0,
867                });
868            }
869            Err(error) => {
870                self.refill_error.get_or_insert(("bandwidth", error));
871            }
872        }
873        self
874    }
875
876    /// Grant a one-time startup burst of `burst` bytes on top of the
877    /// bandwidth bucket. Requires [`bandwidth`](Self::bandwidth).
878    pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
879        self.bandwidth_burst = Some(burst.into().as_u64());
880        self
881    }
882
883    /// Cap packet rate at `count` frames per `refill_time`.
884    ///
885    /// `refill_time` must be at least one millisecond and exactly representable
886    /// as a whole number of milliseconds.
887    ///
888    /// ```ignore
889    /// .ops(1_000, Duration::from_secs(1))
890    /// ```
891    pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
892        match refill_time_ms(refill_time) {
893            Ok(refill_time_ms) => {
894                self.ops = Some(TokenBucketConfig {
895                    size: count,
896                    refill_time_ms,
897                    one_time_burst: 0,
898                });
899            }
900            Err(error) => {
901                self.refill_error.get_or_insert(("ops", error));
902            }
903        }
904        self
905    }
906
907    /// Grant a one-time startup burst of `count` frames on top of the ops
908    /// bucket. Requires [`ops`](Self::ops).
909    pub fn ops_burst(mut self, count: u64) -> Self {
910        self.ops_burst = Some(count);
911        self
912    }
913
914    /// Consume the builder and return the validated configuration.
915    pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
916        let direction = self.direction;
917        if let Some((bucket, error)) = self.refill_error {
918            return Err(match error {
919                RefillTimeError::TooShort => {
920                    BuildError::RateLimitRefillTooShort { direction, bucket }
921                }
922                RefillTimeError::Precision => {
923                    BuildError::RateLimitRefillPrecision { direction, bucket }
924                }
925                RefillTimeError::TooLong => {
926                    BuildError::RateLimitRefillTooLong { direction, bucket }
927                }
928            });
929        }
930
931        let mut config = RateLimiterConfig {
932            bandwidth: self.bandwidth,
933            ops: self.ops,
934        };
935        if let Some(burst) = self.bandwidth_burst {
936            let Some(bandwidth) = &mut config.bandwidth else {
937                return Err(BuildError::RateLimitBurstWithoutBucket {
938                    direction,
939                    bucket: "bandwidth",
940                });
941            };
942            bandwidth.one_time_burst = burst;
943        }
944        if let Some(burst) = self.ops_burst {
945            let Some(ops) = &mut config.ops else {
946                return Err(BuildError::RateLimitBurstWithoutBucket {
947                    direction,
948                    bucket: "ops",
949                });
950            };
951            ops.one_time_burst = burst;
952        }
953
954        config
955            .validate()
956            .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
957        Ok(config)
958    }
959}
960
961//--------------------------------------------------------------------------------------------------
962// Functions
963//--------------------------------------------------------------------------------------------------
964
965/// Convert a refill interval to its exact whole-millisecond wire value.
966fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
967    if refill_time < Duration::from_millis(1) {
968        return Err(RefillTimeError::TooShort);
969    }
970    let refill_time_ms =
971        u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
972    if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
973        return Err(RefillTimeError::Precision);
974    }
975    Ok(refill_time_ms)
976}
977
978//--------------------------------------------------------------------------------------------------
979// Trait Implementations
980//--------------------------------------------------------------------------------------------------
981
982impl Default for NetworkBuilder {
983    fn default() -> Self {
984        Self::new()
985    }
986}
987
988impl Default for TlsBuilder {
989    fn default() -> Self {
990        Self::new()
991    }
992}
993
994impl Default for SecretBuilder {
995    fn default() -> Self {
996        Self::new()
997    }
998}
999//--------------------------------------------------------------------------------------------------
1000// Tests
1001//--------------------------------------------------------------------------------------------------
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    #[test]
1008    #[allow(deprecated)]
1009    fn deprecated_tcp_builder_delegates_to_the_new_name() {
1010        let canonical_last = NetworkBuilder::new()
1011            .max_connections(0)
1012            .max_tcp_connections(64)
1013            .build()
1014            .unwrap();
1015        assert_eq!(
1016            canonical_last.max_tcp_connections,
1017            Some(ConnectionLimit::from(64))
1018        );
1019        let legacy_last = NetworkBuilder::new()
1020            .max_tcp_connections(64)
1021            .max_connections(0)
1022            .build()
1023            .unwrap();
1024        assert_eq!(
1025            legacy_last.max_tcp_connections,
1026            Some(ConnectionLimit::Unlimited)
1027        );
1028        let config = NetworkBuilder::new()
1029            .max_connections(0)
1030            .max_udp_connections(7)
1031            .build()
1032            .unwrap();
1033        assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
1034        assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
1035        assert!(
1036            NetworkBuilder::new()
1037                .max_tcp_connections(1)
1038                .max_tcp_connections(2)
1039                .build()
1040                .is_ok()
1041        );
1042    }
1043
1044    #[test]
1045    fn tcp_accept_queue_size_is_unset_by_default_and_rejects_out_of_range_values() {
1046        assert_eq!(
1047            NetworkBuilder::new().build().unwrap().tcp_accept_queue_size,
1048            None
1049        );
1050        let config = NetworkBuilder::new()
1051            .tcp_accept_queue_size(4096)
1052            .build()
1053            .unwrap();
1054        assert_eq!(
1055            config.tcp_accept_queue_size.map(TcpAcceptQueueSize::get),
1056            Some(4096)
1057        );
1058
1059        for invalid in [0, TcpAcceptQueueSize::MAX + 1] {
1060            let err = NetworkBuilder::new()
1061                .tcp_accept_queue_size(invalid)
1062                .build()
1063                .unwrap_err();
1064            assert!(
1065                matches!(
1066                    err,
1067                    BuildError::InvalidTcpAcceptQueueSize { source } if source.value == invalid
1068                ),
1069                "{invalid}: {err}"
1070            );
1071        }
1072    }
1073
1074    /// Network builder happy path returns the config unchanged.
1075    #[test]
1076    fn network_builder_happy_path_returns_config() {
1077        let cfg = NetworkBuilder::new()
1078            .dns(|d| d.rebind_protection(false))
1079            .build()
1080            .unwrap();
1081        assert!(!cfg.dns.rebind_protection);
1082    }
1083
1084    #[test]
1085    fn network_builder_preserves_explicit_large_caps() {
1086        for limit in [10000, usize::MAX] {
1087            let config = NetworkBuilder::new()
1088                .max_tcp_connections(limit)
1089                .build()
1090                .unwrap();
1091            assert_eq!(
1092                config.max_tcp_connections,
1093                Some(ConnectionLimit::from(limit))
1094            );
1095        }
1096    }
1097
1098    #[test]
1099    fn network_builder_rejects_incomplete_intercept_ca_config() {
1100        let err = NetworkBuilder::new()
1101            .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1102            .build()
1103            .unwrap_err();
1104
1105        assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1106    }
1107
1108    #[test]
1109    fn network_builder_rejects_non_96_nat64_prefix() {
1110        let err = NetworkBuilder::new()
1111            .nat64_prefix("64:ff9b::/64".parse().unwrap())
1112            .build()
1113            .unwrap_err();
1114
1115        assert!(matches!(err, BuildError::InvalidNat64Prefix { .. }));
1116    }
1117
1118    #[test]
1119    fn port_bind_sets_host_bind() {
1120        let bind = "0.0.0.0".parse().unwrap();
1121        let cfg = NetworkBuilder::new()
1122            .port_bind(bind, 8080, 80)
1123            .port_udp_bind(bind, 5353, 53)
1124            .build()
1125            .unwrap();
1126
1127        assert_eq!(cfg.ports[0].host_bind, bind);
1128        assert_eq!(cfg.ports[0].host_port, 8080);
1129        assert_eq!(cfg.ports[0].guest_port, 80);
1130        assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1131        assert_eq!(cfg.ports[1].host_bind, bind);
1132        assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1133    }
1134
1135    #[test]
1136    fn port_helpers_default_to_loopback() {
1137        let cfg = NetworkBuilder::new()
1138            .port(8080, 80)
1139            .port_udp(5353, 53)
1140            .build()
1141            .unwrap();
1142
1143        assert_eq!(
1144            cfg.ports[0].host_bind,
1145            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1146        );
1147        assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1148        assert_eq!(
1149            cfg.ports[1].host_bind,
1150            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1151        );
1152        assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1153    }
1154
1155    #[test]
1156    fn outbound_proxy_defaults_to_none() {
1157        let cfg = NetworkBuilder::new().build().unwrap();
1158        assert_eq!(cfg.outbound_proxy, None);
1159    }
1160
1161    #[test]
1162    fn network_builder_sets_strict_mode() {
1163        let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1164        assert!(cfg.strict);
1165    }
1166
1167    #[test]
1168    fn network_builder_sets_global_violation_action() {
1169        let cfg = NetworkBuilder::new()
1170            .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1171            .build()
1172            .unwrap();
1173
1174        assert_eq!(
1175            cfg.secrets.violation_action,
1176            SecretViolationAction::BlockAndTerminate
1177        );
1178    }
1179
1180    #[test]
1181    #[allow(deprecated)] // Exercise the retained alias alongside the preferred name.
1182    fn secret_builder_sets_passthrough_and_violation_policies() {
1183        let secret = SecretBuilder::new()
1184            .env("TOKEN")
1185            .value("secret-value")
1186            .allow("api.github.com")
1187            .allow_placeholder_for("api.anthropic.com")
1188            .allow_passthrough_for("*.anthropic.com")
1189            .violation_action(SecretViolationAction::BlockAndTerminate)
1190            .build();
1191
1192        assert_eq!(
1193            secret.violation_action,
1194            Some(SecretViolationAction::BlockAndTerminate),
1195        );
1196        assert_eq!(
1197            secret.passthrough_hosts,
1198            vec![
1199                HostPattern::Exact("api.anthropic.com".into()),
1200                HostPattern::Wildcard("*.anthropic.com".into()),
1201            ],
1202        );
1203    }
1204
1205    #[test]
1206    #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1207    fn secret_builder_rejects_empty_allowed_hosts() {
1208        let _ = SecretBuilder::new()
1209            .env("TOKEN")
1210            .value("secret-value")
1211            .build();
1212    }
1213
1214    #[test]
1215    fn secret_builder_source_yields_reference_and_empty_value() {
1216        let secret = SecretBuilder::new()
1217            .env("API_KEY")
1218            .source(SecretSource::Env {
1219                var: "HOST_API_KEY".into(),
1220            })
1221            .allow("api.example.com")
1222            .build();
1223
1224        assert!(secret.value.is_empty());
1225        assert_eq!(
1226            secret.source,
1227            Some(SecretSource::Env {
1228                var: "HOST_API_KEY".into()
1229            })
1230        );
1231
1232        // Serialized durable form carries the reference, not a value.
1233        let json = serde_json::to_string(&secret).unwrap();
1234        assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1235    }
1236
1237    #[test]
1238    #[should_panic(expected = "exactly one of .value() or .source()")]
1239    fn secret_builder_rejects_both_value_and_source() {
1240        let _ = SecretBuilder::new()
1241            .env("API_KEY")
1242            .value("inline")
1243            .source(SecretSource::Env {
1244                var: "HOST_API_KEY".into(),
1245            })
1246            .allow("api.example.com")
1247            .build();
1248    }
1249
1250    #[test]
1251    fn network_builder_rejects_invalid_secret_config() {
1252        let err = NetworkBuilder::new()
1253            .secret_entry(SecretEntry {
1254                env_var: "API=KEY".into(),
1255                value: Zeroizing::new("secret-value".into()),
1256                source: None,
1257                placeholder: "$MSB_API_KEY".into(),
1258                allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1259                substitution: SecretSubstitution::default(),
1260                passthrough_hosts: Vec::new(),
1261                violation_action: None,
1262                require_tls_identity: true,
1263            })
1264            .build()
1265            .unwrap_err();
1266
1267        assert!(err.to_string().contains("env_var must not contain `=`"));
1268    }
1269
1270    #[test]
1271    fn rate_limiter_builder_sets_buckets_and_bursts() {
1272        use microsandbox_utils::size::SizeExt;
1273
1274        let cfg = NetworkBuilder::new()
1275            .rate_limiter(|r| {
1276                r.egress(|r| {
1277                    r.bandwidth(1.mib(), Duration::from_secs(1))
1278                        .bandwidth_burst(512.kib())
1279                        .ops(1_000, Duration::from_secs(1))
1280                        .ops_burst(500)
1281                })
1282                .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1283            })
1284            .build()
1285            .unwrap();
1286
1287        let rate_limiter = cfg.rate_limiter.unwrap();
1288        let egress = rate_limiter.egress.unwrap();
1289        let bandwidth = egress.bandwidth.unwrap();
1290        assert_eq!(bandwidth.size, 1024 * 1024);
1291        assert_eq!(bandwidth.refill_time_ms, 1000);
1292        assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1293        let ops = egress.ops.unwrap();
1294        assert_eq!(ops.size, 1_000);
1295        assert_eq!(ops.refill_time_ms, 1000);
1296        assert_eq!(ops.one_time_burst, 500);
1297
1298        let ingress = rate_limiter.ingress.unwrap();
1299        assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1300        assert!(ingress.ops.is_none());
1301    }
1302
1303    #[test]
1304    fn rate_limiters_default_to_unlimited() {
1305        let cfg = NetworkBuilder::new().build().unwrap();
1306        assert!(cfg.rate_limiter.is_none());
1307    }
1308
1309    #[test]
1310    fn rate_limiter_builder_rejects_empty_limiter() {
1311        let err = NetworkBuilder::new()
1312            .rate_limiter(|r| r.egress(|r| r))
1313            .build()
1314            .unwrap_err();
1315        assert_eq!(
1316            err.to_string(),
1317            "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1318        );
1319    }
1320
1321    #[test]
1322    fn network_rate_limiter_builder_rejects_missing_directions() {
1323        let err = NetworkBuilder::new()
1324            .rate_limiter(|r| r)
1325            .build()
1326            .unwrap_err();
1327        assert_eq!(
1328            err.to_string(),
1329            "rate limiter must configure at least one of egress or ingress"
1330        );
1331    }
1332
1333    #[test]
1334    fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1335        let err = NetworkBuilder::new()
1336            .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1337            .build()
1338            .unwrap_err();
1339        assert_eq!(
1340            err.to_string(),
1341            "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1342        );
1343
1344        let err = NetworkBuilder::new()
1345            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1346            .build()
1347            .unwrap_err();
1348        assert_eq!(
1349            err.to_string(),
1350            "egress rate limiter: ops refill interval must be at least one millisecond"
1351        );
1352
1353        let err = NetworkBuilder::new()
1354            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1355            .build()
1356            .unwrap_err();
1357        assert_eq!(
1358            err.to_string(),
1359            "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1360        );
1361    }
1362
1363    #[test]
1364    fn rate_limiter_builder_rejects_burst_without_bucket() {
1365        use microsandbox_utils::size::SizeExt;
1366
1367        let err = NetworkBuilder::new()
1368            .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1369            .build()
1370            .unwrap_err();
1371        assert_eq!(
1372            err.to_string(),
1373            "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1374        );
1375
1376        let err = NetworkBuilder::new()
1377            .rate_limiter(|r| {
1378                r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1379            })
1380            .build()
1381            .unwrap_err();
1382        assert_eq!(
1383            err.to_string(),
1384            "ingress rate limiter: ops_burst requires the ops bucket"
1385        );
1386    }
1387
1388    #[test]
1389    fn rate_limiter_builder_rejects_refill_interval_overflow() {
1390        let err = NetworkBuilder::new()
1391            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1392            .build()
1393            .unwrap_err();
1394        assert_eq!(
1395            err.to_string(),
1396            "egress rate limiter: ops refill interval overflows u64 milliseconds"
1397        );
1398    }
1399}