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