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    /// Configure query parameter substitution (default: false).
732    pub fn substitute_in_query(mut self, enabled: bool) -> Self {
733        self.substitution.query = enabled;
734        self
735    }
736
737    /// Configure HTTP/1 body substitution (default: false).
738    ///
739    /// Fixed-length bodies up to 16 MiB update `Content-Length`; larger
740    /// fixed-length bodies are blocked. Chunked bodies are decoded and
741    /// re-encoded with fresh chunk sizes. Encoded bodies pass through
742    /// unchanged.
743    pub fn substitute_in_body(mut self, enabled: bool) -> Self {
744        self.substitution.body = enabled;
745        self
746    }
747
748    /// Consume the builder and return a [`SecretEntry`].
749    ///
750    /// Exactly one of [`value`](Self::value) or [`source`](Self::source) must
751    /// be set. A source-backed entry carries an empty durable value; it is
752    /// resolved host-side at spawn time.
753    ///
754    /// # Panics
755    /// Panics if `env` or at least one allowed host was not set, or if neither
756    /// (or both) of `value`/`source` was set.
757    pub fn build(self) -> SecretEntry {
758        let env_var = self.env_var.expect("SecretBuilder: .env() is required");
759        assert!(
760            self.value.is_some() ^ self.source.is_some(),
761            "SecretBuilder: exactly one of .value() or .source() is required"
762        );
763        assert!(
764            !self.allowed_hosts.is_empty(),
765            "SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
766        );
767        let placeholder = self
768            .placeholder
769            .unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
770
771        SecretEntry {
772            env_var,
773            value: Zeroizing::new(self.value.unwrap_or_default()),
774            source: self.source,
775            placeholder,
776            allowed_hosts: self.allowed_hosts,
777            substitution: self.substitution,
778            passthrough_hosts: self.passthrough_hosts,
779            violation_action: self.violation_action,
780            require_tls_identity: self.require_tls_identity,
781        }
782    }
783}
784
785impl NetworkRateLimiterBuilder {
786    fn new() -> Self {
787        Self::default()
788    }
789
790    /// Limit guest-to-runtime (egress) traffic.
791    pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
792        match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
793            Ok(limiter) => self.config.egress = Some(limiter),
794            Err(err) => self.errors.push(err),
795        }
796        self
797    }
798
799    /// Limit runtime-to-guest (ingress) traffic.
800    pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
801        match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
802            Ok(limiter) => self.config.ingress = Some(limiter),
803            Err(err) => self.errors.push(err),
804        }
805        self
806    }
807
808    /// Consume the builder and return both configured directions.
809    pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
810        if let Some(error) = self.errors.drain(..).next() {
811            return Err(error);
812        }
813        if self.config.egress.is_none() && self.config.ingress.is_none() {
814            return Err(BuildError::EmptyNetworkRateLimiter);
815        }
816        Ok(self.config)
817    }
818}
819
820impl RateLimiterBuilder {
821    fn new(direction: NetworkRateLimitDirection) -> Self {
822        Self {
823            direction,
824            bandwidth: None,
825            ops: None,
826            bandwidth_burst: None,
827            ops_burst: None,
828            refill_error: None,
829        }
830    }
831
832    /// Cap bandwidth at `size` bytes per `refill_time`.
833    ///
834    /// `refill_time` must be at least one millisecond and exactly representable
835    /// as a whole number of milliseconds.
836    ///
837    /// ```ignore
838    /// .bandwidth(1.mib(), Duration::from_secs(1))
839    /// ```
840    pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
841        match refill_time_ms(refill_time) {
842            Ok(refill_time_ms) => {
843                self.bandwidth = Some(TokenBucketConfig {
844                    size: size.into().as_u64(),
845                    refill_time_ms,
846                    one_time_burst: 0,
847                });
848            }
849            Err(error) => {
850                self.refill_error.get_or_insert(("bandwidth", error));
851            }
852        }
853        self
854    }
855
856    /// Grant a one-time startup burst of `burst` bytes on top of the
857    /// bandwidth bucket. Requires [`bandwidth`](Self::bandwidth).
858    pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
859        self.bandwidth_burst = Some(burst.into().as_u64());
860        self
861    }
862
863    /// Cap packet rate at `count` frames per `refill_time`.
864    ///
865    /// `refill_time` must be at least one millisecond and exactly representable
866    /// as a whole number of milliseconds.
867    ///
868    /// ```ignore
869    /// .ops(1_000, Duration::from_secs(1))
870    /// ```
871    pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
872        match refill_time_ms(refill_time) {
873            Ok(refill_time_ms) => {
874                self.ops = Some(TokenBucketConfig {
875                    size: count,
876                    refill_time_ms,
877                    one_time_burst: 0,
878                });
879            }
880            Err(error) => {
881                self.refill_error.get_or_insert(("ops", error));
882            }
883        }
884        self
885    }
886
887    /// Grant a one-time startup burst of `count` frames on top of the ops
888    /// bucket. Requires [`ops`](Self::ops).
889    pub fn ops_burst(mut self, count: u64) -> Self {
890        self.ops_burst = Some(count);
891        self
892    }
893
894    /// Consume the builder and return the validated configuration.
895    pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
896        let direction = self.direction;
897        if let Some((bucket, error)) = self.refill_error {
898            return Err(match error {
899                RefillTimeError::TooShort => {
900                    BuildError::RateLimitRefillTooShort { direction, bucket }
901                }
902                RefillTimeError::Precision => {
903                    BuildError::RateLimitRefillPrecision { direction, bucket }
904                }
905                RefillTimeError::TooLong => {
906                    BuildError::RateLimitRefillTooLong { direction, bucket }
907                }
908            });
909        }
910
911        let mut config = RateLimiterConfig {
912            bandwidth: self.bandwidth,
913            ops: self.ops,
914        };
915        if let Some(burst) = self.bandwidth_burst {
916            let Some(bandwidth) = &mut config.bandwidth else {
917                return Err(BuildError::RateLimitBurstWithoutBucket {
918                    direction,
919                    bucket: "bandwidth",
920                });
921            };
922            bandwidth.one_time_burst = burst;
923        }
924        if let Some(burst) = self.ops_burst {
925            let Some(ops) = &mut config.ops else {
926                return Err(BuildError::RateLimitBurstWithoutBucket {
927                    direction,
928                    bucket: "ops",
929                });
930            };
931            ops.one_time_burst = burst;
932        }
933
934        config
935            .validate()
936            .map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
937        Ok(config)
938    }
939}
940
941//--------------------------------------------------------------------------------------------------
942// Functions
943//--------------------------------------------------------------------------------------------------
944
945/// Convert a refill interval to its exact whole-millisecond wire value.
946fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
947    if refill_time < Duration::from_millis(1) {
948        return Err(RefillTimeError::TooShort);
949    }
950    let refill_time_ms =
951        u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
952    if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
953        return Err(RefillTimeError::Precision);
954    }
955    Ok(refill_time_ms)
956}
957
958//--------------------------------------------------------------------------------------------------
959// Trait Implementations
960//--------------------------------------------------------------------------------------------------
961
962impl Default for NetworkBuilder {
963    fn default() -> Self {
964        Self::new()
965    }
966}
967
968impl Default for TlsBuilder {
969    fn default() -> Self {
970        Self::new()
971    }
972}
973
974impl Default for SecretBuilder {
975    fn default() -> Self {
976        Self::new()
977    }
978}
979//--------------------------------------------------------------------------------------------------
980// Tests
981//--------------------------------------------------------------------------------------------------
982
983#[cfg(test)]
984mod tests {
985    use super::*;
986
987    #[test]
988    #[allow(deprecated)]
989    fn deprecated_tcp_builder_delegates_to_the_new_name() {
990        let canonical_last = NetworkBuilder::new()
991            .max_connections(0)
992            .max_tcp_connections(64)
993            .build()
994            .unwrap();
995        assert_eq!(
996            canonical_last.max_tcp_connections,
997            Some(ConnectionLimit::from(64))
998        );
999        let legacy_last = NetworkBuilder::new()
1000            .max_tcp_connections(64)
1001            .max_connections(0)
1002            .build()
1003            .unwrap();
1004        assert_eq!(
1005            legacy_last.max_tcp_connections,
1006            Some(ConnectionLimit::Unlimited)
1007        );
1008        let config = NetworkBuilder::new()
1009            .max_connections(0)
1010            .max_udp_connections(7)
1011            .build()
1012            .unwrap();
1013        assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::Unlimited));
1014        assert_eq!(config.max_udp_connections, Some(ConnectionLimit::from(7)));
1015        assert!(
1016            NetworkBuilder::new()
1017                .max_tcp_connections(1)
1018                .max_tcp_connections(2)
1019                .build()
1020                .is_ok()
1021        );
1022    }
1023
1024    #[test]
1025    fn tcp_accept_queue_size_is_unset_by_default_and_rejects_out_of_range_values() {
1026        assert_eq!(
1027            NetworkBuilder::new().build().unwrap().tcp_accept_queue_size,
1028            None
1029        );
1030        let config = NetworkBuilder::new()
1031            .tcp_accept_queue_size(4096)
1032            .build()
1033            .unwrap();
1034        assert_eq!(
1035            config.tcp_accept_queue_size.map(TcpAcceptQueueSize::get),
1036            Some(4096)
1037        );
1038
1039        for invalid in [0, TcpAcceptQueueSize::MAX + 1] {
1040            let err = NetworkBuilder::new()
1041                .tcp_accept_queue_size(invalid)
1042                .build()
1043                .unwrap_err();
1044            assert!(
1045                matches!(
1046                    err,
1047                    BuildError::InvalidTcpAcceptQueueSize { source } if source.value == invalid
1048                ),
1049                "{invalid}: {err}"
1050            );
1051        }
1052    }
1053
1054    /// Network builder happy path returns the config unchanged.
1055    #[test]
1056    fn network_builder_happy_path_returns_config() {
1057        let cfg = NetworkBuilder::new()
1058            .dns(|d| d.rebind_protection(false))
1059            .build()
1060            .unwrap();
1061        assert!(!cfg.dns.rebind_protection);
1062    }
1063
1064    #[test]
1065    fn network_builder_preserves_explicit_large_caps() {
1066        for limit in [10000, usize::MAX] {
1067            let config = NetworkBuilder::new()
1068                .max_tcp_connections(limit)
1069                .build()
1070                .unwrap();
1071            assert_eq!(
1072                config.max_tcp_connections,
1073                Some(ConnectionLimit::from(limit))
1074            );
1075        }
1076    }
1077
1078    #[test]
1079    fn network_builder_rejects_incomplete_intercept_ca_config() {
1080        let err = NetworkBuilder::new()
1081            .tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
1082            .build()
1083            .unwrap_err();
1084
1085        assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
1086    }
1087
1088    #[test]
1089    fn network_builder_rejects_non_96_nat64_prefix() {
1090        let err = NetworkBuilder::new()
1091            .nat64_prefix("64:ff9b::/64".parse().unwrap())
1092            .build()
1093            .unwrap_err();
1094
1095        assert!(matches!(err, BuildError::InvalidNat64Prefix { .. }));
1096    }
1097
1098    #[test]
1099    fn port_bind_sets_host_bind() {
1100        let bind = "0.0.0.0".parse().unwrap();
1101        let cfg = NetworkBuilder::new()
1102            .port_bind(bind, 8080, 80)
1103            .port_udp_bind(bind, 5353, 53)
1104            .build()
1105            .unwrap();
1106
1107        assert_eq!(cfg.ports[0].host_bind, bind);
1108        assert_eq!(cfg.ports[0].host_port, 8080);
1109        assert_eq!(cfg.ports[0].guest_port, 80);
1110        assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1111        assert_eq!(cfg.ports[1].host_bind, bind);
1112        assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1113    }
1114
1115    #[test]
1116    fn port_helpers_default_to_loopback() {
1117        let cfg = NetworkBuilder::new()
1118            .port(8080, 80)
1119            .port_udp(5353, 53)
1120            .build()
1121            .unwrap();
1122
1123        assert_eq!(
1124            cfg.ports[0].host_bind,
1125            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1126        );
1127        assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
1128        assert_eq!(
1129            cfg.ports[1].host_bind,
1130            IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
1131        );
1132        assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
1133    }
1134
1135    #[test]
1136    fn outbound_proxy_defaults_to_none() {
1137        let cfg = NetworkBuilder::new().build().unwrap();
1138        assert_eq!(cfg.outbound_proxy, None);
1139    }
1140
1141    #[test]
1142    fn network_builder_sets_strict_mode() {
1143        let cfg = NetworkBuilder::new().strict(true).build().unwrap();
1144        assert!(cfg.strict);
1145    }
1146
1147    #[test]
1148    fn network_builder_sets_global_violation_action() {
1149        let cfg = NetworkBuilder::new()
1150            .secret_violation_action(SecretViolationAction::BlockAndTerminate)
1151            .build()
1152            .unwrap();
1153
1154        assert_eq!(
1155            cfg.secrets.violation_action,
1156            SecretViolationAction::BlockAndTerminate
1157        );
1158    }
1159
1160    #[test]
1161    #[allow(deprecated)] // Exercise the retained alias alongside the preferred name.
1162    fn secret_builder_sets_passthrough_and_violation_policies() {
1163        let secret = SecretBuilder::new()
1164            .env("TOKEN")
1165            .value("secret-value")
1166            .allow("api.github.com")
1167            .allow_placeholder_for("api.anthropic.com")
1168            .allow_passthrough_for("*.anthropic.com")
1169            .violation_action(SecretViolationAction::BlockAndTerminate)
1170            .build();
1171
1172        assert_eq!(
1173            secret.violation_action,
1174            Some(SecretViolationAction::BlockAndTerminate),
1175        );
1176        assert_eq!(
1177            secret.passthrough_hosts,
1178            vec![
1179                HostPattern::Exact("api.anthropic.com".into()),
1180                HostPattern::Wildcard("*.anthropic.com".into()),
1181            ],
1182        );
1183    }
1184
1185    #[test]
1186    #[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
1187    fn secret_builder_rejects_empty_allowed_hosts() {
1188        let _ = SecretBuilder::new()
1189            .env("TOKEN")
1190            .value("secret-value")
1191            .build();
1192    }
1193
1194    #[test]
1195    fn secret_builder_source_yields_reference_and_empty_value() {
1196        let secret = SecretBuilder::new()
1197            .env("API_KEY")
1198            .source(SecretSource::Env {
1199                var: "HOST_API_KEY".into(),
1200            })
1201            .allow("api.example.com")
1202            .build();
1203
1204        assert!(secret.value.is_empty());
1205        assert_eq!(
1206            secret.source,
1207            Some(SecretSource::Env {
1208                var: "HOST_API_KEY".into()
1209            })
1210        );
1211
1212        // Serialized durable form carries the reference, not a value.
1213        let json = serde_json::to_string(&secret).unwrap();
1214        assert!(json.contains("\"var\":\"HOST_API_KEY\""));
1215    }
1216
1217    #[test]
1218    #[should_panic(expected = "exactly one of .value() or .source()")]
1219    fn secret_builder_rejects_both_value_and_source() {
1220        let _ = SecretBuilder::new()
1221            .env("API_KEY")
1222            .value("inline")
1223            .source(SecretSource::Env {
1224                var: "HOST_API_KEY".into(),
1225            })
1226            .allow("api.example.com")
1227            .build();
1228    }
1229
1230    #[test]
1231    fn network_builder_rejects_invalid_secret_config() {
1232        let err = NetworkBuilder::new()
1233            .secret_entry(SecretEntry {
1234                env_var: "API=KEY".into(),
1235                value: Zeroizing::new("secret-value".into()),
1236                source: None,
1237                placeholder: "$MSB_API_KEY".into(),
1238                allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
1239                substitution: SecretSubstitution::default(),
1240                passthrough_hosts: Vec::new(),
1241                violation_action: None,
1242                require_tls_identity: true,
1243            })
1244            .build()
1245            .unwrap_err();
1246
1247        assert!(err.to_string().contains("env_var must not contain `=`"));
1248    }
1249
1250    #[test]
1251    fn rate_limiter_builder_sets_buckets_and_bursts() {
1252        use microsandbox_utils::size::SizeExt;
1253
1254        let cfg = NetworkBuilder::new()
1255            .rate_limiter(|r| {
1256                r.egress(|r| {
1257                    r.bandwidth(1.mib(), Duration::from_secs(1))
1258                        .bandwidth_burst(512.kib())
1259                        .ops(1_000, Duration::from_secs(1))
1260                        .ops_burst(500)
1261                })
1262                .ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
1263            })
1264            .build()
1265            .unwrap();
1266
1267        let rate_limiter = cfg.rate_limiter.unwrap();
1268        let egress = rate_limiter.egress.unwrap();
1269        let bandwidth = egress.bandwidth.unwrap();
1270        assert_eq!(bandwidth.size, 1024 * 1024);
1271        assert_eq!(bandwidth.refill_time_ms, 1000);
1272        assert_eq!(bandwidth.one_time_burst, 512 * 1024);
1273        let ops = egress.ops.unwrap();
1274        assert_eq!(ops.size, 1_000);
1275        assert_eq!(ops.refill_time_ms, 1000);
1276        assert_eq!(ops.one_time_burst, 500);
1277
1278        let ingress = rate_limiter.ingress.unwrap();
1279        assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
1280        assert!(ingress.ops.is_none());
1281    }
1282
1283    #[test]
1284    fn rate_limiters_default_to_unlimited() {
1285        let cfg = NetworkBuilder::new().build().unwrap();
1286        assert!(cfg.rate_limiter.is_none());
1287    }
1288
1289    #[test]
1290    fn rate_limiter_builder_rejects_empty_limiter() {
1291        let err = NetworkBuilder::new()
1292            .rate_limiter(|r| r.egress(|r| r))
1293            .build()
1294            .unwrap_err();
1295        assert_eq!(
1296            err.to_string(),
1297            "egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
1298        );
1299    }
1300
1301    #[test]
1302    fn network_rate_limiter_builder_rejects_missing_directions() {
1303        let err = NetworkBuilder::new()
1304            .rate_limiter(|r| r)
1305            .build()
1306            .unwrap_err();
1307        assert_eq!(
1308            err.to_string(),
1309            "rate limiter must configure at least one of egress or ingress"
1310        );
1311    }
1312
1313    #[test]
1314    fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
1315        let err = NetworkBuilder::new()
1316            .rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
1317            .build()
1318            .unwrap_err();
1319        assert_eq!(
1320            err.to_string(),
1321            "ingress rate limiter: bandwidth bucket: size must be greater than zero"
1322        );
1323
1324        let err = NetworkBuilder::new()
1325            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
1326            .build()
1327            .unwrap_err();
1328        assert_eq!(
1329            err.to_string(),
1330            "egress rate limiter: ops refill interval must be at least one millisecond"
1331        );
1332
1333        let err = NetworkBuilder::new()
1334            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
1335            .build()
1336            .unwrap_err();
1337        assert_eq!(
1338            err.to_string(),
1339            "egress rate limiter: ops refill interval must be a whole number of milliseconds"
1340        );
1341    }
1342
1343    #[test]
1344    fn rate_limiter_builder_rejects_burst_without_bucket() {
1345        use microsandbox_utils::size::SizeExt;
1346
1347        let err = NetworkBuilder::new()
1348            .rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
1349            .build()
1350            .unwrap_err();
1351        assert_eq!(
1352            err.to_string(),
1353            "egress rate limiter: bandwidth_burst requires the bandwidth bucket"
1354        );
1355
1356        let err = NetworkBuilder::new()
1357            .rate_limiter(|r| {
1358                r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
1359            })
1360            .build()
1361            .unwrap_err();
1362        assert_eq!(
1363            err.to_string(),
1364            "ingress rate limiter: ops_burst requires the ops bucket"
1365        );
1366    }
1367
1368    #[test]
1369    fn rate_limiter_builder_rejects_refill_interval_overflow() {
1370        let err = NetworkBuilder::new()
1371            .rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
1372            .build()
1373            .unwrap_err();
1374        assert_eq!(
1375            err.to_string(),
1376            "egress rate limiter: ops refill interval overflows u64 milliseconds"
1377        );
1378    }
1379}