use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
use std::num::NonZeroUsize;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use ipnetwork::{Ipv4Network, Ipv6Network};
use microsandbox_protocol::bootstrap::{
BootstrapEnvVar, BootstrapIpv4, BootstrapIpv6, BootstrapNetwork,
};
use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
use microsandbox_types::{
DeploymentProfile, NetworkRateLimitDirection, RateLimitConfigError, RateLimiterConfig,
};
use msb_krun::backends::net::NetBackend;
use crate::config::{ConnectionLimit, ResolvedNetworkConfig};
use crate::engine::tls::state::{TlsState, TlsStateError};
use crate::netstack::{
backend::SmoltcpBackend,
poll::{self, GatewayIps, PollLoopConfig},
shared::{DEFAULT_QUEUE_CAPACITY, SharedState},
};
use crate::policy::{NetworkPolicy, NetworkProfile};
use crate::secrets::handle::SecretsHandle;
const DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
const DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
pub struct SmoltcpNetwork {
config: ResolvedNetworkConfig,
platform_policy: Option<NetworkPolicy>,
shared: Arc<SharedState>,
backend: Option<SmoltcpBackend>,
poll_handle: Option<JoinHandle<()>>,
activation_gate: Option<Arc<NetworkActivationGate>>,
guest_mac: [u8; 6],
gateway_mac: [u8; 6],
mtu: u16,
guest_ipv4: Option<Ipv4Addr>,
gateway_ipv4: Option<Ipv4Addr>,
guest_ipv6: Option<Ipv6Addr>,
gateway_ipv6: Option<Ipv6Addr>,
tls_state: Option<Arc<TlsState>>,
secrets: SecretsHandle,
}
#[derive(Clone, Copy)]
struct HostRoutes {
ipv4: bool,
ipv6: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum NetworkInitError {
#[error("captured gateway MAC must be a nonzero unicast address distinct from the guest")]
InvalidGatewayMac,
#[error("IPv4 pool {pool} cannot assign network slot {slot}")]
Ipv4PoolCapacity {
pool: Ipv4Network,
slot: u16,
},
#[error("IPv6 pool {pool} cannot assign network slot {slot}")]
Ipv6PoolCapacity {
pool: Ipv6Network,
slot: u16,
},
#[error("TLS initialization failed: {0}")]
Tls(#[from] TlsStateError),
#[error("invalid {direction} rate limiter: {source}")]
InvalidRateLimit {
direction: NetworkRateLimitDirection,
#[source]
source: RateLimitConfigError,
},
#[error("invalid network rate limiter: at least one of egress or ingress is required")]
EmptyNetworkRateLimiter,
}
#[derive(Clone)]
pub struct TerminationHandle {
shared: Arc<SharedState>,
}
#[derive(Clone)]
pub struct MetricsHandle {
shared: Arc<SharedState>,
}
#[derive(Clone)]
pub struct NetworkActivationHandle {
gate: Arc<NetworkActivationGate>,
}
struct NetworkActivationGate {
active: Mutex<bool>,
changed: Condvar,
}
impl HostRoutes {
fn detect() -> Self {
Self {
ipv4: host_has_ipv4_route(),
ipv6: host_has_ipv6_route(),
}
}
}
impl SmoltcpNetwork {
pub fn default_gateway_mac(slot: u16) -> [u8; 6] {
derive_gateway_mac(slot)
}
pub fn with_captured_gateway_mac(mut self, mac: [u8; 6]) -> Result<Self, NetworkInitError> {
assert!(
self.poll_handle.is_none(),
"gateway identity must be set before network start"
);
if mac == [0; 6] || mac[0] & 1 != 0 || mac == self.guest_mac {
return Err(NetworkInitError::InvalidGatewayMac);
}
self.gateway_mac = mac;
Ok(self)
}
pub fn new(
config: ResolvedNetworkConfig,
slot: u16,
deployment_profile: DeploymentProfile,
) -> Result<Self, NetworkInitError> {
Self::build(config, slot, deployment_profile, HostRoutes::detect())
}
fn build(
mut config: ResolvedNetworkConfig,
slot: u16,
deployment_profile: DeploymentProfile,
host_routes: HostRoutes,
) -> Result<Self, NetworkInitError> {
enforce_deployment_profile(&mut config, deployment_profile);
let platform_policy = Self::platform_policy(deployment_profile);
let resolved_config = config;
let config = resolved_config.config();
let guest_mac = config
.interface
.mac
.unwrap_or_else(|| derive_guest_mac(slot));
let gateway_mac = derive_gateway_mac(slot);
let mtu = config.interface.mtu.unwrap_or(1500);
let guest_ipv4 = match config.interface.ipv4_address {
Some(address) => Some(address),
None if host_routes.ipv4 => Some(derive_guest_ipv4(
config
.interface
.ipv4_pool
.unwrap_or_else(default_guest_ipv4_pool),
slot,
)?),
None => None,
};
let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
let guest_ipv6 = match config.interface.ipv6_address {
Some(address) => Some(address),
None if host_routes.ipv6 => Some(derive_guest_ipv6(
config
.interface
.ipv6_pool
.unwrap_or_else(default_guest_ipv6_pool),
slot,
)?),
None => None,
};
let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
let shared = Arc::new(SharedState::new(DEFAULT_QUEUE_CAPACITY));
if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
}) {
return Err(NetworkInitError::EmptyNetworkRateLimiter);
}
config
.rate_limiter
.as_ref()
.and_then(|rate_limiter| rate_limiter.ingress.as_ref())
.map(RateLimiterConfig::validate)
.transpose()
.map_err(|source| NetworkInitError::InvalidRateLimit {
direction: NetworkRateLimitDirection::Ingress,
source,
})?;
config
.rate_limiter
.as_ref()
.and_then(|rate_limiter| rate_limiter.egress.as_ref())
.map(RateLimiterConfig::validate)
.transpose()
.map_err(|source| NetworkInitError::InvalidRateLimit {
direction: NetworkRateLimitDirection::Egress,
source,
})?;
let backend = SmoltcpBackend::new(shared.clone());
let secrets = SecretsHandle::new(config.secrets.clone());
let tls_state = if config.tls.enabled {
Some(Arc::new(TlsState::new(
config.tls.clone(),
secrets.clone(),
)?))
} else {
None
};
Ok(Self {
config: resolved_config,
platform_policy,
shared,
backend: Some(backend),
poll_handle: None,
activation_gate: None,
guest_mac,
gateway_mac,
mtu,
guest_ipv4,
gateway_ipv4,
guest_ipv6,
gateway_ipv6,
tls_state,
secrets,
})
}
pub fn defer_activation(&mut self) -> NetworkActivationHandle {
assert!(
self.poll_handle.is_none(),
"network activation can only be deferred before start"
);
let gate = Arc::new(NetworkActivationGate {
active: Mutex::new(false),
changed: Condvar::new(),
});
self.activation_gate = Some(Arc::clone(&gate));
NetworkActivationHandle { gate }
}
fn platform_policy(deployment_profile: DeploymentProfile) -> Option<NetworkPolicy> {
match deployment_profile {
DeploymentProfile::SingleTenant => None,
DeploymentProfile::MultiTenant => {
Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
}
}
}
fn gateway_ips(&self) -> GatewayIps {
GatewayIps {
ipv4: self.gateway_ipv4,
ipv6: self.gateway_ipv6,
}
}
pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
let shared = self.shared.clone();
let poll_config = PollLoopConfig {
gateway_mac: self.gateway_mac,
guest_mac: self.guest_mac,
gateway: self.gateway_ips(),
guest_ipv4: self.guest_ipv4,
guest_ipv6: self.guest_ipv6,
mtu: self.mtu as usize,
};
let config = self.config.config();
let network_policy = config.policy.clone();
let platform_policy = self.platform_policy.clone();
let dns_config = config.dns.clone();
let tls_state = self.tls_state.clone();
let published_ports = config.ports.clone();
let strict = config.strict;
let max_tcp_connections = config.max_tcp_connections.and_then(ConnectionLimit::cap);
let max_udp_connections = config.max_udp_connections;
let secrets = self.secrets.clone();
let activation_gate = self.activation_gate.take();
let outbound_proxy = self.config.outbound_proxy().cloned().map(Arc::new);
self.poll_handle = Some(
std::thread::Builder::new()
.name("smoltcp-poll".into())
.spawn(move || {
if let Some(gate) = activation_gate {
gate.wait();
}
poll::smoltcp_poll_loop(
shared,
poll_config,
network_policy,
platform_policy,
dns_config,
tls_state,
published_ports,
strict,
max_tcp_connections,
max_udp_connections,
tokio_handle,
secrets,
outbound_proxy,
);
})
.expect("failed to spawn smoltcp poll thread"),
);
}
pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
Box::new(self.backend.take().expect("backend already taken"))
}
pub fn guest_mac(&self) -> [u8; 6] {
self.guest_mac
}
pub fn guest_env_vars(&self) -> Vec<(String, String)> {
let mut vars = vec![
(
ENV_NET.into(),
format!(
"iface=eth0,mac={},mtu={}",
format_mac(self.guest_mac),
self.mtu,
),
),
(ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
];
if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
vars.push((
ENV_NET_IPV4.into(),
format!("addr={guest}/30,gw={gateway},dns={gateway}"),
));
}
if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
vars.push((
ENV_NET_IPV6.into(),
format!("addr={guest}/64,gw={gateway},dns={gateway}"),
));
}
for secret in &self.config.config().secrets.secrets {
vars.push((secret.env_var.clone(), secret.placeholder.clone()));
}
vars
}
pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
BootstrapNetwork {
interface: "eth0".to_string(),
mac: self.guest_mac,
mtu: self.mtu,
ipv4: self
.guest_ipv4
.zip(self.gateway_ipv4)
.map(|(address, gateway)| BootstrapIpv4 {
address,
prefix_len: 30,
gateway,
dns: Some(gateway),
}),
ipv6: self
.guest_ipv6
.zip(self.gateway_ipv6)
.map(|(address, gateway)| BootstrapIpv6 {
address,
prefix_len: 64,
gateway,
dns: Some(gateway),
}),
}
}
pub fn guest_host_alias(&self) -> &'static str {
crate::HOST_ALIAS
}
pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
self.config
.config()
.secrets
.secrets
.iter()
.map(|secret| BootstrapEnvVar {
key: secret.env_var.clone(),
value: secret.placeholder.clone(),
})
.collect()
}
pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
self.tls_state.as_ref().map(|s| s.ca_cert_pem())
}
pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
if !self.config.config().trust_host_cas {
return None;
}
crate::engine::tls::host_cas::collect_host_cas()
}
pub fn termination_handle(&self) -> TerminationHandle {
TerminationHandle {
shared: self.shared.clone(),
}
}
pub fn metrics_handle(&self) -> MetricsHandle {
MetricsHandle {
shared: self.shared.clone(),
}
}
pub fn secrets_handle(&self) -> SecretsHandle {
self.secrets.clone()
}
}
impl NetworkActivationHandle {
pub fn activate(&self) {
let mut active = self.gate.active.lock().unwrap();
if !*active {
*active = true;
self.gate.changed.notify_all();
}
}
}
impl NetworkActivationGate {
fn wait(&self) {
let mut active = self.active.lock().unwrap();
while !*active {
active = self.changed.wait(active).unwrap();
}
}
}
impl TerminationHandle {
pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
self.shared.set_termination_hook(hook);
}
}
impl MetricsHandle {
pub fn tx_bytes(&self) -> u64 {
self.shared.tx_bytes()
}
pub fn rx_bytes(&self) -> u64 {
self.shared.rx_bytes()
}
}
fn enforce_deployment_profile(config: &mut ResolvedNetworkConfig, profile: DeploymentProfile) {
if profile == DeploymentProfile::SingleTenant {
return;
}
config.clear_outbound_proxy();
let config = config.config_mut();
config
.max_tcp_connections
.get_or_insert(ConnectionLimit::Limited(
DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS,
));
config
.max_udp_connections
.get_or_insert(ConnectionLimit::Limited(
DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS,
));
let interface_overridden = config.interface.mac.is_some()
|| config.interface.mtu.is_some()
|| config.interface.ipv4_address.is_some()
|| config.interface.ipv4_pool.is_some()
|| config.interface.ipv6_address.is_some()
|| config.interface.ipv6_pool.is_some();
let had_published_ports = !config.ports.is_empty();
let had_custom_nameservers = !config.dns.nameservers.is_empty();
let disabled_rebind_protection = !config.dns.rebind_protection;
let trusted_host_cas = config.trust_host_cas;
let had_outbound_proxy = config.outbound_proxy.is_some();
config.interface = Default::default();
config.ports.clear();
config.dns.nameservers.clear();
config.dns.rebind_protection = true;
config.trust_host_cas = false;
if interface_overridden
|| had_published_ports
|| had_custom_nameservers
|| disabled_rebind_protection
|| trusted_host_cas
|| had_outbound_proxy
{
tracing::warn!(
interface_overridden,
had_published_ports,
had_custom_nameservers,
disabled_rebind_protection,
trusted_host_cas,
had_outbound_proxy,
"multi-tenant deployment profile overrode unsafe network configuration"
);
}
}
fn derive_guest_mac(slot: u16) -> [u8; 6] {
let s = slot.to_be_bytes();
[0x02, 0x6d, 0x73, s[0], s[1], 0x02]
}
fn derive_gateway_mac(slot: u16) -> [u8; 6] {
let s = slot.to_be_bytes();
[0x02, 0x6d, 0x73, s[0], s[1], 0x01]
}
fn derive_guest_ipv4(pool: Ipv4Network, slot: u16) -> Result<Ipv4Addr, NetworkInitError> {
let capacity = 30_u8
.checked_sub(pool.prefix())
.map(|host_bits| 1_u32 << host_bits)
.ok_or(NetworkInitError::Ipv4PoolCapacity { pool, slot })?;
if u32::from(slot) >= capacity {
return Err(NetworkInitError::Ipv4PoolCapacity { pool, slot });
}
let base = u32::from(pool.network());
let offset = u32::from(slot) * 4 + 2; Ok(Ipv4Addr::from(base + offset))
}
fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
Ipv4Addr::from(u32::from(guest) - 1)
}
fn default_guest_ipv4_pool() -> Ipv4Network {
Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
.expect("default IPv4 pool must be a valid network")
}
fn derive_guest_ipv6(pool: Ipv6Network, slot: u16) -> Result<Ipv6Addr, NetworkInitError> {
let capacity = 64_u8
.checked_sub(pool.prefix())
.map(|host_bits| 1_u128 << host_bits)
.ok_or(NetworkInitError::Ipv6PoolCapacity { pool, slot })?;
if u128::from(slot) >= capacity {
return Err(NetworkInitError::Ipv6PoolCapacity { pool, slot });
}
let base = u128::from(pool.network());
let offset = u128::from(slot) << 64;
Ok(Ipv6Addr::from(base + offset + 2))
}
fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
let segs = guest.segments();
Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
}
fn default_guest_ipv6_pool() -> Ipv6Network {
Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
.expect("default IPv6 pool must be a valid network")
}
fn format_mac(mac: [u8; 6]) -> String {
format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
)
}
fn host_has_ipv4_route() -> bool {
UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
.and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
.is_ok()
}
fn host_has_ipv6_route() -> bool {
UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
.and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{EnvNetworkSecretResolver, NetworkConfig, PortProtocol, PublishedPort};
use crate::dns::Nameserver;
fn resolved(config: NetworkConfig) -> ResolvedNetworkConfig {
config.resolve(&EnvNetworkSecretResolver).unwrap()
}
fn routes(ipv4: bool, ipv6: bool) -> HostRoutes {
HostRoutes { ipv4, ipv6 }
}
#[test]
fn captured_gateway_survives_new_slots_without_sharing_backends() {
let mut source_config = NetworkConfig::default();
source_config.interface.mac = Some([2, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]);
let source = SmoltcpNetwork::build(
resolved(source_config),
7,
DeploymentProfile::SingleTenant,
routes(true, true),
)
.unwrap();
let mut config = NetworkConfig::default();
config.interface.mac = Some(source.guest_mac);
config.interface.ipv4_address = source.guest_ipv4;
config.interface.ipv6_address = source.guest_ipv6;
for slot in [8, 400] {
let child = SmoltcpNetwork::build(
resolved(config.clone()),
slot,
DeploymentProfile::SingleTenant,
routes(true, true),
)
.unwrap()
.with_captured_gateway_mac(source.gateway_mac)
.unwrap();
assert_eq!(child.gateway_mac, source.gateway_mac);
assert_eq!(child.gateway_ipv4, source.gateway_ipv4);
assert_eq!(child.gateway_ipv6, source.gateway_ipv6);
assert_eq!(child.guest_mac, source.guest_mac);
assert!(!Arc::ptr_eq(&child.shared, &source.shared));
}
assert_eq!(source.gateway_mac, SmoltcpNetwork::default_gateway_mac(7));
}
#[test]
fn captured_gateway_rejects_unusable_ethernet_addresses() {
for mac in [[0; 6], [1, 2, 3, 4, 5, 6], derive_guest_mac(8)] {
let network = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
8,
DeploymentProfile::SingleTenant,
routes(true, true),
)
.unwrap();
assert!(matches!(
network.with_captured_gateway_mac(mac),
Err(NetworkInitError::InvalidGatewayMac)
));
}
}
#[test]
fn deferred_activation_blocks_until_released() {
let mut network = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(true, false),
)
.unwrap();
let handle = network.defer_activation();
let gate = Arc::clone(network.activation_gate.as_ref().unwrap());
let (released_tx, released_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
gate.wait();
released_tx.send(()).unwrap();
});
assert!(
released_rx
.recv_timeout(std::time::Duration::from_millis(25))
.is_err(),
"deferred network became active before explicit release"
);
handle.activate();
released_rx
.recv_timeout(std::time::Duration::from_secs(1))
.unwrap();
waiter.join().unwrap();
}
#[test]
fn derive_addresses_slot_0() {
assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
assert_eq!(
derive_guest_ipv4(default_guest_ipv4_pool(), 0).unwrap(),
Ipv4Addr::new(172, 16, 0, 2)
);
assert_eq!(
gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
Ipv4Addr::new(172, 16, 0, 1)
);
}
#[test]
fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
let mut config = NetworkConfig::default();
config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
config.interface.mtu = Some(9000);
config.ports.push(PublishedPort {
host_port: 8080,
guest_port: 80,
protocol: PortProtocol::Tcp,
host_bind: Ipv4Addr::UNSPECIFIED.into(),
});
config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
config.dns.rebind_protection = false;
config.trust_host_cas = true;
config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
address: "127.0.0.1:1080".parse().unwrap(),
credentials: None,
});
config.max_tcp_connections = Some(ConnectionLimit::from(257));
config.policy = NetworkPolicy::allow_all();
let mut resolved = resolved(config);
enforce_deployment_profile(&mut resolved, DeploymentProfile::MultiTenant);
let config = resolved.config();
assert!(config.interface.mac.is_none());
assert!(config.interface.mtu.is_none());
assert!(config.ports.is_empty());
assert!(config.dns.nameservers.is_empty());
assert!(config.dns.rebind_protection);
assert!(!config.trust_host_cas);
assert!(config.outbound_proxy.is_none());
assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::from(257)));
assert!(resolved.config().outbound_proxy.is_none());
assert!(resolved.outbound_proxy().is_none());
assert!(config.policy.default_egress.is_allow());
}
#[test]
fn deployment_profile_defaults_and_explicit_connection_limits() {
for profile in [
DeploymentProfile::SingleTenant,
DeploymentProfile::MultiTenant,
] {
for requested in [None, Some(0), Some(64), Some(4096)] {
let config: NetworkConfig =
serde_json::from_value(serde_json::json!({"max_tcp_connections": requested}))
.unwrap();
let mut config = resolved(config);
config = serde_json::from_value(serde_json::to_value(config).unwrap()).unwrap();
enforce_deployment_profile(&mut config, profile);
let expected = requested
.or(match profile {
DeploymentProfile::SingleTenant => None,
DeploymentProfile::MultiTenant => Some(1024),
})
.and_then(NonZeroUsize::new);
assert_eq!(
config
.config()
.max_tcp_connections
.and_then(ConnectionLimit::cap),
expected
);
enforce_deployment_profile(&mut config, profile);
assert_eq!(
config
.config()
.max_tcp_connections
.and_then(ConnectionLimit::cap),
expected
);
}
}
}
#[test]
fn deployment_profiles_resolve_udp_defaults_and_preserve_overrides() {
for profile in [
DeploymentProfile::SingleTenant,
DeploymentProfile::MultiTenant,
] {
for requested in [None, Some(0), Some(7), Some(4096)] {
let config: NetworkConfig = serde_json::from_value(serde_json::json!({
"max_udp_connections": requested
}))
.unwrap();
let mut config = resolved(config);
let expected = requested
.or(match profile {
DeploymentProfile::SingleTenant => None,
DeploymentProfile::MultiTenant => Some(1024),
})
.map(ConnectionLimit::from);
enforce_deployment_profile(&mut config, profile);
assert_eq!(config.config().max_udp_connections, expected);
enforce_deployment_profile(&mut config, profile);
assert_eq!(config.config().max_udp_connections, expected);
}
}
}
#[test]
fn single_tenant_profile_preserves_requested_network_controls() {
let mut config = NetworkConfig::default();
config.interface.mtu = Some(9000);
config.dns.rebind_protection = false;
config.trust_host_cas = true;
config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
address: "127.0.0.1:1080".parse().unwrap(),
credentials: None,
});
let mut resolved = resolved(config);
enforce_deployment_profile(&mut resolved, DeploymentProfile::SingleTenant);
let config = resolved.config();
assert_eq!(config.interface.mtu, Some(9000));
assert!(!config.dns.rebind_protection);
assert!(config.trust_host_cas);
assert!(config.outbound_proxy.is_some());
}
#[test]
fn derive_addresses_slot_1() {
assert_eq!(
derive_guest_ipv4(default_guest_ipv4_pool(), 1).unwrap(),
Ipv4Addr::new(172, 16, 0, 6)
);
assert_eq!(
gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
Ipv4Addr::new(172, 16, 0, 5)
);
}
#[test]
fn derive_addresses_max_slot() {
assert_eq!(
derive_guest_mac(u16::MAX),
[0x02, 0x6d, 0x73, 0xff, 0xff, 0x02]
);
assert_eq!(
derive_guest_ipv4(default_guest_ipv4_pool(), u16::MAX).unwrap(),
Ipv4Addr::new(172, 19, 255, 254)
);
assert_eq!(
derive_guest_ipv6(default_guest_ipv6_pool(), u16::MAX).unwrap(),
"fd42:6d73:62:ffff::2".parse::<Ipv6Addr>().unwrap()
);
}
#[test]
fn derive_addresses_custom_ipv4_pool() {
let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
assert_eq!(
derive_guest_ipv4(pool, 0).unwrap(),
Ipv4Addr::new(172, 31, 240, 2)
);
assert_eq!(
derive_guest_ipv4(pool, 63).unwrap(),
Ipv4Addr::new(172, 31, 240, 254)
);
}
#[test]
fn custom_ipv4_pool_capacity_is_a_typed_error() {
let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
assert!(matches!(
derive_guest_ipv4(pool, 64),
Err(NetworkInitError::Ipv4PoolCapacity { slot: 64, .. })
));
let pool = "172.31.240.0/31".parse::<Ipv4Network>().unwrap();
assert!(matches!(
derive_guest_ipv4(pool, 0),
Err(NetworkInitError::Ipv4PoolCapacity { slot: 0, .. })
));
}
#[test]
fn derive_ipv6_slot_0() {
assert_eq!(
derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap(),
"fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
);
assert_eq!(
gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap()),
"fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
);
}
#[test]
fn derive_addresses_custom_ipv6_pool() {
let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
assert_eq!(
derive_guest_ipv6(pool, 0).unwrap(),
"fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
);
assert_eq!(
derive_guest_ipv6(pool, 3).unwrap(),
"fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
);
}
#[test]
fn custom_ipv6_pool_capacity_is_a_typed_error() {
let pool = "fd7a:115c:a1e0:100::/62".parse::<Ipv6Network>().unwrap();
assert!(matches!(
derive_guest_ipv6(pool, 4),
Err(NetworkInitError::Ipv6PoolCapacity { slot: 4, .. })
));
let pool = "fd7a:115c:a1e0:100::/65".parse::<Ipv6Network>().unwrap();
assert!(matches!(
derive_guest_ipv6(pool, 0),
Err(NetworkInitError::Ipv6PoolCapacity { slot: 0, .. })
));
}
#[test]
fn format_mac_address() {
assert_eq!(
format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
"02:6d:73:00:00:01"
);
}
#[test]
fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(true, false),
)
.unwrap();
let vars = net.guest_env_vars();
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].0, ENV_NET);
assert!(vars[0].1.contains("iface=eth0"));
assert_eq!(vars[1].0, ENV_HOST_ALIAS);
assert_eq!(vars[1].1, crate::HOST_ALIAS);
assert_eq!(vars[2].0, ENV_NET_IPV4);
assert!(vars[2].1.contains("/30"));
}
#[test]
fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(true, true),
)
.unwrap();
let vars = net.guest_env_vars();
assert_eq!(vars.len(), 4);
assert_eq!(vars[0].0, ENV_NET);
assert_eq!(vars[1].0, ENV_HOST_ALIAS);
assert_eq!(vars[2].0, ENV_NET_IPV4);
assert_eq!(vars[3].0, ENV_NET_IPV6);
assert!(vars[3].1.contains("/64"));
}
#[test]
fn guest_env_vars_omit_ipv6_without_host_route() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(true, false),
)
.unwrap();
let vars = net.guest_env_vars();
assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
}
#[test]
fn guest_env_vars_omit_ipv4_without_host_route() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(false, true),
)
.unwrap();
let vars = net.guest_env_vars();
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].0, ENV_NET);
assert_eq!(vars[1].0, ENV_HOST_ALIAS);
assert_eq!(vars[2].0, ENV_NET_IPV6);
}
#[test]
fn explicit_ipv6_address_overrides_missing_host_v6_route() {
let mut config = NetworkConfig::default();
config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
let net = SmoltcpNetwork::build(
resolved(config),
0,
DeploymentProfile::SingleTenant,
routes(true, false),
)
.unwrap();
let vars = net.guest_env_vars();
let v6 = vars
.iter()
.find(|(k, _)| k == ENV_NET_IPV6)
.expect("explicit ipv6 should publish env var even without host route");
assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
}
#[test]
fn neither_family_active_emits_only_base_env_vars() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(false, false),
)
.unwrap();
let vars = net.guest_env_vars();
assert_eq!(vars.len(), 2);
assert_eq!(vars[0].0, ENV_NET);
assert_eq!(vars[1].0, ENV_HOST_ALIAS);
}
#[test]
fn guest_bootstrap_network_preserves_active_address_families() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
7,
DeploymentProfile::SingleTenant,
routes(true, true),
)
.unwrap();
let bootstrap = net.guest_bootstrap_network();
assert_eq!(bootstrap.interface, "eth0");
assert_eq!(bootstrap.mac, net.guest_mac());
assert_eq!(bootstrap.mtu, 1500);
assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
}
#[test]
fn guest_bootstrap_network_allows_no_active_address_family() {
let net = SmoltcpNetwork::build(
resolved(NetworkConfig::default()),
0,
DeploymentProfile::SingleTenant,
routes(false, false),
)
.unwrap();
let bootstrap = net.guest_bootstrap_network();
assert!(bootstrap.ipv4.is_none());
assert!(bootstrap.ipv6.is_none());
}
#[test]
fn large_connection_cap_does_not_preallocate_or_prevent_startup() {
for limit in [10000, usize::MAX] {
let mut config = NetworkConfig::default();
config.tls.enabled = false;
config.max_tcp_connections = Some(ConnectionLimit::from(limit));
let net = SmoltcpNetwork::build(
resolved(config),
0,
DeploymentProfile::MultiTenant,
routes(true, false),
);
assert!(net.is_ok(), "large explicit cap should allow startup");
}
}
#[test]
fn build_rejects_invalid_rate_limiter() {
let mut config = NetworkConfig {
rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
egress: None,
ingress: Some(microsandbox_types::RateLimiterConfig {
bandwidth: None,
ops: None,
}),
}),
..NetworkConfig::default()
};
config.tls.enabled = false;
let err = match SmoltcpNetwork::build(
resolved(config),
0,
DeploymentProfile::SingleTenant,
routes(true, false),
) {
Ok(_) => panic!("empty rate limiter should fail"),
Err(err) => err,
};
assert!(matches!(
err,
NetworkInitError::InvalidRateLimit {
direction: NetworkRateLimitDirection::Ingress,
source: RateLimitConfigError::EmptyLimiter,
}
));
}
}