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, PortProtocol, 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 no_guest_address = !host_routes.ipv4
238 && !host_routes.ipv6
239 && config.interface.ipv4_address.is_none()
240 && config.interface.ipv6_address.is_none();
241 let published_ipv4 = no_guest_address
242 && config
243 .ports
244 .iter()
245 .any(|port| port.protocol == PortProtocol::Tcp || port.host_bind.is_ipv4());
246 let published_ipv6 = no_guest_address
247 && config
248 .ports
249 .iter()
250 .any(|port| port.protocol == PortProtocol::Udp && port.host_bind.is_ipv6());
251
252 let guest_ipv4 = match config.interface.ipv4_address {
253 Some(address) => Some(address),
254 None if host_routes.ipv4 || published_ipv4 => Some(derive_guest_ipv4(
255 config
256 .interface
257 .ipv4_pool
258 .unwrap_or_else(default_guest_ipv4_pool),
259 slot,
260 )?),
261 None => None,
262 };
263 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
264 let guest_ipv6 = match config.interface.ipv6_address {
265 Some(address) => Some(address),
266 None if host_routes.ipv6 || published_ipv6 => Some(derive_guest_ipv6(
267 config
268 .interface
269 .ipv6_pool
270 .unwrap_or_else(default_guest_ipv6_pool),
271 slot,
272 )?),
273 None => None,
274 };
275 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
276
277 let shared = Arc::new(SharedState::new(DEFAULT_QUEUE_CAPACITY));
280 shared.set_http_config(config.http.clone());
281 if let Some(prefix) = config
282 .nat64_prefixes
283 .iter()
284 .find(|prefix| prefix.prefix() != 96)
285 {
286 return Err(NetworkInitError::InvalidNat64Prefix {
287 raw: prefix.to_string(),
288 });
289 }
290 shared.set_nat64_prefixes(config.nat64_prefixes.clone());
291 if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
295 rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
296 }) {
297 return Err(NetworkInitError::EmptyNetworkRateLimiter);
298 }
299 config
300 .rate_limiter
301 .as_ref()
302 .and_then(|rate_limiter| rate_limiter.ingress.as_ref())
303 .map(RateLimiterConfig::validate)
304 .transpose()
305 .map_err(|source| NetworkInitError::InvalidRateLimit {
306 direction: NetworkRateLimitDirection::Ingress,
307 source,
308 })?;
309 config
310 .rate_limiter
311 .as_ref()
312 .and_then(|rate_limiter| rate_limiter.egress.as_ref())
313 .map(RateLimiterConfig::validate)
314 .transpose()
315 .map_err(|source| NetworkInitError::InvalidRateLimit {
316 direction: NetworkRateLimitDirection::Egress,
317 source,
318 })?;
319 let backend = SmoltcpBackend::new(shared.clone());
320
321 let secrets = SecretsHandle::new(config.secrets.clone());
322 let tls_state = if config.tls.enabled {
323 Some(Arc::new(TlsState::new(
324 config.tls.clone(),
325 secrets.clone(),
326 )?))
327 } else {
328 None
329 };
330
331 Ok(Self {
332 config: resolved_config,
333 platform_policy,
334 shared,
335 backend: Some(backend),
336 poll_handle: None,
337 activation_gate: None,
338 guest_mac,
339 gateway_mac,
340 mtu,
341 guest_ipv4,
342 gateway_ipv4,
343 guest_ipv6,
344 gateway_ipv6,
345 tls_state,
346 secrets,
347 })
348 }
349
350 pub fn defer_activation(&mut self) -> NetworkActivationHandle {
356 assert!(
357 self.poll_handle.is_none(),
358 "network activation can only be deferred before start"
359 );
360 let gate = Arc::new(NetworkActivationGate {
361 active: Mutex::new(false),
362 changed: Condvar::new(),
363 });
364 self.activation_gate = Some(Arc::clone(&gate));
365 NetworkActivationHandle { gate }
366 }
367
368 fn platform_policy(deployment_profile: DeploymentProfile) -> Option<NetworkPolicy> {
369 match deployment_profile {
370 DeploymentProfile::SingleTenant => None,
371 DeploymentProfile::MultiTenant => {
372 Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
373 }
374 }
375 }
376
377 fn gateway_ips(&self) -> GatewayIps {
379 GatewayIps {
380 ipv4: self.gateway_ipv4,
381 ipv6: self.gateway_ipv6,
382 }
383 }
384
385 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
390 let shared = self.shared.clone();
391 let poll_config = PollLoopConfig {
392 gateway_mac: self.gateway_mac,
393 guest_mac: self.guest_mac,
394 gateway: self.gateway_ips(),
395 guest_ipv4: self.guest_ipv4,
396 guest_ipv6: self.guest_ipv6,
397 mtu: self.mtu as usize,
398 };
399 let config = self.config.config();
400 let network_policy = config.policy.clone();
401 let platform_policy = self.platform_policy.clone();
402 let dns_config = config.dns.clone();
403 let tls_state = self.tls_state.clone();
404 let published_ports = config.ports.clone();
405 let strict = config.strict;
406 let max_tcp_connections = config.max_tcp_connections.and_then(ConnectionLimit::cap);
407 let max_udp_connections = config.max_udp_connections;
408 let max_inbound_tcp_connections = config.max_inbound_tcp_connections;
409 let tcp_accept_queue_size = config.tcp_accept_queue_size.unwrap_or_default();
410 let secrets = self.secrets.clone();
411 let activation_gate = self.activation_gate.take();
412 let outbound_proxy = self.config.outbound_proxy().cloned().map(Arc::new);
413
414 self.poll_handle = Some(
415 std::thread::Builder::new()
416 .name("smoltcp-poll".into())
417 .spawn(move || {
418 if let Some(gate) = activation_gate {
419 gate.wait();
420 }
421 poll::smoltcp_poll_loop(
422 shared,
423 poll_config,
424 network_policy,
425 platform_policy,
426 dns_config,
427 tls_state,
428 published_ports,
429 strict,
430 max_tcp_connections,
431 max_udp_connections,
432 tcp_accept_queue_size,
433 max_inbound_tcp_connections,
434 tokio_handle,
435 secrets,
436 outbound_proxy,
437 );
438 })
439 .expect("failed to spawn smoltcp poll thread"),
440 );
441 }
442
443 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
445 Box::new(self.backend.take().expect("backend already taken"))
446 }
447
448 pub fn guest_mac(&self) -> [u8; 6] {
450 self.guest_mac
451 }
452
453 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
458 let mut vars = vec![
459 (
460 ENV_NET.into(),
461 format!(
462 "iface=eth0,mac={},mtu={}",
463 format_mac(self.guest_mac),
464 self.mtu,
465 ),
466 ),
467 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
468 ];
469
470 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
471 vars.push((
472 ENV_NET_IPV4.into(),
473 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
474 ));
475 }
476
477 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
478 vars.push((
479 ENV_NET_IPV6.into(),
480 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
481 ));
482 }
483
484 for secret in &self.config.config().secrets.secrets {
486 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
487 }
488
489 vars
490 }
491
492 pub fn guest_bootstrap_network(&self) -> BootstrapNetwork {
494 BootstrapNetwork {
495 interface: "eth0".to_string(),
496 mac: self.guest_mac,
497 mtu: self.mtu,
498 ipv4: self
499 .guest_ipv4
500 .zip(self.gateway_ipv4)
501 .map(|(address, gateway)| BootstrapIpv4 {
502 address,
503 prefix_len: 30,
504 gateway,
505 dns: Some(gateway),
506 }),
507 ipv6: self
508 .guest_ipv6
509 .zip(self.gateway_ipv6)
510 .map(|(address, gateway)| BootstrapIpv6 {
511 address,
512 prefix_len: 64,
513 gateway,
514 dns: Some(gateway),
515 }),
516 }
517 }
518
519 pub fn guest_host_alias(&self) -> &'static str {
521 crate::HOST_ALIAS
522 }
523
524 pub fn guest_secret_env(&self) -> Vec<BootstrapEnvVar> {
529 self.config
530 .config()
531 .secrets
532 .secrets
533 .iter()
534 .map(|secret| BootstrapEnvVar {
535 key: secret.env_var.clone(),
536 value: secret.placeholder.clone(),
537 })
538 .collect()
539 }
540
541 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
545 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
546 }
547
548 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
556 if !self.config.config().trust_host_cas {
557 return None;
558 }
559 crate::engine::tls::host_cas::collect_host_cas()
560 }
561
562 pub fn termination_handle(&self) -> TerminationHandle {
564 TerminationHandle {
565 shared: self.shared.clone(),
566 }
567 }
568
569 pub fn metrics_handle(&self) -> MetricsHandle {
571 MetricsHandle {
572 shared: self.shared.clone(),
573 }
574 }
575
576 pub fn secrets_handle(&self) -> SecretsHandle {
580 self.secrets.clone()
581 }
582}
583
584impl NetworkActivationHandle {
585 pub fn activate(&self) {
587 let mut active = self.gate.active.lock().unwrap();
588 if !*active {
589 *active = true;
590 self.gate.changed.notify_all();
591 }
592 }
593}
594
595impl NetworkActivationGate {
596 fn wait(&self) {
597 let mut active = self.active.lock().unwrap();
598 while !*active {
599 active = self.changed.wait(active).unwrap();
600 }
601 }
602}
603
604impl TerminationHandle {
605 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
607 self.shared.set_termination_hook(hook);
608 }
609}
610
611impl MetricsHandle {
612 pub fn tx_bytes(&self) -> u64 {
614 self.shared.tx_bytes()
615 }
616
617 pub fn rx_bytes(&self) -> u64 {
619 self.shared.rx_bytes()
620 }
621}
622
623fn enforce_deployment_profile(config: &mut ResolvedNetworkConfig, profile: DeploymentProfile) {
634 if profile == DeploymentProfile::SingleTenant {
635 return;
636 }
637
638 config.clear_outbound_proxy();
639
640 let config = config.config_mut();
641 config
642 .max_tcp_connections
643 .get_or_insert(ConnectionLimit::Limited(
644 DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS,
645 ));
646 config
647 .max_udp_connections
648 .get_or_insert(ConnectionLimit::Limited(
649 DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS,
650 ));
651 let interface_overridden = config.interface.mac.is_some()
652 || config.interface.mtu.is_some()
653 || config.interface.ipv4_address.is_some()
654 || config.interface.ipv4_pool.is_some()
655 || config.interface.ipv6_address.is_some()
656 || config.interface.ipv6_pool.is_some();
657 let had_published_ports = !config.ports.is_empty();
658 let had_custom_nameservers = !config.dns.nameservers.is_empty();
659 let disabled_rebind_protection = !config.dns.rebind_protection;
660 let trusted_host_cas = config.trust_host_cas;
661 let had_outbound_proxy = config.outbound_proxy.is_some();
662 config.interface = Default::default();
663 config.ports.clear();
664 config.dns.nameservers.clear();
665 config.dns.rebind_protection = true;
666 config.trust_host_cas = false;
667 if interface_overridden
668 || had_published_ports
669 || had_custom_nameservers
670 || disabled_rebind_protection
671 || trusted_host_cas
672 || had_outbound_proxy
673 {
674 tracing::warn!(
675 interface_overridden,
676 had_published_ports,
677 had_custom_nameservers,
678 disabled_rebind_protection,
679 trusted_host_cas,
680 had_outbound_proxy,
681 "multi-tenant deployment profile overrode unsafe network configuration"
682 );
683 }
684}
685
686fn derive_guest_mac(slot: u16) -> [u8; 6] {
690 let s = slot.to_be_bytes();
691 [0x02, 0x6d, 0x73, s[0], s[1], 0x02]
692}
693
694fn derive_gateway_mac(slot: u16) -> [u8; 6] {
698 let s = slot.to_be_bytes();
699 [0x02, 0x6d, 0x73, s[0], s[1], 0x01]
700}
701
702fn derive_guest_ipv4(pool: Ipv4Network, slot: u16) -> Result<Ipv4Addr, NetworkInitError> {
707 let capacity = 30_u8
708 .checked_sub(pool.prefix())
709 .map(|host_bits| 1_u32 << host_bits)
710 .ok_or(NetworkInitError::Ipv4PoolCapacity { pool, slot })?;
711 if u32::from(slot) >= capacity {
712 return Err(NetworkInitError::Ipv4PoolCapacity { pool, slot });
713 }
714
715 let base = u32::from(pool.network());
716 let offset = u32::from(slot) * 4 + 2; Ok(Ipv4Addr::from(base + offset))
718}
719
720fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
722 Ipv4Addr::from(u32::from(guest) - 1)
723}
724
725fn default_guest_ipv4_pool() -> Ipv4Network {
726 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
727 .expect("default IPv4 pool must be a valid network")
728}
729
730fn derive_guest_ipv6(pool: Ipv6Network, slot: u16) -> Result<Ipv6Addr, NetworkInitError> {
735 let capacity = 64_u8
736 .checked_sub(pool.prefix())
737 .map(|host_bits| 1_u128 << host_bits)
738 .ok_or(NetworkInitError::Ipv6PoolCapacity { pool, slot })?;
739 if u128::from(slot) >= capacity {
740 return Err(NetworkInitError::Ipv6PoolCapacity { pool, slot });
741 }
742
743 let base = u128::from(pool.network());
744 let offset = u128::from(slot) << 64;
745 Ok(Ipv6Addr::from(base + offset + 2))
746}
747
748fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
750 let segs = guest.segments();
751 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
752}
753
754fn default_guest_ipv6_pool() -> Ipv6Network {
755 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
756 .expect("default IPv6 pool must be a valid network")
757}
758
759fn format_mac(mac: [u8; 6]) -> String {
761 format!(
762 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
763 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
764 )
765}
766
767fn host_has_ipv4_route() -> bool {
773 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
774 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
775 .is_ok()
776}
777
778fn host_has_ipv6_route() -> bool {
782 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
783 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
784 .is_ok()
785}
786
787#[cfg(test)]
792#[path = "network/test_support.rs"]
793mod test_support;
794
795#[cfg(test)]
796#[path = "network/tcp_connect_tests.rs"]
797mod tcp_connect_tests;
798
799#[cfg(test)]
800#[path = "network/published_ports_tests.rs"]
801mod published_ports_tests;
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use crate::config::{EnvNetworkSecretResolver, NetworkConfig, PortProtocol, PublishedPort};
807 use crate::dns::Nameserver;
808
809 fn resolved(config: NetworkConfig) -> ResolvedNetworkConfig {
810 config.resolve(&EnvNetworkSecretResolver).unwrap()
811 }
812
813 fn routes(ipv4: bool, ipv6: bool) -> HostRoutes {
814 HostRoutes { ipv4, ipv6 }
815 }
816
817 #[test]
818 fn captured_gateway_survives_new_slots_without_sharing_backends() {
819 let mut source_config = NetworkConfig::default();
820 source_config.interface.mac = Some([2, 0xaa, 0xbb, 0xcc, 0xdd, 0xee]);
822 let source = SmoltcpNetwork::build(
823 resolved(source_config),
824 7,
825 DeploymentProfile::SingleTenant,
826 routes(true, true),
827 )
828 .unwrap();
829 let mut config = NetworkConfig::default();
830 config.interface.mac = Some(source.guest_mac);
831 config.interface.ipv4_address = source.guest_ipv4;
832 config.interface.ipv6_address = source.guest_ipv6;
833 for slot in [8, 400] {
834 let child = SmoltcpNetwork::build(
835 resolved(config.clone()),
836 slot,
837 DeploymentProfile::SingleTenant,
838 routes(true, true),
839 )
840 .unwrap()
841 .with_captured_gateway_mac(source.gateway_mac)
842 .unwrap();
843 assert_eq!(child.gateway_mac, source.gateway_mac);
844 assert_eq!(child.gateway_ipv4, source.gateway_ipv4);
845 assert_eq!(child.gateway_ipv6, source.gateway_ipv6);
846 assert_eq!(child.guest_mac, source.guest_mac);
847 assert!(!Arc::ptr_eq(&child.shared, &source.shared));
848 }
849 assert_eq!(source.gateway_mac, SmoltcpNetwork::default_gateway_mac(7));
850 }
851
852 #[test]
853 fn captured_gateway_rejects_unusable_ethernet_addresses() {
854 for mac in [[0; 6], [1, 2, 3, 4, 5, 6], derive_guest_mac(8)] {
855 let network = SmoltcpNetwork::build(
856 resolved(NetworkConfig::default()),
857 8,
858 DeploymentProfile::SingleTenant,
859 routes(true, true),
860 )
861 .unwrap();
862 assert!(matches!(
863 network.with_captured_gateway_mac(mac),
864 Err(NetworkInitError::InvalidGatewayMac)
865 ));
866 }
867 }
868
869 #[test]
870 fn deferred_activation_blocks_until_released() {
871 let mut network = SmoltcpNetwork::build(
872 resolved(NetworkConfig::default()),
873 0,
874 DeploymentProfile::SingleTenant,
875 routes(true, false),
876 )
877 .unwrap();
878 let handle = network.defer_activation();
879 let gate = Arc::clone(network.activation_gate.as_ref().unwrap());
880 let (released_tx, released_rx) = std::sync::mpsc::channel();
881 let waiter = std::thread::spawn(move || {
882 gate.wait();
883 released_tx.send(()).unwrap();
884 });
885
886 assert!(
887 released_rx
888 .recv_timeout(std::time::Duration::from_millis(25))
889 .is_err(),
890 "deferred network became active before explicit release"
891 );
892 handle.activate();
893 released_rx
894 .recv_timeout(std::time::Duration::from_secs(1))
895 .unwrap();
896 waiter.join().unwrap();
897 }
898
899 #[test]
900 fn derive_addresses_slot_0() {
901 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
902 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
903 assert_eq!(
904 derive_guest_ipv4(default_guest_ipv4_pool(), 0).unwrap(),
905 Ipv4Addr::new(172, 16, 0, 2)
906 );
907 assert_eq!(
908 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
909 Ipv4Addr::new(172, 16, 0, 1)
910 );
911 }
912
913 #[test]
914 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
915 let mut config = NetworkConfig::default();
916 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
917 config.interface.mtu = Some(9000);
918 config.ports.push(PublishedPort {
919 host_port: 8080,
920 guest_port: 80,
921 protocol: PortProtocol::Tcp,
922 host_bind: Ipv4Addr::UNSPECIFIED.into(),
923 });
924 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
925 config.dns.rebind_protection = false;
926 config.trust_host_cas = true;
927 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
928 address: "127.0.0.1:1080".parse().unwrap(),
929 credentials: None,
930 });
931 config.max_tcp_connections = Some(ConnectionLimit::from(257));
932 config.policy = NetworkPolicy::allow_all();
933 let mut resolved = resolved(config);
934
935 enforce_deployment_profile(&mut resolved, DeploymentProfile::MultiTenant);
936 let config = resolved.config();
937
938 assert!(config.interface.mac.is_none());
939 assert!(config.interface.mtu.is_none());
940 assert!(config.ports.is_empty());
941 assert!(config.dns.nameservers.is_empty());
942 assert!(config.dns.rebind_protection);
943 assert!(!config.trust_host_cas);
944 assert!(config.outbound_proxy.is_none());
945 assert_eq!(config.max_tcp_connections, Some(ConnectionLimit::from(257)));
946 assert!(resolved.config().outbound_proxy.is_none());
947 assert!(resolved.outbound_proxy().is_none());
948 assert!(config.policy.default_egress.is_allow());
951 }
952
953 #[test]
954 fn deployment_profile_defaults_and_explicit_connection_limits() {
955 for profile in [
956 DeploymentProfile::SingleTenant,
957 DeploymentProfile::MultiTenant,
958 ] {
959 for requested in [None, Some(0), Some(64), Some(4096)] {
960 let config: NetworkConfig =
961 serde_json::from_value(serde_json::json!({"max_tcp_connections": requested}))
962 .unwrap();
963 let mut config = resolved(config);
964 config = serde_json::from_value(serde_json::to_value(config).unwrap()).unwrap();
966 enforce_deployment_profile(&mut config, profile);
967 let expected = requested
968 .or(match profile {
969 DeploymentProfile::SingleTenant => None,
970 DeploymentProfile::MultiTenant => Some(1024),
971 })
972 .and_then(NonZeroUsize::new);
973 assert_eq!(
974 config
975 .config()
976 .max_tcp_connections
977 .and_then(ConnectionLimit::cap),
978 expected
979 );
980 enforce_deployment_profile(&mut config, profile);
982 assert_eq!(
983 config
984 .config()
985 .max_tcp_connections
986 .and_then(ConnectionLimit::cap),
987 expected
988 );
989 }
990 }
991 }
992
993 #[test]
994 fn deployment_profiles_resolve_udp_defaults_and_preserve_overrides() {
995 for profile in [
996 DeploymentProfile::SingleTenant,
997 DeploymentProfile::MultiTenant,
998 ] {
999 for requested in [None, Some(0), Some(7), Some(4096)] {
1000 let config: NetworkConfig = serde_json::from_value(serde_json::json!({
1001 "max_udp_connections": requested
1002 }))
1003 .unwrap();
1004 let mut config = resolved(config);
1005 let expected = requested
1006 .or(match profile {
1007 DeploymentProfile::SingleTenant => None,
1008 DeploymentProfile::MultiTenant => Some(1024),
1009 })
1010 .map(ConnectionLimit::from);
1011 enforce_deployment_profile(&mut config, profile);
1012 assert_eq!(config.config().max_udp_connections, expected);
1013 enforce_deployment_profile(&mut config, profile);
1014 assert_eq!(config.config().max_udp_connections, expected);
1015 }
1016 }
1017 }
1018
1019 #[test]
1020 fn single_tenant_profile_preserves_requested_network_controls() {
1021 let mut config = NetworkConfig::default();
1022 config.interface.mtu = Some(9000);
1023 config.dns.rebind_protection = false;
1024 config.trust_host_cas = true;
1025 config.outbound_proxy = Some(crate::proxy::OutboundProxy::Socks5 {
1026 address: "127.0.0.1:1080".parse().unwrap(),
1027 credentials: None,
1028 });
1029
1030 let mut resolved = resolved(config);
1031 enforce_deployment_profile(&mut resolved, DeploymentProfile::SingleTenant);
1032 let config = resolved.config();
1033
1034 assert_eq!(config.interface.mtu, Some(9000));
1035 assert!(!config.dns.rebind_protection);
1036 assert!(config.trust_host_cas);
1037 assert!(config.outbound_proxy.is_some());
1038 }
1039
1040 #[test]
1041 fn derive_addresses_slot_1() {
1042 assert_eq!(
1043 derive_guest_ipv4(default_guest_ipv4_pool(), 1).unwrap(),
1044 Ipv4Addr::new(172, 16, 0, 6)
1045 );
1046 assert_eq!(
1047 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
1048 Ipv4Addr::new(172, 16, 0, 5)
1049 );
1050 }
1051
1052 #[test]
1053 fn derive_addresses_max_slot() {
1054 assert_eq!(
1055 derive_guest_mac(u16::MAX),
1056 [0x02, 0x6d, 0x73, 0xff, 0xff, 0x02]
1057 );
1058 assert_eq!(
1059 derive_guest_ipv4(default_guest_ipv4_pool(), u16::MAX).unwrap(),
1060 Ipv4Addr::new(172, 19, 255, 254)
1061 );
1062 assert_eq!(
1063 derive_guest_ipv6(default_guest_ipv6_pool(), u16::MAX).unwrap(),
1064 "fd42:6d73:62:ffff::2".parse::<Ipv6Addr>().unwrap()
1065 );
1066 }
1067
1068 #[test]
1069 fn derive_addresses_custom_ipv4_pool() {
1070 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
1071 assert_eq!(
1072 derive_guest_ipv4(pool, 0).unwrap(),
1073 Ipv4Addr::new(172, 31, 240, 2)
1074 );
1075 assert_eq!(
1076 derive_guest_ipv4(pool, 63).unwrap(),
1077 Ipv4Addr::new(172, 31, 240, 254)
1078 );
1079 }
1080
1081 #[test]
1082 fn custom_ipv4_pool_capacity_is_a_typed_error() {
1083 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
1084 assert!(matches!(
1085 derive_guest_ipv4(pool, 64),
1086 Err(NetworkInitError::Ipv4PoolCapacity { slot: 64, .. })
1087 ));
1088
1089 let pool = "172.31.240.0/31".parse::<Ipv4Network>().unwrap();
1090 assert!(matches!(
1091 derive_guest_ipv4(pool, 0),
1092 Err(NetworkInitError::Ipv4PoolCapacity { slot: 0, .. })
1093 ));
1094 }
1095
1096 #[test]
1097 fn derive_ipv6_slot_0() {
1098 assert_eq!(
1099 derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap(),
1100 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
1101 );
1102 assert_eq!(
1103 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0).unwrap()),
1104 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
1105 );
1106 }
1107
1108 #[test]
1109 fn derive_addresses_custom_ipv6_pool() {
1110 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
1111 assert_eq!(
1112 derive_guest_ipv6(pool, 0).unwrap(),
1113 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
1114 );
1115 assert_eq!(
1116 derive_guest_ipv6(pool, 3).unwrap(),
1117 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
1118 );
1119 }
1120
1121 #[test]
1122 fn custom_ipv6_pool_capacity_is_a_typed_error() {
1123 let pool = "fd7a:115c:a1e0:100::/62".parse::<Ipv6Network>().unwrap();
1124 assert!(matches!(
1125 derive_guest_ipv6(pool, 4),
1126 Err(NetworkInitError::Ipv6PoolCapacity { slot: 4, .. })
1127 ));
1128
1129 let pool = "fd7a:115c:a1e0:100::/65".parse::<Ipv6Network>().unwrap();
1130 assert!(matches!(
1131 derive_guest_ipv6(pool, 0),
1132 Err(NetworkInitError::Ipv6PoolCapacity { slot: 0, .. })
1133 ));
1134 }
1135
1136 #[test]
1137 fn format_mac_address() {
1138 assert_eq!(
1139 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
1140 "02:6d:73:00:00:01"
1141 );
1142 }
1143
1144 #[test]
1145 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
1146 let net = SmoltcpNetwork::build(
1147 resolved(NetworkConfig::default()),
1148 0,
1149 DeploymentProfile::SingleTenant,
1150 routes(true, false),
1151 )
1152 .unwrap();
1153 let vars = net.guest_env_vars();
1154
1155 assert_eq!(vars.len(), 3);
1156 assert_eq!(vars[0].0, ENV_NET);
1157 assert!(vars[0].1.contains("iface=eth0"));
1158 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1159 assert_eq!(vars[1].1, crate::HOST_ALIAS);
1160 assert_eq!(vars[2].0, ENV_NET_IPV4);
1161 assert!(vars[2].1.contains("/30"));
1162 }
1163
1164 #[test]
1165 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
1166 let net = SmoltcpNetwork::build(
1167 resolved(NetworkConfig::default()),
1168 0,
1169 DeploymentProfile::SingleTenant,
1170 routes(true, true),
1171 )
1172 .unwrap();
1173 let vars = net.guest_env_vars();
1174
1175 assert_eq!(vars.len(), 4);
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_IPV4);
1179 assert_eq!(vars[3].0, ENV_NET_IPV6);
1180 assert!(vars[3].1.contains("/64"));
1181 }
1182
1183 #[test]
1184 fn guest_env_vars_omit_ipv6_without_host_route() {
1185 let net = SmoltcpNetwork::build(
1186 resolved(NetworkConfig::default()),
1187 0,
1188 DeploymentProfile::SingleTenant,
1189 routes(true, false),
1190 )
1191 .unwrap();
1192 let vars = net.guest_env_vars();
1193
1194 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
1195 }
1196
1197 #[test]
1198 fn guest_env_vars_omit_ipv4_without_host_route() {
1199 let net = SmoltcpNetwork::build(
1200 resolved(NetworkConfig::default()),
1201 0,
1202 DeploymentProfile::SingleTenant,
1203 routes(false, true),
1204 )
1205 .unwrap();
1206 let vars = net.guest_env_vars();
1207
1208 assert_eq!(vars.len(), 3);
1209 assert_eq!(vars[0].0, ENV_NET);
1210 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1211 assert_eq!(vars[2].0, ENV_NET_IPV6);
1212 }
1213
1214 #[test]
1215 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
1216 let mut config = NetworkConfig::default();
1217 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
1218 let net = SmoltcpNetwork::build(
1219 resolved(config),
1220 0,
1221 DeploymentProfile::SingleTenant,
1222 routes(true, false),
1223 )
1224 .unwrap();
1225 let vars = net.guest_env_vars();
1226
1227 let v6 = vars
1228 .iter()
1229 .find(|(k, _)| k == ENV_NET_IPV6)
1230 .expect("explicit ipv6 should publish env var even without host route");
1231 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
1232 }
1233
1234 #[test]
1235 fn published_ports_preserve_existing_address_families() {
1236 for (host_routes, explicit_address, profile, expected_ipv4, expected_ipv6) in [
1237 (
1238 routes(true, false),
1239 None,
1240 DeploymentProfile::SingleTenant,
1241 true,
1242 false,
1243 ),
1244 (
1245 routes(false, true),
1246 None,
1247 DeploymentProfile::SingleTenant,
1248 false,
1249 true,
1250 ),
1251 (
1252 routes(true, true),
1253 None,
1254 DeploymentProfile::SingleTenant,
1255 true,
1256 true,
1257 ),
1258 (
1259 routes(false, false),
1260 Some("fd42:6d73:62:99::2"),
1261 DeploymentProfile::SingleTenant,
1262 false,
1263 true,
1264 ),
1265 (
1266 routes(false, false),
1267 Some("172.20.0.2"),
1268 DeploymentProfile::SingleTenant,
1269 true,
1270 false,
1271 ),
1272 (
1273 routes(false, false),
1274 None,
1275 DeploymentProfile::MultiTenant,
1276 false,
1277 false,
1278 ),
1279 ] {
1280 let mut config = NetworkConfig::default();
1281 config.tls.enabled = false;
1282 let explicit_address = explicit_address.map(|address| address.parse().unwrap());
1283 match explicit_address {
1284 Some(std::net::IpAddr::V4(address)) => {
1285 config.interface.ipv4_address = Some(address)
1286 }
1287 Some(std::net::IpAddr::V6(address)) => {
1288 config.interface.ipv6_address = Some(address)
1289 }
1290 None => {}
1291 }
1292 config.ports.push(PublishedPort {
1293 host_port: 8080,
1294 guest_port: 8000,
1295 protocol: PortProtocol::Tcp,
1296 host_bind: Ipv4Addr::UNSPECIFIED.into(),
1297 });
1298 for host_bind in [Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()] {
1301 config.ports.push(PublishedPort {
1302 host_port: 8081,
1303 guest_port: 8001,
1304 protocol: PortProtocol::Udp,
1305 host_bind,
1306 });
1307 }
1308 let network = SmoltcpNetwork::build(resolved(config), 7, profile, host_routes).unwrap();
1309 let bootstrap = network.guest_bootstrap_network();
1310
1311 assert_eq!(bootstrap.ipv4.is_some(), expected_ipv4);
1312 assert_eq!(bootstrap.ipv6.is_some(), expected_ipv6);
1313 match explicit_address {
1314 Some(std::net::IpAddr::V4(address)) => {
1315 assert_eq!(bootstrap.ipv4.unwrap().address, address);
1316 }
1317 Some(std::net::IpAddr::V6(address)) => {
1318 assert_eq!(bootstrap.ipv6.unwrap().address, address);
1319 }
1320 None => {}
1321 }
1322 }
1323 }
1324
1325 #[test]
1326 fn neither_family_active_emits_only_base_env_vars() {
1327 let net = SmoltcpNetwork::build(
1328 resolved(NetworkConfig::default()),
1329 0,
1330 DeploymentProfile::SingleTenant,
1331 routes(false, false),
1332 )
1333 .unwrap();
1334 let vars = net.guest_env_vars();
1335
1336 assert_eq!(vars.len(), 2);
1337 assert_eq!(vars[0].0, ENV_NET);
1338 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
1339 }
1340
1341 #[test]
1342 fn guest_bootstrap_network_preserves_active_address_families() {
1343 let net = SmoltcpNetwork::build(
1344 resolved(NetworkConfig::default()),
1345 7,
1346 DeploymentProfile::SingleTenant,
1347 routes(true, true),
1348 )
1349 .unwrap();
1350
1351 let bootstrap = net.guest_bootstrap_network();
1352
1353 assert_eq!(bootstrap.interface, "eth0");
1354 assert_eq!(bootstrap.mac, net.guest_mac());
1355 assert_eq!(bootstrap.mtu, 1500);
1356 assert_eq!(bootstrap.ipv4.unwrap().prefix_len, 30);
1357 assert_eq!(bootstrap.ipv6.unwrap().prefix_len, 64);
1358 assert_eq!(net.guest_host_alias(), crate::HOST_ALIAS);
1359 }
1360
1361 #[test]
1362 fn guest_bootstrap_network_allows_no_active_address_family() {
1363 let net = SmoltcpNetwork::build(
1364 resolved(NetworkConfig::default()),
1365 0,
1366 DeploymentProfile::SingleTenant,
1367 routes(false, false),
1368 )
1369 .unwrap();
1370
1371 let bootstrap = net.guest_bootstrap_network();
1372
1373 assert!(bootstrap.ipv4.is_none());
1374 assert!(bootstrap.ipv6.is_none());
1375 }
1376
1377 #[test]
1378 fn large_connection_cap_does_not_preallocate_or_prevent_startup() {
1379 for limit in [10000, usize::MAX] {
1380 let mut config = NetworkConfig::default();
1381 config.tls.enabled = false;
1382 config.max_tcp_connections = Some(ConnectionLimit::from(limit));
1383 let net = SmoltcpNetwork::build(
1384 resolved(config),
1385 0,
1386 DeploymentProfile::MultiTenant,
1387 routes(true, false),
1388 );
1389 assert!(net.is_ok(), "large explicit cap should allow startup");
1390 }
1391 }
1392
1393 #[test]
1396 fn build_rejects_invalid_rate_limiter() {
1397 let mut config = NetworkConfig {
1398 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
1399 egress: None,
1400 ingress: Some(microsandbox_types::RateLimiterConfig {
1401 bandwidth: None,
1402 ops: None,
1403 }),
1404 }),
1405 ..NetworkConfig::default()
1406 };
1407 config.tls.enabled = false;
1408
1409 let err = match SmoltcpNetwork::build(
1410 resolved(config),
1411 0,
1412 DeploymentProfile::SingleTenant,
1413 routes(true, false),
1414 ) {
1415 Ok(_) => panic!("empty rate limiter should fail"),
1416 Err(err) => err,
1417 };
1418
1419 assert!(matches!(
1420 err,
1421 NetworkInitError::InvalidRateLimit {
1422 direction: NetworkRateLimitDirection::Ingress,
1423 source: RateLimitConfigError::EmptyLimiter,
1424 }
1425 ));
1426 }
1427}