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