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