use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
use std::sync::Arc;
use std::thread::JoinHandle;
use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
use msb_krun::backends::net::NetBackend;
use crate::backend::SmoltcpBackend;
use crate::config::NetworkConfig;
use crate::shared::{DEFAULT_QUEUE_CAPACITY, SharedState};
use crate::stack::{self, GatewayIps, PollLoopConfig};
use crate::tls::state::TlsState;
const MAX_SLOT: u64 = u16::MAX as u64;
pub struct SmoltcpNetwork {
config: NetworkConfig,
shared: Arc<SharedState>,
backend: Option<SmoltcpBackend>,
poll_handle: Option<JoinHandle<()>>,
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>>,
}
#[derive(Clone)]
pub struct TerminationHandle {
shared: Arc<SharedState>,
}
#[derive(Clone)]
pub struct MetricsHandle {
shared: Arc<SharedState>,
}
impl SmoltcpNetwork {
pub fn new(config: NetworkConfig, slot: u64) -> Self {
Self::new_with_routes(config, slot, host_has_ipv4_route(), host_has_ipv6_route())
}
fn new_with_routes(
config: NetworkConfig,
slot: u64,
host_has_ipv4: bool,
host_has_ipv6: bool,
) -> Self {
assert!(
slot <= MAX_SLOT,
"sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
);
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 = config
.interface
.ipv4_address
.or_else(|| host_has_ipv4.then(|| derive_guest_ipv4(slot)));
let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
let guest_ipv6 = config
.interface
.ipv6_address
.or_else(|| host_has_ipv6.then(|| derive_guest_ipv6(slot)));
let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
let queue_capacity = config
.max_connections
.unwrap_or(DEFAULT_QUEUE_CAPACITY)
.max(DEFAULT_QUEUE_CAPACITY);
let shared = Arc::new(SharedState::new(queue_capacity));
let backend = SmoltcpBackend::new(shared.clone());
let tls_state = if config.tls.enabled {
Some(Arc::new(TlsState::new(
config.tls.clone(),
config.secrets.clone(),
)))
} else {
None
};
Self {
config,
shared,
backend: Some(backend),
poll_handle: None,
guest_mac,
gateway_mac,
mtu,
guest_ipv4,
gateway_ipv4,
guest_ipv6,
gateway_ipv6,
tls_state,
}
}
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 network_policy = self.config.policy.clone();
let dns_config = self.config.dns.clone();
let tls_state = self.tls_state.clone();
let published_ports = self.config.ports.clone();
let max_connections = self.config.max_connections;
self.poll_handle = Some(
std::thread::Builder::new()
.name("smoltcp-poll".into())
.spawn(move || {
stack::smoltcp_poll_loop(
shared,
poll_config,
network_policy,
dns_config,
tls_state,
published_ports,
max_connections,
tokio_handle,
);
})
.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.secrets.secrets {
vars.push((secret.env_var.clone(), secret.placeholder.clone()));
}
vars
}
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.trust_host_cas {
return None;
}
crate::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(),
}
}
}
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 derive_guest_mac(slot: u64) -> [u8; 6] {
let s = slot.to_be_bytes();
[0x02, 0x6d, 0x73, s[6], s[7], 0x02]
}
fn derive_gateway_mac(slot: u64) -> [u8; 6] {
let s = slot.to_be_bytes();
[0x02, 0x6d, 0x73, s[6], s[7], 0x01]
}
fn derive_guest_ipv4(slot: u64) -> Ipv4Addr {
let base: u32 = u32::from(Ipv4Addr::new(100, 96, 0, 0));
let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
}
fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
Ipv4Addr::from(u32::from(guest) - 1)
}
fn derive_guest_ipv6(slot: u64) -> Ipv6Addr {
Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, slot as u16, 0, 0, 0, 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 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::*;
#[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(0), Ipv4Addr::new(100, 96, 0, 2));
assert_eq!(
gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 2)),
Ipv4Addr::new(100, 96, 0, 1)
);
}
#[test]
fn derive_addresses_slot_1() {
assert_eq!(derive_guest_ipv4(1), Ipv4Addr::new(100, 96, 0, 6));
assert_eq!(
gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 6)),
Ipv4Addr::new(100, 96, 0, 5)
);
}
#[test]
fn derive_ipv6_slot_0() {
assert_eq!(
derive_guest_ipv6(0),
"fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
);
assert_eq!(
gateway_from_guest_ipv6(derive_guest_ipv6(0)),
"fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
);
}
#[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::new_with_routes(NetworkConfig::default(), 0, true, false);
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::new_with_routes(NetworkConfig::default(), 0, true, true);
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::new_with_routes(NetworkConfig::default(), 0, true, false);
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::new_with_routes(NetworkConfig::default(), 0, false, true);
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::new_with_routes(config, 0, true, false);
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::new_with_routes(NetworkConfig::default(), 0, false, false);
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);
}
}