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, NetworkConfig};
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 MAX_SLOT: u64 = u16::MAX as u64;
41
42const MULTI_TENANT_MAX_CONNECTIONS: usize = 256;
47
48pub struct SmoltcpNetwork {
59 config: NetworkConfig,
60 deployment_profile: DeploymentProfile,
61 shared: Arc<SharedState>,
62 backend: Option<SmoltcpBackend>,
63 poll_handle: Option<JoinHandle<()>>,
64
65 guest_mac: [u8; 6],
67 gateway_mac: [u8; 6],
68 mtu: u16,
69 guest_ipv4: Option<Ipv4Addr>,
72 gateway_ipv4: Option<Ipv4Addr>,
73 guest_ipv6: Option<Ipv6Addr>,
74 gateway_ipv6: Option<Ipv6Addr>,
75
76 tls_state: Option<Arc<TlsState>>,
78
79 secrets: SecretsHandle,
81}
82
83#[derive(Debug, thiserror::Error)]
85pub enum NetworkInitError {
86 #[error("max_connections {configured} exceeds hard limit {limit}")]
88 MaxConnectionsExceeded {
89 configured: usize,
91 limit: usize,
93 },
94
95 #[error("TLS initialization failed: {0}")]
97 Tls(#[from] TlsStateError),
98
99 #[error("invalid {direction} rate limiter: {source}")]
101 InvalidRateLimit {
102 direction: NetworkRateLimitDirection,
104 #[source]
106 source: RateLimitConfigError,
107 },
108
109 #[error("invalid network rate limiter: at least one of egress or ingress is required")]
111 EmptyNetworkRateLimiter,
112}
113
114#[derive(Clone)]
116pub struct TerminationHandle {
117 shared: Arc<SharedState>,
118}
119
120#[derive(Clone)]
122pub struct MetricsHandle {
123 shared: Arc<SharedState>,
124}
125
126impl SmoltcpNetwork {
131 pub fn new(config: NetworkConfig, slot: u64) -> Result<Self, NetworkInitError> {
149 Self::new_with_profile(config, slot, DeploymentProfile::SingleTenant)
150 }
151
152 pub fn new_with_profile(
164 mut config: NetworkConfig,
165 slot: u64,
166 deployment_profile: DeploymentProfile,
167 ) -> Result<Self, NetworkInitError> {
168 enforce_deployment_profile(&mut config, deployment_profile);
169 Self::new_with_profile_and_routes(
170 config,
171 slot,
172 deployment_profile,
173 host_has_ipv4_route(),
174 host_has_ipv6_route(),
175 )
176 }
177
178 #[cfg(test)]
179 fn new_with_routes(
180 config: NetworkConfig,
181 slot: u64,
182 host_has_ipv4: bool,
183 host_has_ipv6: bool,
184 ) -> Result<Self, NetworkInitError> {
185 Self::new_with_profile_and_routes(
186 config,
187 slot,
188 DeploymentProfile::SingleTenant,
189 host_has_ipv4,
190 host_has_ipv6,
191 )
192 }
193
194 fn new_with_profile_and_routes(
195 config: NetworkConfig,
196 slot: u64,
197 deployment_profile: DeploymentProfile,
198 host_has_ipv4: bool,
199 host_has_ipv6: bool,
200 ) -> Result<Self, NetworkInitError> {
201 assert!(
202 slot <= MAX_SLOT,
203 "sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
204 );
205 if let Some(configured) = config.max_connections
206 && configured > MAX_NETWORK_CONNECTIONS
207 {
208 return Err(NetworkInitError::MaxConnectionsExceeded {
209 configured,
210 limit: MAX_NETWORK_CONNECTIONS,
211 });
212 }
213
214 let guest_mac = config
215 .interface
216 .mac
217 .unwrap_or_else(|| derive_guest_mac(slot));
218 let gateway_mac = derive_gateway_mac(slot);
219 let mtu = config.interface.mtu.unwrap_or(1500);
220
221 let guest_ipv4 = config.interface.ipv4_address.or_else(|| {
222 host_has_ipv4.then(|| {
223 derive_guest_ipv4(
224 config
225 .interface
226 .ipv4_pool
227 .unwrap_or_else(default_guest_ipv4_pool),
228 slot,
229 )
230 })
231 });
232 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
233 let guest_ipv6 = config.interface.ipv6_address.or_else(|| {
234 host_has_ipv6.then(|| {
235 derive_guest_ipv6(
236 config
237 .interface
238 .ipv6_pool
239 .unwrap_or_else(default_guest_ipv6_pool),
240 slot,
241 )
242 })
243 });
244 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
245
246 let queue_capacity = config
247 .max_connections
248 .unwrap_or(DEFAULT_QUEUE_CAPACITY)
249 .max(DEFAULT_QUEUE_CAPACITY);
250 let shared = Arc::new(SharedState::new(queue_capacity));
251 if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
255 rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
256 }) {
257 return Err(NetworkInitError::EmptyNetworkRateLimiter);
258 }
259 config
260 .rate_limiter
261 .as_ref()
262 .and_then(|rate_limiter| rate_limiter.ingress.as_ref())
263 .map(RateLimiterConfig::validate)
264 .transpose()
265 .map_err(|source| NetworkInitError::InvalidRateLimit {
266 direction: NetworkRateLimitDirection::Ingress,
267 source,
268 })?;
269 config
270 .rate_limiter
271 .as_ref()
272 .and_then(|rate_limiter| rate_limiter.egress.as_ref())
273 .map(RateLimiterConfig::validate)
274 .transpose()
275 .map_err(|source| NetworkInitError::InvalidRateLimit {
276 direction: NetworkRateLimitDirection::Egress,
277 source,
278 })?;
279 let backend = SmoltcpBackend::new(shared.clone());
280
281 let secrets = SecretsHandle::new(config.secrets.clone());
282 let tls_state = if config.tls.enabled {
283 Some(Arc::new(TlsState::new(
284 config.tls.clone(),
285 secrets.clone(),
286 )?))
287 } else {
288 None
289 };
290
291 Ok(Self {
292 config,
293 deployment_profile,
294 shared,
295 backend: Some(backend),
296 poll_handle: None,
297 guest_mac,
298 gateway_mac,
299 mtu,
300 guest_ipv4,
301 gateway_ipv4,
302 guest_ipv6,
303 gateway_ipv6,
304 tls_state,
305 secrets,
306 })
307 }
308
309 fn gateway_ips(&self) -> GatewayIps {
311 GatewayIps {
312 ipv4: self.gateway_ipv4,
313 ipv6: self.gateway_ipv6,
314 }
315 }
316
317 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
322 let shared = self.shared.clone();
323 let poll_config = PollLoopConfig {
324 gateway_mac: self.gateway_mac,
325 guest_mac: self.guest_mac,
326 gateway: self.gateway_ips(),
327 guest_ipv4: self.guest_ipv4,
328 guest_ipv6: self.guest_ipv6,
329 mtu: self.mtu as usize,
330 };
331 let network_policy = self.config.policy.clone();
332 let platform_policy = match self.deployment_profile {
333 DeploymentProfile::SingleTenant => None,
334 DeploymentProfile::MultiTenant => {
335 Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
336 }
337 };
338 let dns_config = self.config.dns.clone();
339 let tls_state = self.tls_state.clone();
340 let published_ports = self.config.ports.clone();
341 let max_connections = self.config.max_connections;
342 let secrets = self.secrets.clone();
343
344 self.poll_handle = Some(
345 std::thread::Builder::new()
346 .name("smoltcp-poll".into())
347 .spawn(move || {
348 poll::smoltcp_poll_loop(
349 shared,
350 poll_config,
351 network_policy,
352 platform_policy,
353 dns_config,
354 tls_state,
355 published_ports,
356 max_connections,
357 tokio_handle,
358 secrets,
359 );
360 })
361 .expect("failed to spawn smoltcp poll thread"),
362 );
363 }
364
365 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
367 Box::new(self.backend.take().expect("backend already taken"))
368 }
369
370 pub fn guest_mac(&self) -> [u8; 6] {
372 self.guest_mac
373 }
374
375 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
380 let mut vars = vec![
381 (
382 ENV_NET.into(),
383 format!(
384 "iface=eth0,mac={},mtu={}",
385 format_mac(self.guest_mac),
386 self.mtu,
387 ),
388 ),
389 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
390 ];
391
392 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
393 vars.push((
394 ENV_NET_IPV4.into(),
395 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
396 ));
397 }
398
399 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
400 vars.push((
401 ENV_NET_IPV6.into(),
402 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
403 ));
404 }
405
406 for secret in &self.config.secrets.secrets {
408 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
409 }
410
411 vars
412 }
413
414 pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
416 BootstrapNetwork {
417 interface: "eth0".to_string(),
418 mac: self.guest_mac,
419 mtu: self.mtu,
420 ipv4: self
421 .guest_ipv4
422 .zip(self.gateway_ipv4)
423 .map(|(address, gateway)| BootstrapIpv4 {
424 address,
425 prefix_len: 30,
426 gateway,
427 dns: Some(gateway),
428 }),
429 ipv6: self
430 .guest_ipv6
431 .zip(self.gateway_ipv6)
432 .map(|(address, gateway)| BootstrapIpv6 {
433 address,
434 prefix_len: 64,
435 gateway,
436 dns: Some(gateway),
437 }),
438 }
439 }
440
441 pub fn guest_host_alias(&self) -> &'static str {
443 crate::HOST_ALIAS
444 }
445
446 pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
451 self.config
452 .secrets
453 .secrets
454 .iter()
455 .map(|secret| BootstrapEnvVar {
456 key: secret.env_var.clone(),
457 value: secret.placeholder.clone(),
458 })
459 .collect()
460 }
461
462 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
466 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
467 }
468
469 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
477 if !self.config.trust_host_cas {
478 return None;
479 }
480 crate::tls::host_cas::collect_host_cas()
481 }
482
483 pub fn termination_handle(&self) -> TerminationHandle {
485 TerminationHandle {
486 shared: self.shared.clone(),
487 }
488 }
489
490 pub fn metrics_handle(&self) -> MetricsHandle {
492 MetricsHandle {
493 shared: self.shared.clone(),
494 }
495 }
496
497 pub fn secrets_handle(&self) -> SecretsHandle {
501 self.secrets.clone()
502 }
503}
504
505impl TerminationHandle {
506 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
508 self.shared.set_termination_hook(hook);
509 }
510}
511
512impl MetricsHandle {
513 pub fn tx_bytes(&self) -> u64 {
515 self.shared.tx_bytes()
516 }
517
518 pub fn rx_bytes(&self) -> u64 {
520 self.shared.rx_bytes()
521 }
522}
523
524fn enforce_deployment_profile(config: &mut NetworkConfig, profile: DeploymentProfile) {
535 if profile == DeploymentProfile::SingleTenant {
536 return;
537 }
538
539 let interface_overridden = config.interface.mac.is_some()
540 || config.interface.mtu.is_some()
541 || config.interface.ipv4_address.is_some()
542 || config.interface.ipv4_pool.is_some()
543 || config.interface.ipv6_address.is_some()
544 || config.interface.ipv6_pool.is_some();
545 let had_published_ports = !config.ports.is_empty();
546 let had_custom_nameservers = !config.dns.nameservers.is_empty();
547 let disabled_rebind_protection = !config.dns.rebind_protection;
548 let trusted_host_cas = config.trust_host_cas;
549 let connection_limit_clamped = config
550 .max_connections
551 .is_some_and(|limit| limit > MULTI_TENANT_MAX_CONNECTIONS);
552
553 config.interface = Default::default();
554 config.ports.clear();
555 config.dns.nameservers.clear();
556 config.dns.rebind_protection = true;
557 config.trust_host_cas = false;
558 config.max_connections = Some(
559 config
560 .max_connections
561 .unwrap_or(MULTI_TENANT_MAX_CONNECTIONS)
562 .min(MULTI_TENANT_MAX_CONNECTIONS),
563 );
564
565 if interface_overridden
566 || had_published_ports
567 || had_custom_nameservers
568 || disabled_rebind_protection
569 || trusted_host_cas
570 || connection_limit_clamped
571 {
572 tracing::warn!(
573 interface_overridden,
574 had_published_ports,
575 had_custom_nameservers,
576 disabled_rebind_protection,
577 trusted_host_cas,
578 connection_limit_clamped,
579 "multi-tenant deployment profile overrode unsafe network configuration"
580 );
581 }
582}
583
584fn derive_guest_mac(slot: u64) -> [u8; 6] {
588 let s = slot.to_be_bytes();
589 [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
590}
591
592fn derive_gateway_mac(slot: u64) -> [u8; 6] {
596 let s = slot.to_be_bytes();
597 [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
598}
599
600fn derive_guest_ipv4(pool: Ipv4Network, slot: u64) -> Ipv4Addr {
605 assert!(
606 pool.prefix() <= 30,
607 "IPv4 pool {pool} must be large enough to contain at least one /30 block"
608 );
609
610 let capacity = 1u64 << (30 - pool.prefix());
611 assert!(
612 slot < capacity,
613 "sandbox slot {slot} exceeds IPv4 pool {pool} capacity ({capacity} /30 blocks)"
614 );
615
616 let base = u32::from(pool.network());
617 let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
619}
620
621fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
623 Ipv4Addr::from(u32::from(guest) - 1)
624}
625
626fn default_guest_ipv4_pool() -> Ipv4Network {
627 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
628 .expect("default IPv4 pool must be a valid network")
629}
630
631fn derive_guest_ipv6(pool: Ipv6Network, slot: u64) -> Ipv6Addr {
636 assert!(
637 pool.prefix() <= 64,
638 "IPv6 pool {pool} must be large enough to contain at least one /64 prefix"
639 );
640
641 let capacity = 1u128 << (64 - pool.prefix());
642 assert!(
643 (slot as u128) < capacity,
644 "sandbox slot {slot} exceeds IPv6 pool {pool} capacity ({capacity} /64 prefixes)"
645 );
646
647 let base = u128::from(pool.network());
648 let offset = (slot as u128) << 64;
649 Ipv6Addr::from(base + offset + 2)
650}
651
652fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
654 let segs = guest.segments();
655 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
656}
657
658fn default_guest_ipv6_pool() -> Ipv6Network {
659 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
660 .expect("default IPv6 pool must be a valid network")
661}
662
663fn format_mac(mac: [u8; 6]) -> String {
665 format!(
666 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
667 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
668 )
669}
670
671fn host_has_ipv4_route() -> bool {
677 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
678 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
679 .is_ok()
680}
681
682fn host_has_ipv6_route() -> bool {
686 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
687 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
688 .is_ok()
689}
690
691#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::config::{PortProtocol, PublishedPort};
699 use crate::dns::Nameserver;
700
701 #[test]
702 fn derive_addresses_slot_0() {
703 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
704 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
705 assert_eq!(
706 derive_guest_ipv4(default_guest_ipv4_pool(), 0),
707 Ipv4Addr::new(172, 16, 0, 2)
708 );
709 assert_eq!(
710 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
711 Ipv4Addr::new(172, 16, 0, 1)
712 );
713 }
714
715 #[test]
716 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
717 let mut config = NetworkConfig::default();
718 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
719 config.interface.mtu = Some(9000);
720 config.ports.push(PublishedPort {
721 host_port: 8080,
722 guest_port: 80,
723 protocol: PortProtocol::Tcp,
724 host_bind: Ipv4Addr::UNSPECIFIED.into(),
725 });
726 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
727 config.dns.rebind_protection = false;
728 config.trust_host_cas = true;
729 config.max_connections = Some(MULTI_TENANT_MAX_CONNECTIONS + 1);
730 config.policy = NetworkPolicy::allow_all();
731
732 enforce_deployment_profile(&mut config, DeploymentProfile::MultiTenant);
733
734 assert!(config.interface.mac.is_none());
735 assert!(config.interface.mtu.is_none());
736 assert!(config.ports.is_empty());
737 assert!(config.dns.nameservers.is_empty());
738 assert!(config.dns.rebind_protection);
739 assert!(!config.trust_host_cas);
740 assert_eq!(config.max_connections, Some(MULTI_TENANT_MAX_CONNECTIONS));
741 assert!(config.policy.default_egress.is_allow());
744 }
745
746 #[test]
747 fn single_tenant_profile_preserves_requested_network_controls() {
748 let mut config = NetworkConfig::default();
749 config.interface.mtu = Some(9000);
750 config.dns.rebind_protection = false;
751 config.trust_host_cas = true;
752
753 enforce_deployment_profile(&mut config, DeploymentProfile::SingleTenant);
754
755 assert_eq!(config.interface.mtu, Some(9000));
756 assert!(!config.dns.rebind_protection);
757 assert!(config.trust_host_cas);
758 }
759
760 #[test]
761 fn derive_addresses_slot_1() {
762 assert_eq!(
763 derive_guest_ipv4(default_guest_ipv4_pool(), 1),
764 Ipv4Addr::new(172, 16, 0, 6)
765 );
766 assert_eq!(
767 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
768 Ipv4Addr::new(172, 16, 0, 5)
769 );
770 }
771
772 #[test]
773 fn derive_addresses_custom_ipv4_pool() {
774 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
775 assert_eq!(derive_guest_ipv4(pool, 0), Ipv4Addr::new(172, 31, 240, 2));
776 assert_eq!(
777 derive_guest_ipv4(pool, 63),
778 Ipv4Addr::new(172, 31, 240, 254)
779 );
780 }
781
782 #[test]
783 fn derive_ipv6_slot_0() {
784 assert_eq!(
785 derive_guest_ipv6(default_guest_ipv6_pool(), 0),
786 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
787 );
788 assert_eq!(
789 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0)),
790 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
791 );
792 }
793
794 #[test]
795 fn derive_addresses_custom_ipv6_pool() {
796 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
797 assert_eq!(
798 derive_guest_ipv6(pool, 0),
799 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
800 );
801 assert_eq!(
802 derive_guest_ipv6(pool, 3),
803 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
804 );
805 }
806
807 #[test]
808 fn format_mac_address() {
809 assert_eq!(
810 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
811 "02:6d:73:00:00:01"
812 );
813 }
814
815 #[test]
816 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
817 let net =
818 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false).unwrap();
819 let vars = net.guest_env_vars();
820
821 assert_eq!(vars.len(), 3);
822 assert_eq!(vars[0].0, ENV_NET);
823 assert!(vars[0].1.contains("iface=eth0"));
824 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
825 assert_eq!(vars[1].1, crate::HOST_ALIAS);
826 assert_eq!(vars[2].0, ENV_NET_IPV4);
827 assert!(vars[2].1.contains("/30"));
828 }
829
830 #[test]
831 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
832 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, true).unwrap();
833 let vars = net.guest_env_vars();
834
835 assert_eq!(vars.len(), 4);
836 assert_eq!(vars[0].0, ENV_NET);
837 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
838 assert_eq!(vars[2].0, ENV_NET_IPV4);
839 assert_eq!(vars[3].0, ENV_NET_IPV6);
840 assert!(vars[3].1.contains("/64"));
841 }
842
843 #[test]
844 fn guest_env_vars_omit_ipv6_without_host_route() {
845 let net =
846 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false).unwrap();
847 let vars = net.guest_env_vars();
848
849 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
850 }
851
852 #[test]
853 fn guest_env_vars_omit_ipv4_without_host_route() {
854 let net =
855 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, true).unwrap();
856 let vars = net.guest_env_vars();
857
858 assert_eq!(vars.len(), 3);
859 assert_eq!(vars[0].0, ENV_NET);
860 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
861 assert_eq!(vars[2].0, ENV_NET_IPV6);
862 }
863
864 #[test]
865 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
866 let mut config = NetworkConfig::default();
867 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
868 let net = SmoltcpNetwork::new_with_routes(config, 0, true, false).unwrap();
869 let vars = net.guest_env_vars();
870
871 let v6 = vars
872 .iter()
873 .find(|(k, _)| k == ENV_NET_IPV6)
874 .expect("explicit ipv6 should publish env var even without host route");
875 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
876 }
877
878 #[test]
879 fn neither_family_active_emits_only_base_env_vars() {
880 let net =
881 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false).unwrap();
882 let vars = net.guest_env_vars();
883
884 assert_eq!(vars.len(), 2);
885 assert_eq!(vars[0].0, ENV_NET);
886 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
887 }
888
889 #[test]
890 fn guest_bootstrap_network_preserves_active_address_families() {
891 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 7, true, true).unwrap();
892
893 let bootstrap = net.guest_bootstrap_network();
894
895 assert_eq!(bootstrap.interface, "eth0");
896 assert_eq!(bootstrap.mac, net.guest_mac());
897 assert_eq!(bootstrap.mtu, 1500);
898 assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
899 assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
900 assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
901 }
902
903 #[test]
904 fn guest_bootstrap_network_allows_no_active_address_family() {
905 let net =
906 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false).unwrap();
907
908 let bootstrap = net.guest_bootstrap_network();
909
910 assert!(bootstrap.ipv4.is_none());
911 assert!(bootstrap.ipv6.is_none());
912 }
913
914 #[test]
915 fn new_with_routes_rejects_excessive_max_connections() {
916 let mut config = NetworkConfig {
917 max_connections: Some(MAX_NETWORK_CONNECTIONS + 1),
918 ..NetworkConfig::default()
919 };
920 config.tls.enabled = false;
921
922 let err = match SmoltcpNetwork::new_with_routes(config, 0, true, false) {
923 Ok(_) => panic!("excessive max_connections should fail"),
924 Err(err) => err,
925 };
926
927 assert!(matches!(
928 err,
929 NetworkInitError::MaxConnectionsExceeded {
930 configured,
931 limit: MAX_NETWORK_CONNECTIONS
932 } if configured == MAX_NETWORK_CONNECTIONS + 1
933 ));
934 }
935
936 #[test]
939 fn new_with_routes_rejects_invalid_rate_limiter() {
940 let mut config = NetworkConfig {
941 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
942 egress: None,
943 ingress: Some(microsandbox_types::RateLimiterConfig {
944 bandwidth: None,
945 ops: None,
946 }),
947 }),
948 ..NetworkConfig::default()
949 };
950 config.tls.enabled = false;
951
952 let err = match SmoltcpNetwork::new_with_routes(config, 0, true, false) {
953 Ok(_) => panic!("empty rate limiter should fail"),
954 Err(err) => err,
955 };
956
957 assert!(matches!(
958 err,
959 NetworkInitError::InvalidRateLimit {
960 direction: NetworkRateLimitDirection::Ingress,
961 source: RateLimitConfigError::EmptyLimiter,
962 }
963 ));
964 }
965}