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 strict = config.strict;
332 let max_connections = config.max_connections;
333 let secrets = self.secrets.clone();
334 let outbound_proxy = self.config.outbound_proxy().cloned().map(Arc::new);
335
336 self.poll_handle = Some(
337 std::thread::Builder::new()
338 .name("smoltcp-poll".into())
339 .spawn(move || {
340 poll::smoltcp_poll_loop(
341 shared,
342 poll_config,
343 network_policy,
344 platform_policy,
345 dns_config,
346 tls_state,
347 published_ports,
348 strict,
349 max_connections,
350 tokio_handle,
351 secrets,
352 outbound_proxy,
353 );
354 })
355 .expect("failed to spawn smoltcp poll thread"),
356 );
357 }
358
359 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
361 Box::new(self.backend.take().expect("backend already taken"))
362 }
363
364 pub fn guest_mac(&self) -> [u8; 6] {
366 self.guest_mac
367 }
368
369 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
374 let mut vars = vec![
375 (
376 ENV_NET.into(),
377 format!(
378 "iface=eth0,mac={},mtu={}",
379 format_mac(self.guest_mac),
380 self.mtu,
381 ),
382 ),
383 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
384 ];
385
386 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
387 vars.push((
388 ENV_NET_IPV4.into(),
389 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
390 ));
391 }
392
393 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
394 vars.push((
395 ENV_NET_IPV6.into(),
396 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
397 ));
398 }
399
400 for secret in &self.config.config().secrets.secrets {
402 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
403 }
404
405 vars
406 }
407
408 pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
410 BootstrapNetwork {
411 interface: "eth0".to_string(),
412 mac: self.guest_mac,
413 mtu: self.mtu,
414 ipv4: self
415 .guest_ipv4
416 .zip(self.gateway_ipv4)
417 .map(|(address, gateway)| BootstrapIpv4 {
418 address,
419 prefix_len: 30,
420 gateway,
421 dns: Some(gateway),
422 }),
423 ipv6: self
424 .guest_ipv6
425 .zip(self.gateway_ipv6)
426 .map(|(address, gateway)| BootstrapIpv6 {
427 address,
428 prefix_len: 64,
429 gateway,
430 dns: Some(gateway),
431 }),
432 }
433 }
434
435 pub fn guest_host_alias(&self) -> &'static str {
437 crate::HOST_ALIAS
438 }
439
440 pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
445 self.config
446 .config()
447 .secrets
448 .secrets
449 .iter()
450 .map(|secret| BootstrapEnvVar {
451 key: secret.env_var.clone(),
452 value: secret.placeholder.clone(),
453 })
454 .collect()
455 }
456
457 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
461 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
462 }
463
464 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
472 if !self.config.config().trust_host_cas {
473 return None;
474 }
475 crate::tls::host_cas::collect_host_cas()
476 }
477
478 pub fn termination_handle(&self) -> TerminationHandle {
480 TerminationHandle {
481 shared: self.shared.clone(),
482 }
483 }
484
485 pub fn metrics_handle(&self) -> MetricsHandle {
487 MetricsHandle {
488 shared: self.shared.clone(),
489 }
490 }
491
492 pub fn secrets_handle(&self) -> SecretsHandle {
496 self.secrets.clone()
497 }
498}
499
500impl TerminationHandle {
501 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
503 self.shared.set_termination_hook(hook);
504 }
505}
506
507impl MetricsHandle {
508 pub fn tx_bytes(&self) -> u64 {
510 self.shared.tx_bytes()
511 }
512
513 pub fn rx_bytes(&self) -> u64 {
515 self.shared.rx_bytes()
516 }
517}
518
519fn enforce_deployment_profile(config: &mut ResolvedNetworkConfig, profile: DeploymentProfile) {
530 if profile == DeploymentProfile::SingleTenant {
531 return;
532 }
533
534 config.clear_outbound_proxy();
535
536 let config = config.config_mut();
537 let interface_overridden = config.interface.mac.is_some()
538 || config.interface.mtu.is_some()
539 || config.interface.ipv4_address.is_some()
540 || config.interface.ipv4_pool.is_some()
541 || config.interface.ipv6_address.is_some()
542 || config.interface.ipv6_pool.is_some();
543 let had_published_ports = !config.ports.is_empty();
544 let had_custom_nameservers = !config.dns.nameservers.is_empty();
545 let disabled_rebind_protection = !config.dns.rebind_protection;
546 let trusted_host_cas = config.trust_host_cas;
547 let had_outbound_proxy = config.outbound_proxy.is_some();
548 let connection_limit_clamped = config
549 .max_connections
550 .is_some_and(|limit| limit > MULTI_TENANT_MAX_CONNECTIONS);
551
552 config.interface = Default::default();
553 config.ports.clear();
554 config.dns.nameservers.clear();
555 config.dns.rebind_protection = true;
556 config.trust_host_cas = false;
557 config.max_connections = Some(
558 config
559 .max_connections
560 .unwrap_or(MULTI_TENANT_MAX_CONNECTIONS)
561 .min(MULTI_TENANT_MAX_CONNECTIONS),
562 );
563
564 if interface_overridden
565 || had_published_ports
566 || had_custom_nameservers
567 || disabled_rebind_protection
568 || trusted_host_cas
569 || had_outbound_proxy
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 had_outbound_proxy,
579 connection_limit_clamped,
580 "multi-tenant deployment profile overrode unsafe network configuration"
581 );
582 }
583}
584
585fn derive_guest_mac(slot: u16) -> [u8; 6] {
589 let s = slot.to_be_bytes();
590 [0x02, 0x6d, 0x73, s[0], s[1], 0x02]
591}
592
593fn derive_gateway_mac(slot: u16) -> [u8; 6] {
597 let s = slot.to_be_bytes();
598 [0x02, 0x6d, 0x73, s[0], s[1], 0x01]
599}
600
601fn derive_guest_ipv4(pool: Ipv4Network, slot: u16) -> Result<Ipv4Addr, NetworkInitError> {
606 let capacity = 30_u8
607 .checked_sub(pool.prefix())
608 .map(|host_bits| 1_u32 << host_bits)
609 .ok_or(NetworkInitError::Ipv4PoolCapacity { pool, slot })?;
610 if u32::from(slot) >= capacity {
611 return Err(NetworkInitError::Ipv4PoolCapacity { pool, slot });
612 }
613
614 let base = u32::from(pool.network());
615 let offset = u32::from(slot) * 4 + 2; Ok(Ipv4Addr::from(base + offset))
617}
618
619fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
621 Ipv4Addr::from(u32::from(guest) - 1)
622}
623
624fn default_guest_ipv4_pool() -> Ipv4Network {
625 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
626 .expect("default IPv4 pool must be a valid network")
627}
628
629fn derive_guest_ipv6(pool: Ipv6Network, slot: u16) -> Result<Ipv6Addr, NetworkInitError> {
634 let capacity = 64_u8
635 .checked_sub(pool.prefix())
636 .map(|host_bits| 1_u128 << host_bits)
637 .ok_or(NetworkInitError::Ipv6PoolCapacity { pool, slot })?;
638 if u128::from(slot) >= capacity {
639 return Err(NetworkInitError::Ipv6PoolCapacity { pool, slot });
640 }
641
642 let base = u128::from(pool.network());
643 let offset = u128::from(slot) << 64;
644 Ok(Ipv6Addr::from(base + offset + 2))
645}
646
647fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
649 let segs = guest.segments();
650 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
651}
652
653fn default_guest_ipv6_pool() -> Ipv6Network {
654 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
655 .expect("default IPv6 pool must be a valid network")
656}
657
658fn format_mac(mac: [u8; 6]) -> String {
660 format!(
661 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
662 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
663 )
664}
665
666fn host_has_ipv4_route() -> bool {
672 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
673 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
674 .is_ok()
675}
676
677fn host_has_ipv6_route() -> bool {
681 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
682 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
683 .is_ok()
684}
685
686#[cfg(test)]
691mod tests {
692 use super::*;
693 use crate::config::{EnvNetworkSecretResolver, NetworkConfig, PortProtocol, PublishedPort};
694 use crate::dns::Nameserver;
695
696 fn resolved(config: NetworkConfig) -> ResolvedNetworkConfig {
697 config.resolve(&EnvNetworkSecretResolver).unwrap()
698 }
699
700 fn routes(ipv4: bool, ipv6: bool) -> HostRoutes {
701 HostRoutes { ipv4, ipv6 }
702 }
703
704 #[test]
705 fn derive_addresses_slot_0() {
706 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
707 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
708 assert_eq!(
709 derive_guest_ipv4(default_guest_ipv4_pool(), 0).unwrap(),
710 Ipv4Addr::new(172, 16, 0, 2)
711 );
712 assert_eq!(
713 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
714 Ipv4Addr::new(172, 16, 0, 1)
715 );
716 }
717
718 #[test]
719 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
720 let mut config = NetworkConfig::default();
721 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
722 config.interface.mtu = Some(9000);
723 config.ports.push(PublishedPort {
724 host_port: 8080,
725 guest_port: 80,
726 protocol: PortProtocol::Tcp,
727 host_bind: Ipv4Addr::UNSPECIFIED.into(),
728 });
729 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
730 config.dns.rebind_protection = false;
731 config.trust_host_cas = true;
732 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
733 address: "127.0.0.1:1080".parse().unwrap(),
734 credentials: None,
735 });
736 config.max_connections = Some(MULTI_TENANT_MAX_CONNECTIONS + 1);
737 config.policy = NetworkPolicy::allow_all();
738 let mut resolved = resolved(config);
739
740 enforce_deployment_profile(&mut resolved, DeploymentProfile::MultiTenant);
741 let config = resolved.config();
742
743 assert!(config.interface.mac.is_none());
744 assert!(config.interface.mtu.is_none());
745 assert!(config.ports.is_empty());
746 assert!(config.dns.nameservers.is_empty());
747 assert!(config.dns.rebind_protection);
748 assert!(!config.trust_host_cas);
749 assert!(config.outbound_proxy.is_none());
750 assert_eq!(config.max_connections, Some(MULTI_TENANT_MAX_CONNECTIONS));
751 assert!(resolved.config().outbound_proxy.is_none());
752 assert!(resolved.outbound_proxy().is_none());
753 assert!(config.policy.default_egress.is_allow());
756 }
757
758 #[test]
759 fn single_tenant_profile_preserves_requested_network_controls() {
760 let mut config = NetworkConfig::default();
761 config.interface.mtu = Some(9000);
762 config.dns.rebind_protection = false;
763 config.trust_host_cas = true;
764 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
765 address: "127.0.0.1:1080".parse().unwrap(),
766 credentials: None,
767 });
768
769 let mut resolved = resolved(config);
770 enforce_deployment_profile(&mut resolved, DeploymentProfile::SingleTenant);
771 let config = resolved.config();
772
773 assert_eq!(config.interface.mtu, Some(9000));
774 assert!(!config.dns.rebind_protection);
775 assert!(config.trust_host_cas);
776 assert!(config.outbound_proxy.is_some());
777 }
778
779 #[test]
780 fn derive_addresses_slot_1() {
781 assert_eq!(
782 derive_guest_ipv4(default_guest_ipv4_pool(), 1).unwrap(),
783 Ipv4Addr::new(172, 16, 0, 6)
784 );
785 assert_eq!(
786 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
787 Ipv4Addr::new(172, 16, 0, 5)
788 );
789 }
790
791 #[test]
792 fn derive_addresses_max_slot() {
793 assert_eq!(
794 derive_guest_mac(u16::MAX),
795 [0x02, 0x6d, 0x73, 0xff, 0xff, 0x02]
796 );
797 assert_eq!(
798 derive_guest_ipv4(default_guest_ipv4_pool(), u16::MAX).unwrap(),
799 Ipv4Addr::new(172, 19, 255, 254)
800 );
801 assert_eq!(
802 derive_guest_ipv6(default_guest_ipv6_pool(), u16::MAX).unwrap(),
803 "fd42:6d73:62:ffff::2".parse::<Ipv6Addr>().unwrap()
804 );
805 }
806
807 #[test]
808 fn derive_addresses_custom_ipv4_pool() {
809 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
810 assert_eq!(
811 derive_guest_ipv4(pool, 0).unwrap(),
812 Ipv4Addr::new(172, 31, 240, 2)
813 );
814 assert_eq!(
815 derive_guest_ipv4(pool, 63).unwrap(),
816 Ipv4Addr::new(172, 31, 240, 254)
817 );
818 }
819
820 #[test]
821 fn custom_ipv4_pool_capacity_is_a_typed_error() {
822 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
823 assert!(matches!(
824 derive_guest_ipv4(pool, 64),
825 Err(NetworkInitError::Ipv4PoolCapacity { slot: 64, .. })
826 ));
827
828 let pool = "172.31.240.0/31".parse::<Ipv4Network>().unwrap();
829 assert!(matches!(
830 derive_guest_ipv4(pool, 0),
831 Err(NetworkInitError::Ipv4PoolCapacity { slot: 0, .. })
832 ));
833 }
834
835 #[test]
836 fn derive_ipv6_slot_0() {
837 assert_eq!(
838 derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap(),
839 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
840 );
841 assert_eq!(
842 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap()),
843 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
844 );
845 }
846
847 #[test]
848 fn derive_addresses_custom_ipv6_pool() {
849 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
850 assert_eq!(
851 derive_guest_ipv6(pool, 0).unwrap(),
852 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
853 );
854 assert_eq!(
855 derive_guest_ipv6(pool, 3).unwrap(),
856 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
857 );
858 }
859
860 #[test]
861 fn custom_ipv6_pool_capacity_is_a_typed_error() {
862 let pool = "fd7a:115c:a1e0:100::/62".parse::<Ipv6Network>().unwrap();
863 assert!(matches!(
864 derive_guest_ipv6(pool, 4),
865 Err(NetworkInitError::Ipv6PoolCapacity { slot: 4, .. })
866 ));
867
868 let pool = "fd7a:115c:a1e0:100::/65".parse::<Ipv6Network>().unwrap();
869 assert!(matches!(
870 derive_guest_ipv6(pool, 0),
871 Err(NetworkInitError::Ipv6PoolCapacity { slot: 0, .. })
872 ));
873 }
874
875 #[test]
876 fn format_mac_address() {
877 assert_eq!(
878 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
879 "02:6d:73:00:00:01"
880 );
881 }
882
883 #[test]
884 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
885 let net = SmoltcpNetwork::build(
886 resolved(NetworkConfig::default()),
887 0,
888 DeploymentProfile::SingleTenant,
889 routes(true, false),
890 )
891 .unwrap();
892 let vars = net.guest_env_vars();
893
894 assert_eq!(vars.len(), 3);
895 assert_eq!(vars[0].0, ENV_NET);
896 assert!(vars[0].1.contains("iface=eth0"));
897 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
898 assert_eq!(vars[1].1, crate::HOST_ALIAS);
899 assert_eq!(vars[2].0, ENV_NET_IPV4);
900 assert!(vars[2].1.contains("/30"));
901 }
902
903 #[test]
904 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
905 let net = SmoltcpNetwork::build(
906 resolved(NetworkConfig::default()),
907 0,
908 DeploymentProfile::SingleTenant,
909 routes(true, true),
910 )
911 .unwrap();
912 let vars = net.guest_env_vars();
913
914 assert_eq!(vars.len(), 4);
915 assert_eq!(vars[0].0, ENV_NET);
916 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
917 assert_eq!(vars[2].0, ENV_NET_IPV4);
918 assert_eq!(vars[3].0, ENV_NET_IPV6);
919 assert!(vars[3].1.contains("/64"));
920 }
921
922 #[test]
923 fn guest_env_vars_omit_ipv6_without_host_route() {
924 let net = SmoltcpNetwork::build(
925 resolved(NetworkConfig::default()),
926 0,
927 DeploymentProfile::SingleTenant,
928 routes(true, false),
929 )
930 .unwrap();
931 let vars = net.guest_env_vars();
932
933 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
934 }
935
936 #[test]
937 fn guest_env_vars_omit_ipv4_without_host_route() {
938 let net = SmoltcpNetwork::build(
939 resolved(NetworkConfig::default()),
940 0,
941 DeploymentProfile::SingleTenant,
942 routes(false, true),
943 )
944 .unwrap();
945 let vars = net.guest_env_vars();
946
947 assert_eq!(vars.len(), 3);
948 assert_eq!(vars[0].0, ENV_NET);
949 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
950 assert_eq!(vars[2].0, ENV_NET_IPV6);
951 }
952
953 #[test]
954 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
955 let mut config = NetworkConfig::default();
956 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
957 let net = SmoltcpNetwork::build(
958 resolved(config),
959 0,
960 DeploymentProfile::SingleTenant,
961 routes(true, false),
962 )
963 .unwrap();
964 let vars = net.guest_env_vars();
965
966 let v6 = vars
967 .iter()
968 .find(|(k, _)| k == ENV_NET_IPV6)
969 .expect("explicit ipv6 should publish env var even without host route");
970 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
971 }
972
973 #[test]
974 fn neither_family_active_emits_only_base_env_vars() {
975 let net = SmoltcpNetwork::build(
976 resolved(NetworkConfig::default()),
977 0,
978 DeploymentProfile::SingleTenant,
979 routes(false, false),
980 )
981 .unwrap();
982 let vars = net.guest_env_vars();
983
984 assert_eq!(vars.len(), 2);
985 assert_eq!(vars[0].0, ENV_NET);
986 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
987 }
988
989 #[test]
990 fn guest_bootstrap_network_preserves_active_address_families() {
991 let net = SmoltcpNetwork::build(
992 resolved(NetworkConfig::default()),
993 7,
994 DeploymentProfile::SingleTenant,
995 routes(true, true),
996 )
997 .unwrap();
998
999 let bootstrap = net.guest_bootstrap_network();
1000
1001 assert_eq!(bootstrap.interface, "eth0");
1002 assert_eq!(bootstrap.mac, net.guest_mac());
1003 assert_eq!(bootstrap.mtu, 1500);
1004 assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
1005 assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
1006 assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
1007 }
1008
1009 #[test]
1010 fn guest_bootstrap_network_allows_no_active_address_family() {
1011 let net = SmoltcpNetwork::build(
1012 resolved(NetworkConfig::default()),
1013 0,
1014 DeploymentProfile::SingleTenant,
1015 routes(false, false),
1016 )
1017 .unwrap();
1018
1019 let bootstrap = net.guest_bootstrap_network();
1020
1021 assert!(bootstrap.ipv4.is_none());
1022 assert!(bootstrap.ipv6.is_none());
1023 }
1024
1025 #[test]
1026 fn build_rejects_excessive_max_connections() {
1027 let mut config = NetworkConfig {
1028 max_connections: Some(MAX_NETWORK_CONNECTIONS + 1),
1029 ..NetworkConfig::default()
1030 };
1031 config.tls.enabled = false;
1032
1033 let err = match SmoltcpNetwork::build(
1034 resolved(config),
1035 0,
1036 DeploymentProfile::SingleTenant,
1037 routes(true, false),
1038 ) {
1039 Ok(_) => panic!("excessive max_connections should fail"),
1040 Err(err) => err,
1041 };
1042
1043 assert!(matches!(
1044 err,
1045 NetworkInitError::MaxConnectionsExceeded {
1046 configured,
1047 limit: MAX_NETWORK_CONNECTIONS
1048 } if configured == MAX_NETWORK_CONNECTIONS + 1
1049 ));
1050 }
1051
1052 #[test]
1055 fn build_rejects_invalid_rate_limiter() {
1056 let mut config = NetworkConfig {
1057 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
1058 egress: None,
1059 ingress: Some(microsandbox_types::RateLimiterConfig {
1060 bandwidth: None,
1061 ops: None,
1062 }),
1063 }),
1064 ..NetworkConfig::default()
1065 };
1066 config.tls.enabled = false;
1067
1068 let err = match SmoltcpNetwork::build(
1069 resolved(config),
1070 0,
1071 DeploymentProfile::SingleTenant,
1072 routes(true, false),
1073 ) {
1074 Ok(_) => panic!("empty rate limiter should fail"),
1075 Err(err) => err,
1076 };
1077
1078 assert!(matches!(
1079 err,
1080 NetworkInitError::InvalidRateLimit {
1081 direction: NetworkRateLimitDirection::Ingress,
1082 source: RateLimitConfigError::EmptyLimiter,
1083 }
1084 ));
1085 }
1086}