1use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
9use std::sync::Arc;
10use std::thread::JoinHandle;
11
12use ipnetwork::{Ipv4Network, Ipv6Network};
13use microsandbox_protocol::bootstrap::{
14 BootstrapEnvVar, BootstrapIpv4, BootstrapIpv6, BootstrapNetwork,
15};
16use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
17use microsandbox_types::{
18 DeploymentProfile, NetworkRateLimitDirection, RateLimitConfigError, RateLimiterConfig,
19};
20use msb_krun::backends::net::NetBackend;
21
22use crate::config::{MAX_NETWORK_CONNECTIONS, ResolvedNetworkConfig};
23use crate::netstack::{
24 backend::SmoltcpBackend,
25 poll::{self, GatewayIps, PollLoopConfig},
26 shared::{DEFAULT_QUEUE_CAPACITY, SharedState},
27};
28use crate::policy::{NetworkPolicy, NetworkProfile};
29use crate::secrets::handle::SecretsHandle;
30use crate::tls::state::{TlsState, TlsStateError};
31
32const MULTI_TENANT_MAX_CONNECTIONS: usize = 256;
41
42pub struct SmoltcpNetwork {
53 config: ResolvedNetworkConfig,
54 platform_policy: Option<NetworkPolicy>,
57 shared: Arc<SharedState>,
58 backend: Option<SmoltcpBackend>,
59 poll_handle: Option<JoinHandle<()>>,
60
61 guest_mac: [u8; 6],
63 gateway_mac: [u8; 6],
64 mtu: u16,
65 guest_ipv4: Option<Ipv4Addr>,
68 gateway_ipv4: Option<Ipv4Addr>,
69 guest_ipv6: Option<Ipv6Addr>,
70 gateway_ipv6: Option<Ipv6Addr>,
71
72 tls_state: Option<Arc<TlsState>>,
74
75 secrets: SecretsHandle,
77}
78
79#[derive(Clone, Copy)]
80struct HostRoutes {
81 ipv4: bool,
82 ipv6: bool,
83}
84
85#[derive(Debug, thiserror::Error)]
87pub enum NetworkInitError {
88 #[error("max_connections {configured} exceeds hard limit {limit}")]
90 MaxConnectionsExceeded {
91 configured: usize,
93 limit: usize,
95 },
96
97 #[error("IPv4 pool {pool} cannot assign network slot {slot}")]
99 Ipv4PoolCapacity {
100 pool: Ipv4Network,
102 slot: u16,
104 },
105
106 #[error("IPv6 pool {pool} cannot assign network slot {slot}")]
108 Ipv6PoolCapacity {
109 pool: Ipv6Network,
111 slot: u16,
113 },
114
115 #[error("TLS initialization failed: {0}")]
117 Tls(#[from] TlsStateError),
118
119 #[error("invalid {direction} rate limiter: {source}")]
121 InvalidRateLimit {
122 direction: NetworkRateLimitDirection,
124 #[source]
126 source: RateLimitConfigError,
127 },
128
129 #[error("invalid network rate limiter: at least one of egress or ingress is required")]
131 EmptyNetworkRateLimiter,
132}
133
134#[derive(Clone)]
136pub struct TerminationHandle {
137 shared: Arc<SharedState>,
138}
139
140#[derive(Clone)]
142pub struct MetricsHandle {
143 shared: Arc<SharedState>,
144}
145
146impl HostRoutes {
151 fn detect() -> Self {
152 Self {
153 ipv4: host_has_ipv4_route(),
154 ipv6: host_has_ipv6_route(),
155 }
156 }
157}
158
159impl SmoltcpNetwork {
160 pub fn new(
172 config: ResolvedNetworkConfig,
173 slot: u16,
174 deployment_profile: DeploymentProfile,
175 ) -> Result<Self, NetworkInitError> {
176 Self::build(config, slot, deployment_profile, HostRoutes::detect())
177 }
178
179 fn build(
180 mut config: ResolvedNetworkConfig,
181 slot: u16,
182 deployment_profile: DeploymentProfile,
183 host_routes: HostRoutes,
184 ) -> Result<Self, NetworkInitError> {
185 enforce_deployment_profile(&mut config, deployment_profile);
186 let platform_policy = Self::platform_policy(deployment_profile);
187 let resolved_config = config;
188 let config = resolved_config.config();
189
190 if let Some(configured) = config.max_connections
191 && configured > MAX_NETWORK_CONNECTIONS
192 {
193 return Err(NetworkInitError::MaxConnectionsExceeded {
194 configured,
195 limit: MAX_NETWORK_CONNECTIONS,
196 });
197 }
198
199 let guest_mac = config
200 .interface
201 .mac
202 .unwrap_or_else(|| derive_guest_mac(slot));
203 let gateway_mac = derive_gateway_mac(slot);
204 let mtu = config.interface.mtu.unwrap_or(1500);
205
206 let guest_ipv4 = match config.interface.ipv4_address {
207 Some(address) => Some(address),
208 None if host_routes.ipv4 => Some(derive_guest_ipv4(
209 config
210 .interface
211 .ipv4_pool
212 .unwrap_or_else(default_guest_ipv4_pool),
213 slot,
214 )?),
215 None => None,
216 };
217 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
218 let guest_ipv6 = match config.interface.ipv6_address {
219 Some(address) => Some(address),
220 None if host_routes.ipv6 => Some(derive_guest_ipv6(
221 config
222 .interface
223 .ipv6_pool
224 .unwrap_or_else(default_guest_ipv6_pool),
225 slot,
226 )?),
227 None => None,
228 };
229 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
230
231 let queue_capacity = config
232 .max_connections
233 .unwrap_or(DEFAULT_QUEUE_CAPACITY)
234 .max(DEFAULT_QUEUE_CAPACITY);
235 let shared = Arc::new(SharedState::new(queue_capacity));
236 if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
240 rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
241 }) {
242 return Err(NetworkInitError::EmptyNetworkRateLimiter);
243 }
244 config
245 .rate_limiter
246 .as_ref()
247 .and_then(|rate_limiter| rate_limiter.ingress.as_ref())
248 .map(RateLimiterConfig::validate)
249 .transpose()
250 .map_err(|source| NetworkInitError::InvalidRateLimit {
251 direction: NetworkRateLimitDirection::Ingress,
252 source,
253 })?;
254 config
255 .rate_limiter
256 .as_ref()
257 .and_then(|rate_limiter| rate_limiter.egress.as_ref())
258 .map(RateLimiterConfig::validate)
259 .transpose()
260 .map_err(|source| NetworkInitError::InvalidRateLimit {
261 direction: NetworkRateLimitDirection::Egress,
262 source,
263 })?;
264 let backend = SmoltcpBackend::new(shared.clone());
265
266 let secrets = SecretsHandle::new(config.secrets.clone());
267 let tls_state = if config.tls.enabled {
268 Some(Arc::new(TlsState::new(
269 config.tls.clone(),
270 secrets.clone(),
271 )?))
272 } else {
273 None
274 };
275
276 Ok(Self {
277 config: resolved_config,
278 platform_policy,
279 shared,
280 backend: Some(backend),
281 poll_handle: None,
282 guest_mac,
283 gateway_mac,
284 mtu,
285 guest_ipv4,
286 gateway_ipv4,
287 guest_ipv6,
288 gateway_ipv6,
289 tls_state,
290 secrets,
291 })
292 }
293
294 fn platform_policy(deployment_profile: DeploymentProfile) -> Option<NetworkPolicy> {
295 match deployment_profile {
296 DeploymentProfile::SingleTenant => None,
297 DeploymentProfile::MultiTenant => {
298 Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
299 }
300 }
301 }
302
303 fn gateway_ips(&self) -> GatewayIps {
305 GatewayIps {
306 ipv4: self.gateway_ipv4,
307 ipv6: self.gateway_ipv6,
308 }
309 }
310
311 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
316 let shared = self.shared.clone();
317 let poll_config = PollLoopConfig {
318 gateway_mac: self.gateway_mac,
319 guest_mac: self.guest_mac,
320 gateway: self.gateway_ips(),
321 guest_ipv4: self.guest_ipv4,
322 guest_ipv6: self.guest_ipv6,
323 mtu: self.mtu as usize,
324 };
325 let config = self.config.config();
326 let network_policy = config.policy.clone();
327 let platform_policy = self.platform_policy.clone();
328 let dns_config = config.dns.clone();
329 let tls_state = self.tls_state.clone();
330 let published_ports = config.ports.clone();
331 let max_connections = config.max_connections;
332 let secrets = self.secrets.clone();
333 let outbound_proxy = self.config.outbound_proxy().cloned().map(Arc::new);
334
335 self.poll_handle = Some(
336 std::thread::Builder::new()
337 .name("smoltcp-poll".into())
338 .spawn(move || {
339 poll::smoltcp_poll_loop(
340 shared,
341 poll_config,
342 network_policy,
343 platform_policy,
344 dns_config,
345 tls_state,
346 published_ports,
347 max_connections,
348 tokio_handle,
349 secrets,
350 outbound_proxy,
351 );
352 })
353 .expect("failed to spawn smoltcp poll thread"),
354 );
355 }
356
357 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
359 Box::new(self.backend.take().expect("backend already taken"))
360 }
361
362 pub fn guest_mac(&self) -> [u8; 6] {
364 self.guest_mac
365 }
366
367 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
372 let mut vars = vec![
373 (
374 ENV_NET.into(),
375 format!(
376 "iface=eth0,mac={},mtu={}",
377 format_mac(self.guest_mac),
378 self.mtu,
379 ),
380 ),
381 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
382 ];
383
384 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
385 vars.push((
386 ENV_NET_IPV4.into(),
387 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
388 ));
389 }
390
391 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
392 vars.push((
393 ENV_NET_IPV6.into(),
394 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
395 ));
396 }
397
398 for secret in &self.config.config().secrets.secrets {
400 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
401 }
402
403 vars
404 }
405
406 pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
408 BootstrapNetwork {
409 interface: "eth0".to_string(),
410 mac: self.guest_mac,
411 mtu: self.mtu,
412 ipv4: self
413 .guest_ipv4
414 .zip(self.gateway_ipv4)
415 .map(|(address, gateway)| BootstrapIpv4 {
416 address,
417 prefix_len: 30,
418 gateway,
419 dns: Some(gateway),
420 }),
421 ipv6: self
422 .guest_ipv6
423 .zip(self.gateway_ipv6)
424 .map(|(address, gateway)| BootstrapIpv6 {
425 address,
426 prefix_len: 64,
427 gateway,
428 dns: Some(gateway),
429 }),
430 }
431 }
432
433 pub fn guest_host_alias(&self) -> &'static str {
435 crate::HOST_ALIAS
436 }
437
438 pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
443 self.config
444 .config()
445 .secrets
446 .secrets
447 .iter()
448 .map(|secret| BootstrapEnvVar {
449 key: secret.env_var.clone(),
450 value: secret.placeholder.clone(),
451 })
452 .collect()
453 }
454
455 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
459 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
460 }
461
462 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
470 if !self.config.config().trust_host_cas {
471 return None;
472 }
473 crate::tls::host_cas::collect_host_cas()
474 }
475
476 pub fn termination_handle(&self) -> TerminationHandle {
478 TerminationHandle {
479 shared: self.shared.clone(),
480 }
481 }
482
483 pub fn metrics_handle(&self) -> MetricsHandle {
485 MetricsHandle {
486 shared: self.shared.clone(),
487 }
488 }
489
490 pub fn secrets_handle(&self) -> SecretsHandle {
494 self.secrets.clone()
495 }
496}
497
498impl TerminationHandle {
499 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
501 self.shared.set_termination_hook(hook);
502 }
503}
504
505impl MetricsHandle {
506 pub fn tx_bytes(&self) -> u64 {
508 self.shared.tx_bytes()
509 }
510
511 pub fn rx_bytes(&self) -> u64 {
513 self.shared.rx_bytes()
514 }
515}
516
517fn enforce_deployment_profile(config: &mut ResolvedNetworkConfig, profile: DeploymentProfile) {
528 if profile == DeploymentProfile::SingleTenant {
529 return;
530 }
531
532 config.clear_outbound_proxy();
533
534 let config = config.config_mut();
535 let interface_overridden = config.interface.mac.is_some()
536 || config.interface.mtu.is_some()
537 || config.interface.ipv4_address.is_some()
538 || config.interface.ipv4_pool.is_some()
539 || config.interface.ipv6_address.is_some()
540 || config.interface.ipv6_pool.is_some();
541 let had_published_ports = !config.ports.is_empty();
542 let had_custom_nameservers = !config.dns.nameservers.is_empty();
543 let disabled_rebind_protection = !config.dns.rebind_protection;
544 let trusted_host_cas = config.trust_host_cas;
545 let had_outbound_proxy = config.outbound_proxy.is_some();
546 let connection_limit_clamped = config
547 .max_connections
548 .is_some_and(|limit| limit > MULTI_TENANT_MAX_CONNECTIONS);
549
550 config.interface = Default::default();
551 config.ports.clear();
552 config.dns.nameservers.clear();
553 config.dns.rebind_protection = true;
554 config.trust_host_cas = false;
555 config.max_connections = Some(
556 config
557 .max_connections
558 .unwrap_or(MULTI_TENANT_MAX_CONNECTIONS)
559 .min(MULTI_TENANT_MAX_CONNECTIONS),
560 );
561
562 if interface_overridden
563 || had_published_ports
564 || had_custom_nameservers
565 || disabled_rebind_protection
566 || trusted_host_cas
567 || had_outbound_proxy
568 || connection_limit_clamped
569 {
570 tracing::warn!(
571 interface_overridden,
572 had_published_ports,
573 had_custom_nameservers,
574 disabled_rebind_protection,
575 trusted_host_cas,
576 had_outbound_proxy,
577 connection_limit_clamped,
578 "multi-tenant deployment profile overrode unsafe network configuration"
579 );
580 }
581}
582
583fn derive_guest_mac(slot: u16) -> [u8; 6] {
587 let s = slot.to_be_bytes();
588 [0x02, 0x6d, 0x73, s[0], s[1], 0x02]
589}
590
591fn derive_gateway_mac(slot: u16) -> [u8; 6] {
595 let s = slot.to_be_bytes();
596 [0x02, 0x6d, 0x73, s[0], s[1], 0x01]
597}
598
599fn derive_guest_ipv4(pool: Ipv4Network, slot: u16) -> Result<Ipv4Addr, NetworkInitError> {
604 let capacity = 30_u8
605 .checked_sub(pool.prefix())
606 .map(|host_bits| 1_u32 << host_bits)
607 .ok_or(NetworkInitError::Ipv4PoolCapacity { pool, slot })?;
608 if u32::from(slot) >= capacity {
609 return Err(NetworkInitError::Ipv4PoolCapacity { pool, slot });
610 }
611
612 let base = u32::from(pool.network());
613 let offset = u32::from(slot) * 4 + 2; Ok(Ipv4Addr::from(base + offset))
615}
616
617fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
619 Ipv4Addr::from(u32::from(guest) - 1)
620}
621
622fn default_guest_ipv4_pool() -> Ipv4Network {
623 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
624 .expect("default IPv4 pool must be a valid network")
625}
626
627fn derive_guest_ipv6(pool: Ipv6Network, slot: u16) -> Result<Ipv6Addr, NetworkInitError> {
632 let capacity = 64_u8
633 .checked_sub(pool.prefix())
634 .map(|host_bits| 1_u128 << host_bits)
635 .ok_or(NetworkInitError::Ipv6PoolCapacity { pool, slot })?;
636 if u128::from(slot) >= capacity {
637 return Err(NetworkInitError::Ipv6PoolCapacity { pool, slot });
638 }
639
640 let base = u128::from(pool.network());
641 let offset = u128::from(slot) << 64;
642 Ok(Ipv6Addr::from(base + offset + 2))
643}
644
645fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
647 let segs = guest.segments();
648 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
649}
650
651fn default_guest_ipv6_pool() -> Ipv6Network {
652 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
653 .expect("default IPv6 pool must be a valid network")
654}
655
656fn format_mac(mac: [u8; 6]) -> String {
658 format!(
659 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
660 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
661 )
662}
663
664fn host_has_ipv4_route() -> bool {
670 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
671 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
672 .is_ok()
673}
674
675fn host_has_ipv6_route() -> bool {
679 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
680 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
681 .is_ok()
682}
683
684#[cfg(test)]
689mod tests {
690 use super::*;
691 use crate::config::{EnvNetworkSecretResolver, NetworkConfig, PortProtocol, PublishedPort};
692 use crate::dns::Nameserver;
693
694 fn resolved(config: NetworkConfig) -> ResolvedNetworkConfig {
695 config.resolve(&EnvNetworkSecretResolver).unwrap()
696 }
697
698 fn routes(ipv4: bool, ipv6: bool) -> HostRoutes {
699 HostRoutes { ipv4, ipv6 }
700 }
701
702 #[test]
703 fn derive_addresses_slot_0() {
704 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
705 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
706 assert_eq!(
707 derive_guest_ipv4(default_guest_ipv4_pool(), 0).unwrap(),
708 Ipv4Addr::new(172, 16, 0, 2)
709 );
710 assert_eq!(
711 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
712 Ipv4Addr::new(172, 16, 0, 1)
713 );
714 }
715
716 #[test]
717 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
718 let mut config = NetworkConfig::default();
719 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
720 config.interface.mtu = Some(9000);
721 config.ports.push(PublishedPort {
722 host_port: 8080,
723 guest_port: 80,
724 protocol: PortProtocol::Tcp,
725 host_bind: Ipv4Addr::UNSPECIFIED.into(),
726 });
727 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
728 config.dns.rebind_protection = false;
729 config.trust_host_cas = true;
730 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
731 address: "127.0.0.1:1080".parse().unwrap(),
732 credentials: None,
733 });
734 config.max_connections = Some(MULTI_TENANT_MAX_CONNECTIONS + 1);
735 config.policy = NetworkPolicy::allow_all();
736 let mut resolved = resolved(config);
737
738 enforce_deployment_profile(&mut resolved, DeploymentProfile::MultiTenant);
739 let config = resolved.config();
740
741 assert!(config.interface.mac.is_none());
742 assert!(config.interface.mtu.is_none());
743 assert!(config.ports.is_empty());
744 assert!(config.dns.nameservers.is_empty());
745 assert!(config.dns.rebind_protection);
746 assert!(!config.trust_host_cas);
747 assert!(config.outbound_proxy.is_none());
748 assert_eq!(config.max_connections, Some(MULTI_TENANT_MAX_CONNECTIONS));
749 assert!(resolved.config().outbound_proxy.is_none());
750 assert!(resolved.outbound_proxy().is_none());
751 assert!(config.policy.default_egress.is_allow());
754 }
755
756 #[test]
757 fn single_tenant_profile_preserves_requested_network_controls() {
758 let mut config = NetworkConfig::default();
759 config.interface.mtu = Some(9000);
760 config.dns.rebind_protection = false;
761 config.trust_host_cas = true;
762 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
763 address: "127.0.0.1:1080".parse().unwrap(),
764 credentials: None,
765 });
766
767 let mut resolved = resolved(config);
768 enforce_deployment_profile(&mut resolved, DeploymentProfile::SingleTenant);
769 let config = resolved.config();
770
771 assert_eq!(config.interface.mtu, Some(9000));
772 assert!(!config.dns.rebind_protection);
773 assert!(config.trust_host_cas);
774 assert!(config.outbound_proxy.is_some());
775 }
776
777 #[test]
778 fn derive_addresses_slot_1() {
779 assert_eq!(
780 derive_guest_ipv4(default_guest_ipv4_pool(), 1).unwrap(),
781 Ipv4Addr::new(172, 16, 0, 6)
782 );
783 assert_eq!(
784 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
785 Ipv4Addr::new(172, 16, 0, 5)
786 );
787 }
788
789 #[test]
790 fn derive_addresses_max_slot() {
791 assert_eq!(
792 derive_guest_mac(u16::MAX),
793 [0x02, 0x6d, 0x73, 0xff, 0xff, 0x02]
794 );
795 assert_eq!(
796 derive_guest_ipv4(default_guest_ipv4_pool(), u16::MAX).unwrap(),
797 Ipv4Addr::new(172, 19, 255, 254)
798 );
799 assert_eq!(
800 derive_guest_ipv6(default_guest_ipv6_pool(), u16::MAX).unwrap(),
801 "fd42:6d73:62:ffff::2".parse::<Ipv6Addr>().unwrap()
802 );
803 }
804
805 #[test]
806 fn derive_addresses_custom_ipv4_pool() {
807 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
808 assert_eq!(
809 derive_guest_ipv4(pool, 0).unwrap(),
810 Ipv4Addr::new(172, 31, 240, 2)
811 );
812 assert_eq!(
813 derive_guest_ipv4(pool, 63).unwrap(),
814 Ipv4Addr::new(172, 31, 240, 254)
815 );
816 }
817
818 #[test]
819 fn custom_ipv4_pool_capacity_is_a_typed_error() {
820 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
821 assert!(matches!(
822 derive_guest_ipv4(pool, 64),
823 Err(NetworkInitError::Ipv4PoolCapacity { slot: 64, .. })
824 ));
825
826 let pool = "172.31.240.0/31".parse::<Ipv4Network>().unwrap();
827 assert!(matches!(
828 derive_guest_ipv4(pool, 0),
829 Err(NetworkInitError::Ipv4PoolCapacity { slot: 0, .. })
830 ));
831 }
832
833 #[test]
834 fn derive_ipv6_slot_0() {
835 assert_eq!(
836 derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap(),
837 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
838 );
839 assert_eq!(
840 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap()),
841 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
842 );
843 }
844
845 #[test]
846 fn derive_addresses_custom_ipv6_pool() {
847 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
848 assert_eq!(
849 derive_guest_ipv6(pool, 0).unwrap(),
850 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
851 );
852 assert_eq!(
853 derive_guest_ipv6(pool, 3).unwrap(),
854 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
855 );
856 }
857
858 #[test]
859 fn custom_ipv6_pool_capacity_is_a_typed_error() {
860 let pool = "fd7a:115c:a1e0:100::/62".parse::<Ipv6Network>().unwrap();
861 assert!(matches!(
862 derive_guest_ipv6(pool, 4),
863 Err(NetworkInitError::Ipv6PoolCapacity { slot: 4, .. })
864 ));
865
866 let pool = "fd7a:115c:a1e0:100::/65".parse::<Ipv6Network>().unwrap();
867 assert!(matches!(
868 derive_guest_ipv6(pool, 0),
869 Err(NetworkInitError::Ipv6PoolCapacity { slot: 0, .. })
870 ));
871 }
872
873 #[test]
874 fn format_mac_address() {
875 assert_eq!(
876 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
877 "02:6d:73:00:00:01"
878 );
879 }
880
881 #[test]
882 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
883 let net = SmoltcpNetwork::build(
884 resolved(NetworkConfig::default()),
885 0,
886 DeploymentProfile::SingleTenant,
887 routes(true, false),
888 )
889 .unwrap();
890 let vars = net.guest_env_vars();
891
892 assert_eq!(vars.len(), 3);
893 assert_eq!(vars[0].0, ENV_NET);
894 assert!(vars[0].1.contains("iface=eth0"));
895 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
896 assert_eq!(vars[1].1, crate::HOST_ALIAS);
897 assert_eq!(vars[2].0, ENV_NET_IPV4);
898 assert!(vars[2].1.contains("/30"));
899 }
900
901 #[test]
902 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
903 let net = SmoltcpNetwork::build(
904 resolved(NetworkConfig::default()),
905 0,
906 DeploymentProfile::SingleTenant,
907 routes(true, true),
908 )
909 .unwrap();
910 let vars = net.guest_env_vars();
911
912 assert_eq!(vars.len(), 4);
913 assert_eq!(vars[0].0, ENV_NET);
914 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
915 assert_eq!(vars[2].0, ENV_NET_IPV4);
916 assert_eq!(vars[3].0, ENV_NET_IPV6);
917 assert!(vars[3].1.contains("/64"));
918 }
919
920 #[test]
921 fn guest_env_vars_omit_ipv6_without_host_route() {
922 let net = SmoltcpNetwork::build(
923 resolved(NetworkConfig::default()),
924 0,
925 DeploymentProfile::SingleTenant,
926 routes(true, false),
927 )
928 .unwrap();
929 let vars = net.guest_env_vars();
930
931 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
932 }
933
934 #[test]
935 fn guest_env_vars_omit_ipv4_without_host_route() {
936 let net = SmoltcpNetwork::build(
937 resolved(NetworkConfig::default()),
938 0,
939 DeploymentProfile::SingleTenant,
940 routes(false, true),
941 )
942 .unwrap();
943 let vars = net.guest_env_vars();
944
945 assert_eq!(vars.len(), 3);
946 assert_eq!(vars[0].0, ENV_NET);
947 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
948 assert_eq!(vars[2].0, ENV_NET_IPV6);
949 }
950
951 #[test]
952 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
953 let mut config = NetworkConfig::default();
954 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
955 let net = SmoltcpNetwork::build(
956 resolved(config),
957 0,
958 DeploymentProfile::SingleTenant,
959 routes(true, false),
960 )
961 .unwrap();
962 let vars = net.guest_env_vars();
963
964 let v6 = vars
965 .iter()
966 .find(|(k, _)| k == ENV_NET_IPV6)
967 .expect("explicit ipv6 should publish env var even without host route");
968 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
969 }
970
971 #[test]
972 fn neither_family_active_emits_only_base_env_vars() {
973 let net = SmoltcpNetwork::build(
974 resolved(NetworkConfig::default()),
975 0,
976 DeploymentProfile::SingleTenant,
977 routes(false, false),
978 )
979 .unwrap();
980 let vars = net.guest_env_vars();
981
982 assert_eq!(vars.len(), 2);
983 assert_eq!(vars[0].0, ENV_NET);
984 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
985 }
986
987 #[test]
988 fn guest_bootstrap_network_preserves_active_address_families() {
989 let net = SmoltcpNetwork::build(
990 resolved(NetworkConfig::default()),
991 7,
992 DeploymentProfile::SingleTenant,
993 routes(true, true),
994 )
995 .unwrap();
996
997 let bootstrap = net.guest_bootstrap_network();
998
999 assert_eq!(bootstrap.interface, "eth0");
1000 assert_eq!(bootstrap.mac, net.guest_mac());
1001 assert_eq!(bootstrap.mtu, 1500);
1002 assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
1003 assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
1004 assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
1005 }
1006
1007 #[test]
1008 fn guest_bootstrap_network_allows_no_active_address_family() {
1009 let net = SmoltcpNetwork::build(
1010 resolved(NetworkConfig::default()),
1011 0,
1012 DeploymentProfile::SingleTenant,
1013 routes(false, false),
1014 )
1015 .unwrap();
1016
1017 let bootstrap = net.guest_bootstrap_network();
1018
1019 assert!(bootstrap.ipv4.is_none());
1020 assert!(bootstrap.ipv6.is_none());
1021 }
1022
1023 #[test]
1024 fn build_rejects_excessive_max_connections() {
1025 let mut config = NetworkConfig {
1026 max_connections: Some(MAX_NETWORK_CONNECTIONS + 1),
1027 ..NetworkConfig::default()
1028 };
1029 config.tls.enabled = false;
1030
1031 let err = match SmoltcpNetwork::build(
1032 resolved(config),
1033 0,
1034 DeploymentProfile::SingleTenant,
1035 routes(true, false),
1036 ) {
1037 Ok(_) => panic!("excessive max_connections should fail"),
1038 Err(err) => err,
1039 };
1040
1041 assert!(matches!(
1042 err,
1043 NetworkInitError::MaxConnectionsExceeded {
1044 configured,
1045 limit: MAX_NETWORK_CONNECTIONS
1046 } if configured == MAX_NETWORK_CONNECTIONS + 1
1047 ));
1048 }
1049
1050 #[test]
1053 fn build_rejects_invalid_rate_limiter() {
1054 let mut config = NetworkConfig {
1055 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
1056 egress: None,
1057 ingress: Some(microsandbox_types::RateLimiterConfig {
1058 bandwidth: None,
1059 ops: None,
1060 }),
1061 }),
1062 ..NetworkConfig::default()
1063 };
1064 config.tls.enabled = false;
1065
1066 let err = match SmoltcpNetwork::build(
1067 resolved(config),
1068 0,
1069 DeploymentProfile::SingleTenant,
1070 routes(true, false),
1071 ) {
1072 Ok(_) => panic!("empty rate limiter should fail"),
1073 Err(err) => err,
1074 };
1075
1076 assert!(matches!(
1077 err,
1078 NetworkInitError::InvalidRateLimit {
1079 direction: NetworkRateLimitDirection::Ingress,
1080 source: RateLimitConfigError::EmptyLimiter,
1081 }
1082 ));
1083 }
1084}