use std::net::IpAddr;
use std::path::PathBuf;
use std::time::Duration;
use ipnetwork::{Ipv4Network, Ipv6Network};
use microsandbox_types::{
NetworkRateLimitDirection, NetworkRateLimiterConfig, RateLimiterConfig, ScopedUpstreamCaCert,
ScopedVerifyUpstream, TlsConfig, TokenBucketConfig,
};
use microsandbox_utils::size::Bytes;
use zeroize::Zeroizing;
use crate::config::{
DnsConfig, InterfaceOverrides, MAX_NETWORK_CONNECTIONS, NetworkConfig, PortProtocol,
PublishedPort,
};
use crate::dns::Nameserver;
use crate::policy::{BuildError, NetworkPolicy};
use crate::secrets::config::{
HostPattern, SecretEntry, SecretInjection, SecretSource, ViolationAction,
};
#[derive(Clone)]
pub struct NetworkBuilder {
config: NetworkConfig,
errors: Vec<BuildError>,
}
pub struct DnsBuilder {
config: DnsConfig,
}
pub struct TlsBuilder {
config: TlsConfig,
}
pub struct SecretBuilder {
env_var: Option<String>,
value: Option<String>,
source: Option<SecretSource>,
placeholder: Option<String>,
allowed_hosts: Vec<HostPattern>,
injection: SecretInjection,
on_violation: Option<ViolationAction>,
require_tls_identity: bool,
}
#[derive(Default)]
pub struct ViolationActionBuilder {
action: ViolationAction,
}
#[derive(Default)]
pub struct NetworkRateLimiterBuilder {
config: NetworkRateLimiterConfig,
errors: Vec<BuildError>,
}
pub struct RateLimiterBuilder {
direction: NetworkRateLimitDirection,
bandwidth: Option<TokenBucketConfig>,
ops: Option<TokenBucketConfig>,
bandwidth_burst: Option<u64>,
ops_burst: Option<u64>,
refill_error: Option<(&'static str, RefillTimeError)>,
}
#[derive(Clone, Copy, Debug)]
enum RefillTimeError {
TooShort,
Precision,
TooLong,
}
impl NetworkBuilder {
pub fn new() -> Self {
Self {
config: NetworkConfig::default(),
errors: Vec::new(),
}
}
pub fn from_config(config: NetworkConfig) -> Self {
Self {
config,
errors: Vec::new(),
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn port(self, host_port: u16, guest_port: u16) -> Self {
self.port_bind(
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
host_port,
guest_port,
)
}
pub fn port_udp(self, host_port: u16, guest_port: u16) -> Self {
self.port_udp_bind(
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
host_port,
guest_port,
)
}
pub fn port_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
self.add_port(host_bind, host_port, guest_port, PortProtocol::Tcp)
}
pub fn port_udp_bind(self, host_bind: IpAddr, host_port: u16, guest_port: u16) -> Self {
self.add_port(host_bind, host_port, guest_port, PortProtocol::Udp)
}
fn add_port(
mut self,
host_bind: IpAddr,
host_port: u16,
guest_port: u16,
protocol: PortProtocol,
) -> Self {
self.config.ports.push(PublishedPort {
host_port,
guest_port,
protocol,
host_bind,
});
self
}
pub fn policy(mut self, policy: NetworkPolicy) -> Self {
self.config.policy = policy;
self
}
pub fn dns(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
self.config.dns = f(DnsBuilder::new()).build();
self
}
#[doc(hidden)]
pub fn dns_overlay(mut self, f: impl FnOnce(DnsBuilder) -> DnsBuilder) -> Self {
self.config.dns = f(DnsBuilder::from_config(self.config.dns)).build();
self
}
pub fn tls(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
self.config.tls = f(TlsBuilder::new()).build();
self
}
#[doc(hidden)]
pub fn tls_overlay(mut self, f: impl FnOnce(TlsBuilder) -> TlsBuilder) -> Self {
self.config.tls = f(TlsBuilder::from_config(self.config.tls)).build();
self
}
pub fn secret(self, f: impl FnOnce(SecretBuilder) -> SecretBuilder) -> Self {
self.secret_entry(f(SecretBuilder::new()).build())
}
pub fn secret_entry(mut self, entry: SecretEntry) -> Self {
self.config.secrets.secrets.push(entry);
self
}
pub fn secret_env(
mut self,
env_var: impl Into<String>,
value: impl Into<String>,
placeholder: impl Into<String>,
allowed_host: impl Into<String>,
) -> Self {
self.config.secrets.secrets.push(SecretEntry {
env_var: env_var.into(),
value: Zeroizing::new(value.into()),
source: None,
placeholder: placeholder.into(),
allowed_hosts: vec![HostPattern::Exact(allowed_host.into())],
injection: SecretInjection::default(),
on_violation: None,
require_tls_identity: true,
});
self
}
pub fn on_secret_violation(
mut self,
f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
) -> Self {
self.config.secrets.on_violation = f(ViolationActionBuilder::default()).build();
self
}
pub fn max_connections(mut self, max: usize) -> Self {
if max > MAX_NETWORK_CONNECTIONS {
self.errors.push(BuildError::MaxConnectionsExceeded {
configured: max,
limit: MAX_NETWORK_CONNECTIONS,
});
} else {
self.config.max_connections = Some(max);
}
self
}
pub fn interface(mut self, overrides: InterfaceOverrides) -> Self {
self.config.interface = overrides;
self
}
pub fn ipv4_pool(mut self, pool: Ipv4Network) -> Self {
if pool.prefix() > 30 {
self.errors.push(BuildError::InvalidIpv4Pool {
raw: pool.to_string(),
});
} else {
self.config.interface.ipv4_pool = Some(pool);
}
self
}
pub fn ipv6_pool(mut self, pool: Ipv6Network) -> Self {
if pool.prefix() > 64 {
self.errors.push(BuildError::InvalidIpv6Pool {
raw: pool.to_string(),
});
} else {
self.config.interface.ipv6_pool = Some(pool);
}
self
}
pub fn trust_host_cas(mut self, enabled: bool) -> Self {
self.config.trust_host_cas = enabled;
self
}
pub fn rate_limiter(
mut self,
f: impl FnOnce(NetworkRateLimiterBuilder) -> NetworkRateLimiterBuilder,
) -> Self {
match f(NetworkRateLimiterBuilder::new()).build() {
Ok(limiter) => self.config.rate_limiter = Some(limiter),
Err(err) => self.errors.push(err),
}
self
}
pub fn build(mut self) -> Result<NetworkConfig, BuildError> {
if let Some(err) = self.errors.drain(..).next() {
return Err(err);
}
if let Some(max) = self.config.max_connections
&& max > MAX_NETWORK_CONNECTIONS
{
return Err(BuildError::MaxConnectionsExceeded {
configured: max,
limit: MAX_NETWORK_CONNECTIONS,
});
}
if self.config.tls.enabled
&& (self.config.tls.intercept_ca.cert_path.is_some()
!= self.config.tls.intercept_ca.key_path.is_some())
{
return Err(BuildError::IncompleteInterceptCaConfig);
}
self.config.secrets.validate()?;
Ok(self.config)
}
}
impl DnsBuilder {
pub fn new() -> Self {
Self {
config: DnsConfig::default(),
}
}
fn from_config(config: DnsConfig) -> Self {
Self { config }
}
pub fn rebind_protection(mut self, enabled: bool) -> Self {
self.config.rebind_protection = enabled;
self
}
pub fn nameservers<I>(mut self, nameservers: I) -> Self
where
I: IntoIterator,
I::Item: Into<Nameserver>,
{
self.config.nameservers = nameservers.into_iter().map(Into::into).collect();
self
}
pub fn query_timeout_ms(mut self, ms: u64) -> Self {
self.config.query_timeout_ms = ms;
self
}
pub fn build(self) -> DnsConfig {
self.config
}
}
impl Default for DnsBuilder {
fn default() -> Self {
Self::new()
}
}
impl TlsBuilder {
pub fn new() -> Self {
Self {
config: TlsConfig {
enabled: true,
..TlsConfig::default()
},
}
}
fn from_config(config: TlsConfig) -> Self {
Self { config }
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.config.enabled = enabled;
self
}
pub fn bypass(mut self, pattern: impl Into<String>) -> Self {
self.config.bypass.push(pattern.into());
self
}
pub fn verify_upstream(mut self, verify: bool) -> Self {
self.config.verify_upstream = verify;
self
}
pub fn verify_upstream_for(mut self, pattern: impl Into<String>, verify: bool) -> Self {
self.config
.scoped_verify_upstream
.push(ScopedVerifyUpstream {
pattern: pattern.into(),
verify,
});
self
}
pub fn intercepted_ports(mut self, ports: Vec<u16>) -> Self {
self.config.intercepted_ports = ports;
self
}
pub fn block_quic(mut self, block: bool) -> Self {
self.config.block_quic_on_intercept = block;
self
}
pub fn upstream_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
self.config.upstream_ca_cert.push(path.into());
self
}
pub fn upstream_ca_cert_for(
mut self,
pattern: impl Into<String>,
path: impl Into<PathBuf>,
) -> Self {
self.config
.scoped_upstream_ca_cert
.push(ScopedUpstreamCaCert {
pattern: pattern.into(),
path: path.into(),
});
self
}
pub fn intercept_ca_cert(mut self, path: impl Into<PathBuf>) -> Self {
self.config.intercept_ca.cert_path = Some(path.into());
self
}
pub fn intercept_ca_key(mut self, path: impl Into<PathBuf>) -> Self {
self.config.intercept_ca.key_path = Some(path.into());
self
}
pub fn build(self) -> TlsConfig {
self.config
}
}
impl SecretBuilder {
pub fn new() -> Self {
Self {
env_var: None,
value: None,
source: None,
placeholder: None,
allowed_hosts: Vec::new(),
injection: SecretInjection::default(),
on_violation: None,
require_tls_identity: true,
}
}
pub fn env(mut self, var: impl Into<String>) -> Self {
self.env_var = Some(var.into());
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = Some(value.into());
self
}
pub fn source(mut self, source: SecretSource) -> Self {
self.source = Some(source);
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = Some(placeholder.into());
self
}
pub fn allow_host(mut self, host: impl Into<String>) -> Self {
self.allowed_hosts.push(HostPattern::Exact(host.into()));
self
}
pub fn allow_host_pattern(mut self, pattern: impl Into<String>) -> Self {
self.allowed_hosts
.push(HostPattern::Wildcard(pattern.into()));
self
}
pub fn allow_any_host_dangerous(mut self, i_understand_the_risk: bool) -> Self {
if i_understand_the_risk {
self.allowed_hosts.push(HostPattern::Any);
}
self
}
pub fn on_violation(
mut self,
f: impl FnOnce(ViolationActionBuilder) -> ViolationActionBuilder,
) -> Self {
self.on_violation = Some(f(ViolationActionBuilder::default()).build());
self
}
pub fn require_tls_identity(mut self, enabled: bool) -> Self {
self.require_tls_identity = enabled;
self
}
pub fn inject_headers(mut self, enabled: bool) -> Self {
self.injection.headers = enabled;
self
}
pub fn inject_basic_auth(mut self, enabled: bool) -> Self {
self.injection.basic_auth = enabled;
self
}
pub fn inject_query(mut self, enabled: bool) -> Self {
self.injection.query_params = enabled;
self
}
pub fn inject_body(mut self, enabled: bool) -> Self {
self.injection.body = enabled;
self
}
pub fn build(self) -> SecretEntry {
let env_var = self.env_var.expect("SecretBuilder: .env() is required");
assert!(
self.value.is_some() ^ self.source.is_some(),
"SecretBuilder: exactly one of .value() or .source() is required"
);
assert!(
!self.allowed_hosts.is_empty(),
"SecretBuilder: at least one allowed host is required; use .allow_any_host_dangerous(true) for an explicit any-host secret"
);
let placeholder = self
.placeholder
.unwrap_or_else(|| microsandbox_utils::secret::default_placeholder(&env_var));
SecretEntry {
env_var,
value: Zeroizing::new(self.value.unwrap_or_default()),
source: self.source,
placeholder,
allowed_hosts: self.allowed_hosts,
injection: self.injection,
on_violation: self.on_violation,
require_tls_identity: self.require_tls_identity,
}
}
}
impl NetworkRateLimiterBuilder {
fn new() -> Self {
Self::default()
}
pub fn egress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Egress)).build() {
Ok(limiter) => self.config.egress = Some(limiter),
Err(err) => self.errors.push(err),
}
self
}
pub fn ingress(mut self, f: impl FnOnce(RateLimiterBuilder) -> RateLimiterBuilder) -> Self {
match f(RateLimiterBuilder::new(NetworkRateLimitDirection::Ingress)).build() {
Ok(limiter) => self.config.ingress = Some(limiter),
Err(err) => self.errors.push(err),
}
self
}
pub fn build(mut self) -> Result<NetworkRateLimiterConfig, BuildError> {
if let Some(error) = self.errors.drain(..).next() {
return Err(error);
}
if self.config.egress.is_none() && self.config.ingress.is_none() {
return Err(BuildError::EmptyNetworkRateLimiter);
}
Ok(self.config)
}
}
impl RateLimiterBuilder {
fn new(direction: NetworkRateLimitDirection) -> Self {
Self {
direction,
bandwidth: None,
ops: None,
bandwidth_burst: None,
ops_burst: None,
refill_error: None,
}
}
pub fn bandwidth(mut self, size: impl Into<Bytes>, refill_time: Duration) -> Self {
match refill_time_ms(refill_time) {
Ok(refill_time_ms) => {
self.bandwidth = Some(TokenBucketConfig {
size: size.into().as_u64(),
refill_time_ms,
one_time_burst: 0,
});
}
Err(error) => {
self.refill_error.get_or_insert(("bandwidth", error));
}
}
self
}
pub fn bandwidth_burst(mut self, burst: impl Into<Bytes>) -> Self {
self.bandwidth_burst = Some(burst.into().as_u64());
self
}
pub fn ops(mut self, count: u64, refill_time: Duration) -> Self {
match refill_time_ms(refill_time) {
Ok(refill_time_ms) => {
self.ops = Some(TokenBucketConfig {
size: count,
refill_time_ms,
one_time_burst: 0,
});
}
Err(error) => {
self.refill_error.get_or_insert(("ops", error));
}
}
self
}
pub fn ops_burst(mut self, count: u64) -> Self {
self.ops_burst = Some(count);
self
}
pub fn build(self) -> Result<RateLimiterConfig, BuildError> {
let direction = self.direction;
if let Some((bucket, error)) = self.refill_error {
return Err(match error {
RefillTimeError::TooShort => {
BuildError::RateLimitRefillTooShort { direction, bucket }
}
RefillTimeError::Precision => {
BuildError::RateLimitRefillPrecision { direction, bucket }
}
RefillTimeError::TooLong => {
BuildError::RateLimitRefillTooLong { direction, bucket }
}
});
}
let mut config = RateLimiterConfig {
bandwidth: self.bandwidth,
ops: self.ops,
};
if let Some(burst) = self.bandwidth_burst {
let Some(bandwidth) = &mut config.bandwidth else {
return Err(BuildError::RateLimitBurstWithoutBucket {
direction,
bucket: "bandwidth",
});
};
bandwidth.one_time_burst = burst;
}
if let Some(burst) = self.ops_burst {
let Some(ops) = &mut config.ops else {
return Err(BuildError::RateLimitBurstWithoutBucket {
direction,
bucket: "ops",
});
};
ops.one_time_burst = burst;
}
config
.validate()
.map_err(|source| BuildError::InvalidRateLimitConfig { direction, source })?;
Ok(config)
}
}
impl ViolationActionBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn from_action(action: ViolationAction) -> Self {
action.into()
}
pub fn block(mut self) -> Self {
self.action = ViolationAction::Block;
self
}
pub fn block_and_log(mut self) -> Self {
self.action = ViolationAction::BlockAndLog;
self
}
pub fn block_and_terminate(mut self) -> Self {
self.action = ViolationAction::BlockAndTerminate;
self
}
pub fn passthrough_host(mut self, host: impl Into<String>) -> Self {
self.push_passthrough_host(HostPattern::Exact(host.into()));
self
}
pub fn passthrough_host_pattern(mut self, pattern: impl Into<String>) -> Self {
self.push_passthrough_host(HostPattern::Wildcard(pattern.into()));
self
}
pub fn passthrough_all_hosts(mut self, i_understand_the_risk: bool) -> Self {
if i_understand_the_risk {
self.push_passthrough_host(HostPattern::Any);
}
self
}
fn push_passthrough_host(&mut self, host: HostPattern) {
match self.action {
ViolationAction::Passthrough(ref mut hosts) => hosts.push(host),
_ => self.action = ViolationAction::Passthrough(vec![host]),
}
}
pub fn build(self) -> ViolationAction {
self.action
}
}
fn refill_time_ms(refill_time: Duration) -> Result<u64, RefillTimeError> {
if refill_time < Duration::from_millis(1) {
return Err(RefillTimeError::TooShort);
}
let refill_time_ms =
u64::try_from(refill_time.as_millis()).map_err(|_| RefillTimeError::TooLong)?;
if !refill_time.subsec_nanos().is_multiple_of(1_000_000) {
return Err(RefillTimeError::Precision);
}
Ok(refill_time_ms)
}
impl Default for NetworkBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for TlsBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for SecretBuilder {
fn default() -> Self {
Self::new()
}
}
impl From<ViolationAction> for ViolationActionBuilder {
fn from(action: ViolationAction) -> Self {
Self { action }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn network_builder_happy_path_returns_config() {
let cfg = NetworkBuilder::new()
.dns(|d| d.rebind_protection(false))
.build()
.unwrap();
assert!(!cfg.dns.rebind_protection);
}
#[test]
fn network_builder_rejects_excessive_max_connections() {
let err = NetworkBuilder::new()
.max_connections(MAX_NETWORK_CONNECTIONS + 1)
.build()
.unwrap_err();
assert!(matches!(
err,
BuildError::MaxConnectionsExceeded {
configured,
limit: MAX_NETWORK_CONNECTIONS
} if configured == MAX_NETWORK_CONNECTIONS + 1
));
}
#[test]
fn network_builder_rejects_incomplete_intercept_ca_config() {
let err = NetworkBuilder::new()
.tls(|t| t.intercept_ca_cert("/tmp/ca.crt"))
.build()
.unwrap_err();
assert!(matches!(err, BuildError::IncompleteInterceptCaConfig));
}
#[test]
fn port_bind_sets_host_bind() {
let bind = "0.0.0.0".parse().unwrap();
let cfg = NetworkBuilder::new()
.port_bind(bind, 8080, 80)
.port_udp_bind(bind, 5353, 53)
.build()
.unwrap();
assert_eq!(cfg.ports[0].host_bind, bind);
assert_eq!(cfg.ports[0].host_port, 8080);
assert_eq!(cfg.ports[0].guest_port, 80);
assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
assert_eq!(cfg.ports[1].host_bind, bind);
assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
}
#[test]
fn port_helpers_default_to_loopback() {
let cfg = NetworkBuilder::new()
.port(8080, 80)
.port_udp(5353, 53)
.build()
.unwrap();
assert_eq!(
cfg.ports[0].host_bind,
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
);
assert_eq!(cfg.ports[0].protocol, PortProtocol::Tcp);
assert_eq!(
cfg.ports[1].host_bind,
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
);
assert_eq!(cfg.ports[1].protocol, PortProtocol::Udp);
}
#[test]
fn network_builder_sets_global_passthrough_action() {
let cfg = NetworkBuilder::new()
.on_secret_violation(|v| {
v.passthrough_host("api.anthropic.com")
.passthrough_host_pattern("*.anthropic.com")
})
.build()
.unwrap();
assert_eq!(
cfg.secrets.on_violation,
ViolationAction::Passthrough(vec![
HostPattern::Exact("api.anthropic.com".into()),
HostPattern::Wildcard("*.anthropic.com".into()),
])
);
}
#[test]
fn secret_builder_sets_violation_action() {
let secret = SecretBuilder::new()
.env("TOKEN")
.value("secret-value")
.allow_host("api.github.com")
.on_violation(|v| {
v.passthrough_host("api.anthropic.com")
.passthrough_host_pattern("*.anthropic.com")
})
.build();
assert_eq!(
secret.on_violation,
Some(ViolationAction::Passthrough(vec![
HostPattern::Exact("api.anthropic.com".into()),
HostPattern::Wildcard("*.anthropic.com".into()),
])),
);
}
#[test]
#[should_panic(expected = "SecretBuilder: at least one allowed host is required")]
fn secret_builder_rejects_empty_allowed_hosts() {
let _ = SecretBuilder::new()
.env("TOKEN")
.value("secret-value")
.build();
}
#[test]
fn secret_builder_source_yields_reference_and_empty_value() {
let secret = SecretBuilder::new()
.env("API_KEY")
.source(SecretSource::Env {
var: "HOST_API_KEY".into(),
})
.allow_host("api.example.com")
.build();
assert!(secret.value.is_empty());
assert_eq!(
secret.source,
Some(SecretSource::Env {
var: "HOST_API_KEY".into()
})
);
let json = serde_json::to_string(&secret).unwrap();
assert!(json.contains("\"var\":\"HOST_API_KEY\""));
}
#[test]
#[should_panic(expected = "exactly one of .value() or .source()")]
fn secret_builder_rejects_both_value_and_source() {
let _ = SecretBuilder::new()
.env("API_KEY")
.value("inline")
.source(SecretSource::Env {
var: "HOST_API_KEY".into(),
})
.allow_host("api.example.com")
.build();
}
#[test]
fn network_builder_rejects_invalid_secret_config() {
let err = NetworkBuilder::new()
.secret_entry(SecretEntry {
env_var: "API=KEY".into(),
value: Zeroizing::new("secret-value".into()),
source: None,
placeholder: "$MSB_API_KEY".into(),
allowed_hosts: vec![HostPattern::Exact("api.example.com".into())],
injection: SecretInjection::default(),
on_violation: None,
require_tls_identity: true,
})
.build()
.unwrap_err();
assert!(err.to_string().contains("env_var must not contain `=`"));
}
#[test]
fn violation_action_builder_blocking_call_replaces_passthrough_policy() {
let action = ViolationActionBuilder::default()
.passthrough_host("google.com")
.block_and_terminate()
.passthrough_host("facebook.com")
.build();
assert_eq!(
action,
ViolationAction::Passthrough(vec![HostPattern::Exact("facebook.com".into())])
);
}
#[test]
fn rate_limiter_builder_sets_buckets_and_bursts() {
use microsandbox_utils::size::SizeExt;
let cfg = NetworkBuilder::new()
.rate_limiter(|r| {
r.egress(|r| {
r.bandwidth(1.mib(), Duration::from_secs(1))
.bandwidth_burst(512.kib())
.ops(1_000, Duration::from_secs(1))
.ops_burst(500)
})
.ingress(|r| r.bandwidth(2.mib(), Duration::from_millis(500)))
})
.build()
.unwrap();
let rate_limiter = cfg.rate_limiter.unwrap();
let egress = rate_limiter.egress.unwrap();
let bandwidth = egress.bandwidth.unwrap();
assert_eq!(bandwidth.size, 1024 * 1024);
assert_eq!(bandwidth.refill_time_ms, 1000);
assert_eq!(bandwidth.one_time_burst, 512 * 1024);
let ops = egress.ops.unwrap();
assert_eq!(ops.size, 1_000);
assert_eq!(ops.refill_time_ms, 1000);
assert_eq!(ops.one_time_burst, 500);
let ingress = rate_limiter.ingress.unwrap();
assert_eq!(ingress.bandwidth.unwrap().refill_time_ms, 500);
assert!(ingress.ops.is_none());
}
#[test]
fn rate_limiters_default_to_unlimited() {
let cfg = NetworkBuilder::new().build().unwrap();
assert!(cfg.rate_limiter.is_none());
}
#[test]
fn rate_limiter_builder_rejects_empty_limiter() {
let err = NetworkBuilder::new()
.rate_limiter(|r| r.egress(|r| r))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"egress rate limiter: rate limiter must configure at least one of bandwidth or ops"
);
}
#[test]
fn network_rate_limiter_builder_rejects_missing_directions() {
let err = NetworkBuilder::new()
.rate_limiter(|r| r)
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"rate limiter must configure at least one of egress or ingress"
);
}
#[test]
fn rate_limiter_builder_rejects_zero_size_and_unrepresentable_refill() {
let err = NetworkBuilder::new()
.rate_limiter(|r| r.ingress(|r| r.bandwidth(0u64, Duration::from_secs(1))))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"ingress rate limiter: bandwidth bucket: size must be greater than zero"
);
let err = NetworkBuilder::new()
.rate_limiter(|r| r.egress(|r| r.ops(10, Duration::ZERO)))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"egress rate limiter: ops refill interval must be at least one millisecond"
);
let err = NetworkBuilder::new()
.rate_limiter(|r| r.egress(|r| r.ops(10, Duration::from_micros(1_500))))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"egress rate limiter: ops refill interval must be a whole number of milliseconds"
);
}
#[test]
fn rate_limiter_builder_rejects_burst_without_bucket() {
use microsandbox_utils::size::SizeExt;
let err = NetworkBuilder::new()
.rate_limiter(|r| r.egress(|r| r.bandwidth_burst(512.kib())))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"egress rate limiter: bandwidth_burst requires the bandwidth bucket"
);
let err = NetworkBuilder::new()
.rate_limiter(|r| {
r.ingress(|r| r.bandwidth(1.mib(), Duration::from_secs(1)).ops_burst(5))
})
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"ingress rate limiter: ops_burst requires the ops bucket"
);
}
#[test]
fn rate_limiter_builder_rejects_refill_interval_overflow() {
let err = NetworkBuilder::new()
.rate_limiter(|r| r.egress(|r| r.ops(10, Duration::MAX)))
.build()
.unwrap_err();
assert_eq!(
err.to_string(),
"egress rate limiter: ops refill interval overflows u64 milliseconds"
);
}
#[test]
fn violation_action_builder_accumulates_passthrough_hosts() {
let action = ViolationActionBuilder::default()
.block()
.passthrough_host("google.com")
.passthrough_host("facebook.com")
.build();
assert_eq!(
action,
ViolationAction::Passthrough(vec![
HostPattern::Exact("google.com".into()),
HostPattern::Exact("facebook.com".into()),
]),
);
}
}