1use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
9use std::num::NonZeroUsize;
10use std::sync::{Arc, Condvar, Mutex};
11use std::thread::JoinHandle;
12
13use ipnetwork::{Ipv4Network, Ipv6Network};
14use microsandbox_protocol::bootstrap::{
15 BootstrapEnvVar, BootstrapIpv4, BootstrapIpv6, BootstrapNetwork,
16};
17use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
18use microsandbox_types::{
19 DeploymentProfile, NetworkRateLimitDirection, RateLimitConfigError, RateLimiterConfig,
20};
21use msb_krun::backends::net::NetBackend;
22
23use crate::config::{ConnectionLimit, ResolvedNetworkConfig};
24use crate::engine::tls::state::{TlsState, TlsStateError};
25use crate::netstack::{
26 backend::SmoltcpBackend,
27 poll::{self, GatewayIps, PollLoopConfig},
28 shared::{DEFAULT_QUEUE_CAPACITY, SharedState},
29};
30use crate::policy::{NetworkPolicy, NetworkProfile};
31use crate::secrets::handle::SecretsHandle;
32
33const DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
39
40const DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
42
43pub struct SmoltcpNetwork {
54 config: ResolvedNetworkConfig,
55 platform_policy: Option<NetworkPolicy>,
58 shared: Arc<SharedState>,
59 backend: Option<SmoltcpBackend>,
60 poll_handle: Option<JoinHandle<()>>,
61 activation_gate: Option<Arc<NetworkActivationGate>>,
62
63 guest_mac: [u8; 6],
65 gateway_mac: [u8; 6],
66 mtu: u16,
67 guest_ipv4: Option<Ipv4Addr>,
70 gateway_ipv4: Option<Ipv4Addr>,
71 guest_ipv6: Option<Ipv6Addr>,
72 gateway_ipv6: Option<Ipv6Addr>,
73
74 tls_state: Option<Arc<TlsState>>,
76
77 secrets: SecretsHandle,
79}
80
81#[derive(Clone, Copy)]
82struct HostRoutes {
83 ipv4: bool,
84 ipv6: bool,
85}
86
87#[derive(Debug, thiserror::Error)]
89pub enum NetworkInitError {
90 #[error("captured gateway MAC must be a nonzero unicast address distinct from the guest")]
92 InvalidGatewayMac,
93 #[error("IPv4 pool {pool} cannot assign network slot {slot}")]
95 Ipv4PoolCapacity {
96 pool: Ipv4Network,
98 slot: u16,
100 },
101
102 #[error("IPv6 pool {pool} cannot assign network slot {slot}")]
104 Ipv6PoolCapacity {
105 pool: Ipv6Network,
107 slot: u16,
109 },
110
111 #[error("invalid NAT64 prefix `{raw}`: prefix must be IPv6 /96")]
113 InvalidNat64Prefix {
114 raw: String,
116 },
117
118 #[error("TLS initialization failed: {0}")]
120 Tls(#[from] TlsStateError),
121
122 #[error("invalid {direction} rate limiter: {source}")]
124 InvalidRateLimit {
125 direction: NetworkRateLimitDirection,
127 #[source]
129 source: RateLimitConfigError,
130 },
131
132 #[error("invalid network rate limiter: at least one of egress or ingress is required")]
134 EmptyNetworkRateLimiter,
135}
136
137#[derive(Clone)]
139pub struct TerminationHandle {
140 shared: Arc<SharedState>,
141}
142
143#[derive(Clone)]
145pub struct MetricsHandle {
146 shared: Arc<SharedState>,
147}
148
149#[derive(Clone)]
151pub struct NetworkActivationHandle {
152 gate: Arc<NetworkActivationGate>,
153}
154
155struct NetworkActivationGate {
156 active: Mutex<bool>,
157 changed: Condvar,
158}
159
160impl HostRoutes {
165 fn detect() -> Self {
166 Self {
167 ipv4: host_has_ipv4_route(),
168 ipv6: host_has_ipv6_route(),
169 }
170 }
171}
172
173impl SmoltcpNetwork {
174 pub fn default_gateway_mac(slot: u16) -> [u8; 6] {
176 derive_gateway_mac(slot)
177 }
178
179 pub fn with_captured_gateway_mac(mut self, mac: [u8; 6]) -> Result<Self, NetworkInitError> {
185 assert!(
186 self.poll_handle.is_none(),
187 "gateway identity must be set before network start"
188 );
189 if mac == [0; 6] || mac[0] & 1 != 0 || mac == self.guest_mac {
190 return Err(NetworkInitError::InvalidGatewayMac);
191 }
192 self.gateway_mac = mac;
193 Ok(self)
194 }
195
196 pub fn new(
208 config: ResolvedNetworkConfig,
209 slot: u16,
210 deployment_profile: DeploymentProfile,
211 ) -> Result<Self, NetworkInitError> {
212 Self::build(config, slot, deployment_profile, HostRoutes::detect())
213 }
214
215 fn build(
216 mut config: ResolvedNetworkConfig,
217 slot: u16,
218 deployment_profile: DeploymentProfile,
219 host_routes: HostRoutes,
220 ) -> Result<Self, NetworkInitError> {
221 enforce_deployment_profile(&mut config, deployment_profile);
222 let platform_policy = Self::platform_policy(deployment_profile);
223 let resolved_config = config;
224 let config = resolved_config.config();
225
226 let guest_mac = config
227 .interface
228 .mac
229 .unwrap_or_else(|| derive_guest_mac(slot));
230 let gateway_mac = derive_gateway_mac(slot);
231 let mtu = config.interface.mtu.unwrap_or(1500);
232
233 let guest_ipv4 = match config.interface.ipv4_address {
234 Some(address) => Some(address),
235 None if host_routes.ipv4 => Some(derive_guest_ipv4(
236 config
237 .interface
238 .ipv4_pool
239 .unwrap_or_else(default_guest_ipv4_pool),
240 slot,
241 )?),
242 None => None,
243 };
244 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
245 let guest_ipv6 = match config.interface.ipv6_address {
246 Some(address) => Some(address),
247 None if host_routes.ipv6 => Some(derive_guest_ipv6(
248 config
249 .interface
250 .ipv6_pool
251 .unwrap_or_else(default_guest_ipv6_pool),
252 slot,
253 )?),
254 None => None,
255 };
256 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
257
258 let shared = Arc::new(SharedState::new(DEFAULT_QUEUE_CAPACITY));
261 shared.set_http_config(config.http.clone());
262 if let Some(prefix) = config
263 .nat64_prefixes
264 .iter()
265 .find(|prefix| prefix.prefix() != 96)
266 {
267 return Err(NetworkInitError::InvalidNat64Prefix {
268 raw: prefix.to_string(),
269 });
270 }
271 shared.set_nat64_prefixes(config.nat64_prefixes.clone());
272 if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
276 rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
277 }) {
278 return Err(NetworkInitError::EmptyNetworkRateLimiter);
279 }
280 config
281 .rate_limiter
282 .as_ref()
283 .and_then(|rate_limiter| rate_limiter.ingress.as_ref())
284 .map(RateLimiterConfig::validate)
285 .transpose()
286 .map_err(|source| NetworkInitError::InvalidRateLimit {
287 direction: NetworkRateLimitDirection::Ingress,
288 source,
289 })?;
290 config
291 .rate_limiter
292 .as_ref()
293 .and_then(|rate_limiter| rate_limiter.egress.as_ref())
294 .map(RateLimiterConfig::validate)
295 .transpose()
296 .map_err(|source| NetworkInitError::InvalidRateLimit {
297 direction: NetworkRateLimitDirection::Egress,
298 source,
299 })?;
300 let backend = SmoltcpBackend::new(shared.clone());
301
302 let secrets = SecretsHandle::new(config.secrets.clone());
303 let tls_state = if config.tls.enabled {
304 Some(Arc::new(TlsState::new(
305 config.tls.clone(),
306 secrets.clone(),
307 )?))
308 } else {
309 None
310 };
311
312 Ok(Self {
313 config: resolved_config,
314 platform_policy,
315 shared,
316 backend: Some(backend),
317 poll_handle: None,
318 activation_gate: None,
319 guest_mac,
320 gateway_mac,
321 mtu,
322 guest_ipv4,
323 gateway_ipv4,
324 guest_ipv6,
325 gateway_ipv6,
326 tls_state,
327 secrets,
328 })
329 }
330
331 pub fn defer_activation(&mut self) -> NetworkActivationHandle {
337 assert!(
338 self.poll_handle.is_none(),
339 "network activation can only be deferred before start"
340 );
341 let gate = Arc::new(NetworkActivationGate {
342 active: Mutex::new(false),
343 changed: Condvar::new(),
344 });
345 self.activation_gate = Some(Arc::clone(&gate));
346 NetworkActivationHandle { gate }
347 }
348
349 fn platform_policy(deployment_profile: DeploymentProfile) -> Option<NetworkPolicy> {
350 match deployment_profile {
351 DeploymentProfile::SingleTenant => None,
352 DeploymentProfile::MultiTenant => {
353 Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
354 }
355 }
356 }
357
358 fn gateway_ips(&self) -> GatewayIps {
360 GatewayIps {
361 ipv4: self.gateway_ipv4,
362 ipv6: self.gateway_ipv6,
363 }
364 }
365
366 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
371 let shared = self.shared.clone();
372 let poll_config = PollLoopConfig {
373 gateway_mac: self.gateway_mac,
374 guest_mac: self.guest_mac,
375 gateway: self.gateway_ips(),
376 guest_ipv4: self.guest_ipv4,
377 guest_ipv6: self.guest_ipv6,
378 mtu: self.mtu as usize,
379 };
380 let config = self.config.config();
381 let network_policy = config.policy.clone();
382 let platform_policy = self.platform_policy.clone();
383 let dns_config = config.dns.clone();
384 let tls_state = self.tls_state.clone();
385 let published_ports = config.ports.clone();
386 let strict = config.strict;
387 let max_tcp_connections = config.max_tcp_connections.and_then(ConnectionLimit::cap);
388 let max_udp_connections = config.max_udp_connections;
389 let tcp_accept_queue_size = config.tcp_accept_queue_size.unwrap_or_default();
390 let secrets = self.secrets.clone();
391 let activation_gate = self.activation_gate.take();
392 let outbound_proxy = self.config.outbound_proxy().cloned().map(Arc::new);
393
394 self.poll_handle = Some(
395 std::thread::Builder::new()
396 .name("smoltcp-poll".into())
397 .spawn(move || {
398 if let Some(gate) = activation_gate {
399 gate.wait();
400 }
401 poll::smoltcp_poll_loop(
402 shared,
403 poll_config,
404 network_policy,
405 platform_policy,
406 dns_config,
407 tls_state,
408 published_ports,
409 strict,
410 max_tcp_connections,
411 max_udp_connections,
412 tcp_accept_queue_size,
413 tokio_handle,
414 secrets,
415 outbound_proxy,
416 );
417 })
418 .expect("failed to spawn smoltcp poll thread"),
419 );
420 }
421
422 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
424 Box::new(self.backend.take().expect("backend already taken"))
425 }
426
427 pub fn guest_mac(&self) -> [u8; 6] {
429 self.guest_mac
430 }
431
432 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
437 let mut vars = vec![
438 (
439 ENV_NET.into(),
440 format!(
441 "iface=eth0,mac={},mtu={}",
442 format_mac(self.guest_mac),
443 self.mtu,
444 ),
445 ),
446 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
447 ];
448
449 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
450 vars.push((
451 ENV_NET_IPV4.into(),
452 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
453 ));
454 }
455
456 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
457 vars.push((
458 ENV_NET_IPV6.into(),
459 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
460 ));
461 }
462
463 for secret in &self.config.config().secrets.secrets {
465 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
466 }
467
468 vars
469 }
470
471 pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
473 BootstrapNetwork {
474 interface: "eth0".to_string(),
475 mac: self.guest_mac,
476 mtu: self.mtu,
477 ipv4: self
478 .guest_ipv4
479 .zip(self.gateway_ipv4)
480 .map(|(address, gateway)| BootstrapIpv4 {
481 address,
482 prefix_len: 30,
483 gateway,
484 dns: Some(gateway),
485 }),
486 ipv6: self
487 .guest_ipv6
488 .zip(self.gateway_ipv6)
489 .map(|(address, gateway)| BootstrapIpv6 {
490 address,
491 prefix_len: 64,
492 gateway,
493 dns: Some(gateway),
494 }),
495 }
496 }
497
498 pub fn guest_host_alias(&self) -> &'static str {
500 crate::HOST_ALIAS
501 }
502
503 pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
508 self.config
509 .config()
510 .secrets
511 .secrets
512 .iter()
513 .map(|secret| BootstrapEnvVar {
514 key: secret.env_var.clone(),
515 value: secret.placeholder.clone(),
516 })
517 .collect()
518 }
519
520 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
524 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
525 }
526
527 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
535 if !self.config.config().trust_host_cas {
536 return None;
537 }
538 crate::engine::tls::host_cas::collect_host_cas()
539 }
540
541 pub fn termination_handle(&self) -> TerminationHandle {
543 TerminationHandle {
544 shared: self.shared.clone(),
545 }
546 }
547
548 pub fn metrics_handle(&self) -> MetricsHandle {
550 MetricsHandle {
551 shared: self.shared.clone(),
552 }
553 }
554
555 pub fn secrets_handle(&self) -> SecretsHandle {
559 self.secrets.clone()
560 }
561}
562
563impl NetworkActivationHandle {
564 pub fn activate(&self) {
566 let mut active = self.gate.active.lock().unwrap();
567 if !*active {
568 *active = true;
569 self.gate.changed.notify_all();
570 }
571 }
572}
573
574impl NetworkActivationGate {
575 fn wait(&self) {
576 let mut active = self.active.lock().unwrap();
577 while !*active {
578 active = self.changed.wait(active).unwrap();
579 }
580 }
581}
582
583impl TerminationHandle {
584 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
586 self.shared.set_termination_hook(hook);
587 }
588}
589
590impl MetricsHandle {
591 pub fn tx_bytes(&self) -> u64 {
593 self.shared.tx_bytes()
594 }
595
596 pub fn rx_bytes(&self) -> u64 {
598 self.shared.rx_bytes()
599 }
600}
601
602fn enforce_deployment_profile(config: &mut ResolvedNetworkConfig, profile: DeploymentProfile) {
613 if profile == DeploymentProfile::SingleTenant {
614 return;
615 }
616
617 config.clear_outbound_proxy();
618
619 let config = config.config_mut();
620 config
621 .max_tcp_connections
622 .get_or_insert(ConnectionLimit::Limited(
623 DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS,
624 ));
625 config
626 .max_udp_connections
627 .get_or_insert(ConnectionLimit::Limited(
628 DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS,
629 ));
630 let interface_overridden = config.interface.mac.is_some()
631 || config.interface.mtu.is_some()
632 || config.interface.ipv4_address.is_some()
633 || config.interface.ipv4_pool.is_some()
634 || config.interface.ipv6_address.is_some()
635 || config.interface.ipv6_pool.is_some();
636 let had_published_ports = !config.ports.is_empty();
637 let had_custom_nameservers = !config.dns.nameservers.is_empty();
638 let disabled_rebind_protection = !config.dns.rebind_protection;
639 let trusted_host_cas = config.trust_host_cas;
640 let had_outbound_proxy = config.outbound_proxy.is_some();
641 config.interface = Default::default();
642 config.ports.clear();
643 config.dns.nameservers.clear();
644 config.dns.rebind_protection = true;
645 config.trust_host_cas = false;
646 if interface_overridden
647 || had_published_ports
648 || had_custom_nameservers
649 || disabled_rebind_protection
650 || trusted_host_cas
651 || had_outbound_proxy
652 {
653 tracing::warn!(
654 interface_overridden,
655 had_published_ports,
656 had_custom_nameservers,
657 disabled_rebind_protection,
658 trusted_host_cas,
659 had_outbound_proxy,
660 "multi-tenant deployment profile overrode unsafe network configuration"
661 );
662 }
663}
664
665fn derive_guest_mac(slot: u16) -> [u8; 6] {
669 let s = slot.to_be_bytes();
670 [0x02, 0x6d, 0x73, s[0], s[1], 0x02]
671}
672
673fn derive_gateway_mac(slot: u16) -> [u8; 6] {
677 let s = slot.to_be_bytes();
678 [0x02, 0x6d, 0x73, s[0], s[1], 0x01]
679}
680
681fn derive_guest_ipv4(pool: Ipv4Network, slot: u16) -> Result<Ipv4Addr, NetworkInitError> {
686 let capacity = 30_u8
687 .checked_sub(pool.prefix())
688 .map(|host_bits| 1_u32 << host_bits)
689 .ok_or(NetworkInitError::Ipv4PoolCapacity { pool, slot })?;
690 if u32::from(slot) >= capacity {
691 return Err(NetworkInitError::Ipv4PoolCapacity { pool, slot });
692 }
693
694 let base = u32::from(pool.network());
695 let offset = u32::from(slot) * 4 + 2; Ok(Ipv4Addr::from(base + offset))
697}
698
699fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
701 Ipv4Addr::from(u32::from(guest) - 1)
702}
703
704fn default_guest_ipv4_pool() -> Ipv4Network {
705 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
706 .expect("default IPv4 pool must be a valid network")
707}
708
709fn derive_guest_ipv6(pool: Ipv6Network, slot: u16) -> Result<Ipv6Addr, NetworkInitError> {
714 let capacity = 64_u8
715 .checked_sub(pool.prefix())
716 .map(|host_bits| 1_u128 << host_bits)
717 .ok_or(NetworkInitError::Ipv6PoolCapacity { pool, slot })?;
718 if u128::from(slot) >= capacity {
719 return Err(NetworkInitError::Ipv6PoolCapacity { pool, slot });
720 }
721
722 let base = u128::from(pool.network());
723 let offset = u128::from(slot) << 64;
724 Ok(Ipv6Addr::from(base + offset + 2))
725}
726
727fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
729 let segs = guest.segments();
730 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
731}
732
733fn default_guest_ipv6_pool() -> Ipv6Network {
734 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
735 .expect("default IPv6 pool must be a valid network")
736}
737
738fn format_mac(mac: [u8; 6]) -> String {
740 format!(
741 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
742 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
743 )
744}
745
746fn host_has_ipv4_route() -> bool {
752 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
753 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
754 .is_ok()
755}
756
757fn host_has_ipv6_route() -> bool {
761 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
762 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
763 .is_ok()
764}
765
766#[cfg(test)]
771mod tests {
772 use super::*;
773 use crate::config::{EnvNetworkSecretResolver, NetworkConfig, PortProtocol, PublishedPort};
774 use crate::dns::Nameserver;
775
776 fn resolved(config: NetworkConfig) -> ResolvedNetworkConfig {
777 config.resolve(&EnvNetworkSecretResolver).unwrap()
778 }
779
780 fn routes(ipv4: bool, ipv6: bool) -> HostRoutes {
781 HostRoutes { ipv4, ipv6 }
782 }
783
784 #[test]
785 fn captured_gateway_survives_new_slots_without_sharing_backends() {
786 let mut source_config = NetworkConfig::default();
787 source_config.interface.mac = Some([2, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]);
789 let source = SmoltcpNetwork::build(
790 resolved(source_config),
791 7,
792 DeploymentProfile::SingleTenant,
793 routes(true, true),
794 )
795 .unwrap();
796 let mut config = NetworkConfig::default();
797 config.interface.mac = Some(source.guest_mac);
798 config.interface.ipv4_address = source.guest_ipv4;
799 config.interface.ipv6_address = source.guest_ipv6;
800 for slot in [8, 400] {
801 let child = SmoltcpNetwork::build(
802 resolved(config.clone()),
803 slot,
804 DeploymentProfile::SingleTenant,
805 routes(true, true),
806 )
807 .unwrap()
808 .with_captured_gateway_mac(source.gateway_mac)
809 .unwrap();
810 assert_eq!(child.gateway_mac, source.gateway_mac);
811 assert_eq!(child.gateway_ipv4, source.gateway_ipv4);
812 assert_eq!(child.gateway_ipv6, source.gateway_ipv6);
813 assert_eq!(child.guest_mac, source.guest_mac);
814 assert!(!Arc::ptr_eq(&child.shared, &source.shared));
815 }
816 assert_eq!(source.gateway_mac, SmoltcpNetwork::default_gateway_mac(7));
817 }
818
819 #[test]
820 fn captured_gateway_rejects_unusable_ethernet_addresses() {
821 for mac in [[0; 6], [1, 2, 3, 4, 5, 6], derive_guest_mac(8)] {
822 let network = SmoltcpNetwork::build(
823 resolved(NetworkConfig::default()),
824 8,
825 DeploymentProfile::SingleTenant,
826 routes(true, true),
827 )
828 .unwrap();
829 assert!(matches!(
830 network.with_captured_gateway_mac(mac),
831 Err(NetworkInitError::InvalidGatewayMac)
832 ));
833 }
834 }
835
836 #[test]
837 fn deferred_activation_blocks_until_released() {
838 let mut network = SmoltcpNetwork::build(
839 resolved(NetworkConfig::default()),
840 0,
841 DeploymentProfile::SingleTenant,
842 routes(true, false),
843 )
844 .unwrap();
845 let handle = network.defer_activation();
846 let gate = Arc::clone(network.activation_gate.as_ref().unwrap());
847 let (released_tx, released_rx) = std::sync::mpsc::channel();
848 let waiter = std::thread::spawn(move || {
849 gate.wait();
850 released_tx.send(()).unwrap();
851 });
852
853 assert!(
854 released_rx
855 .recv_timeout(std::time::Duration::from_millis(25))
856 .is_err(),
857 "deferred network became active before explicit release"
858 );
859 handle.activate();
860 released_rx
861 .recv_timeout(std::time::Duration::from_secs(1))
862 .unwrap();
863 waiter.join().unwrap();
864 }
865
866 #[test]
867 fn derive_addresses_slot_0() {
868 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
869 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
870 assert_eq!(
871 derive_guest_ipv4(default_guest_ipv4_pool(), 0).unwrap(),
872 Ipv4Addr::new(172, 16, 0, 2)
873 );
874 assert_eq!(
875 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
876 Ipv4Addr::new(172, 16, 0, 1)
877 );
878 }
879
880 #[test]
881 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
882 let mut config = NetworkConfig::default();
883 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
884 config.interface.mtu = Some(9000);
885 config.ports.push(PublishedPort {
886 host_port: 8080,
887 guest_port: 80,
888 protocol: PortProtocol::Tcp,
889 host_bind: Ipv4Addr::UNSPECIFIED.into(),
890 });
891 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
892 config.dns.rebind_protection = false;
893 config.trust_host_cas = true;
894 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
895 address: "127.0.0.1:1080".parse().unwrap(),
896 credentials: None,
897 });
898 config.max_tcp_connections = Some(ConnectionLimit::from(257));
899 config.policy = NetworkPolicy::allow_all();
900 let mut resolved = resolved(config);
901
902 enforce_deployment_profile(&mut resolved, DeploymentProfile::MultiTenant);
903 let config = resolved.config();
904
905 assert!(config.interface.mac.is_none());
906 assert!(config.interface.mtu.is_none());
907 assert!(config.ports.is_empty());
908 assert!(config.dns.nameservers.is_empty());
909 assert!(config.dns.rebind_protection);
910 assert!(!config.trust_host_cas);
911 assert!(config.outbound_proxy.is_none());
912 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::from(257)));
913 assert!(resolved.config().outbound_proxy.is_none());
914 assert!(resolved.outbound_proxy().is_none());
915 assert!(config.policy.default_egress.is_allow());
918 }
919
920 #[test]
921 fn deployment_profile_defaults_and_explicit_connection_limits() {
922 for profile in [
923 DeploymentProfile::SingleTenant,
924 DeploymentProfile::MultiTenant,
925 ] {
926 for requested in [None, Some(0), Some(64), Some(4096)] {
927 let config: NetworkConfig =
928 serde_json::from_value(serde_json::json!({"max_tcp_connections": requested}))
929 .unwrap();
930 let mut config = resolved(config);
931 config = serde_json::from_value(serde_json::to_value(config).unwrap()).unwrap();
933 enforce_deployment_profile(&mut config, profile);
934 let expected = requested
935 .or(match profile {
936 DeploymentProfile::SingleTenant => None,
937 DeploymentProfile::MultiTenant => Some(1024),
938 })
939 .and_then(NonZeroUsize::new);
940 assert_eq!(
941 config
942 .config()
943 .max_tcp_connections
944 .and_then(ConnectionLimit::cap),
945 expected
946 );
947 enforce_deployment_profile(&mut config, profile);
949 assert_eq!(
950 config
951 .config()
952 .max_tcp_connections
953 .and_then(ConnectionLimit::cap),
954 expected
955 );
956 }
957 }
958 }
959
960 #[test]
961 fn deployment_profiles_resolve_udp_defaults_and_preserve_overrides() {
962 for profile in [
963 DeploymentProfile::SingleTenant,
964 DeploymentProfile::MultiTenant,
965 ] {
966 for requested in [None, Some(0), Some(7), Some(4096)] {
967 let config: NetworkConfig = serde_json::from_value(serde_json::json!({
968 "max_udp_connections": requested
969 }))
970 .unwrap();
971 let mut config = resolved(config);
972 let expected = requested
973 .or(match profile {
974 DeploymentProfile::SingleTenant => None,
975 DeploymentProfile::MultiTenant => Some(1024),
976 })
977 .map(ConnectionLimit::from);
978 enforce_deployment_profile(&mut config, profile);
979 assert_eq!(config.config().max_udp_connections, expected);
980 enforce_deployment_profile(&mut config, profile);
981 assert_eq!(config.config().max_udp_connections, expected);
982 }
983 }
984 }
985
986 #[test]
987 fn single_tenant_profile_preserves_requested_network_controls() {
988 let mut config = NetworkConfig::default();
989 config.interface.mtu = Some(9000);
990 config.dns.rebind_protection = false;
991 config.trust_host_cas = true;
992 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
993 address: "127.0.0.1:1080".parse().unwrap(),
994 credentials: None,
995 });
996
997 let mut resolved = resolved(config);
998 enforce_deployment_profile(&mut resolved, DeploymentProfile::SingleTenant);
999 let config = resolved.config();
1000
1001 assert_eq!(config.interface.mtu, Some(9000));
1002 assert!(!config.dns.rebind_protection);
1003 assert!(config.trust_host_cas);
1004 assert!(config.outbound_proxy.is_some());
1005 }
1006
1007 #[test]
1008 fn derive_addresses_slot_1() {
1009 assert_eq!(
1010 derive_guest_ipv4(default_guest_ipv4_pool(), 1).unwrap(),
1011 Ipv4Addr::new(172, 16, 0, 6)
1012 );
1013 assert_eq!(
1014 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
1015 Ipv4Addr::new(172, 16, 0, 5)
1016 );
1017 }
1018
1019 #[test]
1020 fn derive_addresses_max_slot() {
1021 assert_eq!(
1022 derive_guest_mac(u16::MAX),
1023 [0x02, 0x6d, 0x73, 0xff, 0xff, 0x02]
1024 );
1025 assert_eq!(
1026 derive_guest_ipv4(default_guest_ipv4_pool(), u16::MAX).unwrap(),
1027 Ipv4Addr::new(172, 19, 255, 254)
1028 );
1029 assert_eq!(
1030 derive_guest_ipv6(default_guest_ipv6_pool(), u16::MAX).unwrap(),
1031 "fd42:6d73:62:ffff::2".parse::<Ipv6Addr>().unwrap()
1032 );
1033 }
1034
1035 #[test]
1036 fn derive_addresses_custom_ipv4_pool() {
1037 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
1038 assert_eq!(
1039 derive_guest_ipv4(pool, 0).unwrap(),
1040 Ipv4Addr::new(172, 31, 240, 2)
1041 );
1042 assert_eq!(
1043 derive_guest_ipv4(pool, 63).unwrap(),
1044 Ipv4Addr::new(172, 31, 240, 254)
1045 );
1046 }
1047
1048 #[test]
1049 fn custom_ipv4_pool_capacity_is_a_typed_error() {
1050 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
1051 assert!(matches!(
1052 derive_guest_ipv4(pool, 64),
1053 Err(NetworkInitError::Ipv4PoolCapacity { slot: 64, .. })
1054 ));
1055
1056 let pool = "172.31.240.0/31".parse::<Ipv4Network>().unwrap();
1057 assert!(matches!(
1058 derive_guest_ipv4(pool, 0),
1059 Err(NetworkInitError::Ipv4PoolCapacity { slot: 0, .. })
1060 ));
1061 }
1062
1063 #[test]
1064 fn derive_ipv6_slot_0() {
1065 assert_eq!(
1066 derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap(),
1067 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
1068 );
1069 assert_eq!(
1070 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap()),
1071 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
1072 );
1073 }
1074
1075 #[test]
1076 fn derive_addresses_custom_ipv6_pool() {
1077 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
1078 assert_eq!(
1079 derive_guest_ipv6(pool, 0).unwrap(),
1080 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
1081 );
1082 assert_eq!(
1083 derive_guest_ipv6(pool, 3).unwrap(),
1084 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
1085 );
1086 }
1087
1088 #[test]
1089 fn custom_ipv6_pool_capacity_is_a_typed_error() {
1090 let pool = "fd7a:115c:a1e0:100::/62".parse::<Ipv6Network>().unwrap();
1091 assert!(matches!(
1092 derive_guest_ipv6(pool, 4),
1093 Err(NetworkInitError::Ipv6PoolCapacity { slot: 4, .. })
1094 ));
1095
1096 let pool = "fd7a:115c:a1e0:100::/65".parse::<Ipv6Network>().unwrap();
1097 assert!(matches!(
1098 derive_guest_ipv6(pool, 0),
1099 Err(NetworkInitError::Ipv6PoolCapacity { slot: 0, .. })
1100 ));
1101 }
1102
1103 #[test]
1104 fn format_mac_address() {
1105 assert_eq!(
1106 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
1107 "02:6d:73:00:00:01"
1108 );
1109 }
1110
1111 #[test]
1112 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
1113 let net = SmoltcpNetwork::build(
1114 resolved(NetworkConfig::default()),
1115 0,
1116 DeploymentProfile::SingleTenant,
1117 routes(true, false),
1118 )
1119 .unwrap();
1120 let vars = net.guest_env_vars();
1121
1122 assert_eq!(vars.len(), 3);
1123 assert_eq!(vars[0].0, ENV_NET);
1124 assert!(vars[0].1.contains("iface=eth0"));
1125 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1126 assert_eq!(vars[1].1, crate::HOST_ALIAS);
1127 assert_eq!(vars[2].0, ENV_NET_IPV4);
1128 assert!(vars[2].1.contains("/30"));
1129 }
1130
1131 #[test]
1132 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
1133 let net = SmoltcpNetwork::build(
1134 resolved(NetworkConfig::default()),
1135 0,
1136 DeploymentProfile::SingleTenant,
1137 routes(true, true),
1138 )
1139 .unwrap();
1140 let vars = net.guest_env_vars();
1141
1142 assert_eq!(vars.len(), 4);
1143 assert_eq!(vars[0].0, ENV_NET);
1144 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1145 assert_eq!(vars[2].0, ENV_NET_IPV4);
1146 assert_eq!(vars[3].0, ENV_NET_IPV6);
1147 assert!(vars[3].1.contains("/64"));
1148 }
1149
1150 #[test]
1151 fn guest_env_vars_omit_ipv6_without_host_route() {
1152 let net = SmoltcpNetwork::build(
1153 resolved(NetworkConfig::default()),
1154 0,
1155 DeploymentProfile::SingleTenant,
1156 routes(true, false),
1157 )
1158 .unwrap();
1159 let vars = net.guest_env_vars();
1160
1161 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
1162 }
1163
1164 #[test]
1165 fn guest_env_vars_omit_ipv4_without_host_route() {
1166 let net = SmoltcpNetwork::build(
1167 resolved(NetworkConfig::default()),
1168 0,
1169 DeploymentProfile::SingleTenant,
1170 routes(false, true),
1171 )
1172 .unwrap();
1173 let vars = net.guest_env_vars();
1174
1175 assert_eq!(vars.len(), 3);
1176 assert_eq!(vars[0].0, ENV_NET);
1177 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1178 assert_eq!(vars[2].0, ENV_NET_IPV6);
1179 }
1180
1181 #[test]
1182 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
1183 let mut config = NetworkConfig::default();
1184 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
1185 let net = SmoltcpNetwork::build(
1186 resolved(config),
1187 0,
1188 DeploymentProfile::SingleTenant,
1189 routes(true, false),
1190 )
1191 .unwrap();
1192 let vars = net.guest_env_vars();
1193
1194 let v6 = vars
1195 .iter()
1196 .find(|(k, _)| k == ENV_NET_IPV6)
1197 .expect("explicit ipv6 should publish env var even without host route");
1198 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
1199 }
1200
1201 #[test]
1202 fn neither_family_active_emits_only_base_env_vars() {
1203 let net = SmoltcpNetwork::build(
1204 resolved(NetworkConfig::default()),
1205 0,
1206 DeploymentProfile::SingleTenant,
1207 routes(false, false),
1208 )
1209 .unwrap();
1210 let vars = net.guest_env_vars();
1211
1212 assert_eq!(vars.len(), 2);
1213 assert_eq!(vars[0].0, ENV_NET);
1214 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1215 }
1216
1217 #[test]
1218 fn guest_bootstrap_network_preserves_active_address_families() {
1219 let net = SmoltcpNetwork::build(
1220 resolved(NetworkConfig::default()),
1221 7,
1222 DeploymentProfile::SingleTenant,
1223 routes(true, true),
1224 )
1225 .unwrap();
1226
1227 let bootstrap = net.guest_bootstrap_network();
1228
1229 assert_eq!(bootstrap.interface, "eth0");
1230 assert_eq!(bootstrap.mac, net.guest_mac());
1231 assert_eq!(bootstrap.mtu, 1500);
1232 assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
1233 assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
1234 assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
1235 }
1236
1237 #[test]
1238 fn guest_bootstrap_network_allows_no_active_address_family() {
1239 let net = SmoltcpNetwork::build(
1240 resolved(NetworkConfig::default()),
1241 0,
1242 DeploymentProfile::SingleTenant,
1243 routes(false, false),
1244 )
1245 .unwrap();
1246
1247 let bootstrap = net.guest_bootstrap_network();
1248
1249 assert!(bootstrap.ipv4.is_none());
1250 assert!(bootstrap.ipv6.is_none());
1251 }
1252
1253 #[test]
1254 fn large_connection_cap_does_not_preallocate_or_prevent_startup() {
1255 for limit in [10000, usize::MAX] {
1256 let mut config = NetworkConfig::default();
1257 config.tls.enabled = false;
1258 config.max_tcp_connections = Some(ConnectionLimit::from(limit));
1259 let net = SmoltcpNetwork::build(
1260 resolved(config),
1261 0,
1262 DeploymentProfile::MultiTenant,
1263 routes(true, false),
1264 );
1265 assert!(net.is_ok(), "large explicit cap should allow startup");
1266 }
1267 }
1268
1269 #[test]
1272 fn build_rejects_invalid_rate_limiter() {
1273 let mut config = NetworkConfig {
1274 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
1275 egress: None,
1276 ingress: Some(microsandbox_types::RateLimiterConfig {
1277 bandwidth: None,
1278 ops: None,
1279 }),
1280 }),
1281 ..NetworkConfig::default()
1282 };
1283 config.tls.enabled = false;
1284
1285 let err = match SmoltcpNetwork::build(
1286 resolved(config),
1287 0,
1288 DeploymentProfile::SingleTenant,
1289 routes(true, false),
1290 ) {
1291 Ok(_) => panic!("empty rate limiter should fail"),
1292 Err(err) => err,
1293 };
1294
1295 assert!(matches!(
1296 err,
1297 NetworkInitError::InvalidRateLimit {
1298 direction: NetworkRateLimitDirection::Ingress,
1299 source: RateLimitConfigError::EmptyLimiter,
1300 }
1301 ));
1302 }
1303}