1use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
9use std::sync::Arc;
10use std::thread::JoinHandle;
11
12use ipnetwork::{Ipv4Network, Ipv6Network};
13use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
14use microsandbox_types::{
15 DeploymentProfile, NetworkRateLimitDirection, RateLimitConfigError, RateLimiterConfig,
16};
17use msb_krun::backends::net::NetBackend;
18
19use crate::backend::SmoltcpBackend;
20use crate::config::{MAX_NETWORK_CONNECTIONS, NetworkConfig};
21use crate::policy::{NetworkPolicy, NetworkProfile};
22use crate::secrets::handle::SecretsHandle;
23use crate::shared::{DEFAULT_QUEUE_CAPACITY, SharedState};
24use crate::stack::{self, GatewayIps, PollLoopConfig};
25use crate::tls::state::{TlsState, TlsStateError};
26
27const MAX_SLOT: u64 = u16::MAX as u64;
36
37const MULTI_TENANT_MAX_CONNECTIONS: usize = 256;
42
43pub struct SmoltcpNetwork {
54 config: NetworkConfig,
55 deployment_profile: DeploymentProfile,
56 shared: Arc<SharedState>,
57 backend: Option<SmoltcpBackend>,
58 poll_handle: Option<JoinHandle<()>>,
59
60 guest_mac: [u8; 6],
62 gateway_mac: [u8; 6],
63 mtu: u16,
64 guest_ipv4: Option<Ipv4Addr>,
67 gateway_ipv4: Option<Ipv4Addr>,
68 guest_ipv6: Option<Ipv6Addr>,
69 gateway_ipv6: Option<Ipv6Addr>,
70
71 tls_state: Option<Arc<TlsState>>,
73
74 secrets: SecretsHandle,
76}
77
78#[derive(Debug, thiserror::Error)]
80pub enum NetworkInitError {
81 #[error("max_connections {configured} exceeds hard limit {limit}")]
83 MaxConnectionsExceeded {
84 configured: usize,
86 limit: usize,
88 },
89
90 #[error("TLS initialization failed: {0}")]
92 Tls(#[from] TlsStateError),
93
94 #[error("invalid {direction} rate limiter: {source}")]
96 InvalidRateLimit {
97 direction: NetworkRateLimitDirection,
99 #[source]
101 source: RateLimitConfigError,
102 },
103
104 #[error("invalid network rate limiter: at least one of egress or ingress is required")]
106 EmptyNetworkRateLimiter,
107}
108
109#[derive(Clone)]
111pub struct TerminationHandle {
112 shared: Arc<SharedState>,
113}
114
115#[derive(Clone)]
117pub struct MetricsHandle {
118 shared: Arc<SharedState>,
119}
120
121impl SmoltcpNetwork {
126 pub fn new(config: NetworkConfig, slot: u64) -> Result<Self, NetworkInitError> {
144 Self::new_with_profile(config, slot, DeploymentProfile::SingleTenant)
145 }
146
147 pub fn new_with_profile(
159 mut config: NetworkConfig,
160 slot: u64,
161 deployment_profile: DeploymentProfile,
162 ) -> Result<Self, NetworkInitError> {
163 enforce_deployment_profile(&mut config, deployment_profile);
164 Self::new_with_profile_and_routes(
165 config,
166 slot,
167 deployment_profile,
168 host_has_ipv4_route(),
169 host_has_ipv6_route(),
170 )
171 }
172
173 #[cfg(test)]
174 fn new_with_routes(
175 config: NetworkConfig,
176 slot: u64,
177 host_has_ipv4: bool,
178 host_has_ipv6: bool,
179 ) -> Result<Self, NetworkInitError> {
180 Self::new_with_profile_and_routes(
181 config,
182 slot,
183 DeploymentProfile::SingleTenant,
184 host_has_ipv4,
185 host_has_ipv6,
186 )
187 }
188
189 fn new_with_profile_and_routes(
190 config: NetworkConfig,
191 slot: u64,
192 deployment_profile: DeploymentProfile,
193 host_has_ipv4: bool,
194 host_has_ipv6: bool,
195 ) -> Result<Self, NetworkInitError> {
196 assert!(
197 slot <= MAX_SLOT,
198 "sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
199 );
200 if let Some(configured) = config.max_connections
201 && configured > MAX_NETWORK_CONNECTIONS
202 {
203 return Err(NetworkInitError::MaxConnectionsExceeded {
204 configured,
205 limit: MAX_NETWORK_CONNECTIONS,
206 });
207 }
208
209 let guest_mac = config
210 .interface
211 .mac
212 .unwrap_or_else(|| derive_guest_mac(slot));
213 let gateway_mac = derive_gateway_mac(slot);
214 let mtu = config.interface.mtu.unwrap_or(1500);
215
216 let guest_ipv4 = config.interface.ipv4_address.or_else(|| {
217 host_has_ipv4.then(|| {
218 derive_guest_ipv4(
219 config
220 .interface
221 .ipv4_pool
222 .unwrap_or_else(default_guest_ipv4_pool),
223 slot,
224 )
225 })
226 });
227 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
228 let guest_ipv6 = config.interface.ipv6_address.or_else(|| {
229 host_has_ipv6.then(|| {
230 derive_guest_ipv6(
231 config
232 .interface
233 .ipv6_pool
234 .unwrap_or_else(default_guest_ipv6_pool),
235 slot,
236 )
237 })
238 });
239 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
240
241 let queue_capacity = config
242 .max_connections
243 .unwrap_or(DEFAULT_QUEUE_CAPACITY)
244 .max(DEFAULT_QUEUE_CAPACITY);
245 let shared = Arc::new(SharedState::new(queue_capacity));
246 if config.rate_limiter.as_ref().is_some_and(|rate_limiter| {
250 rate_limiter.egress.is_none() && rate_limiter.ingress.is_none()
251 }) {
252 return Err(NetworkInitError::EmptyNetworkRateLimiter);
253 }
254 config
255 .rate_limiter
256 .as_ref()
257 .and_then(|rate_limiter| rate_limiter.ingress.as_ref())
258 .map(RateLimiterConfig::validate)
259 .transpose()
260 .map_err(|source| NetworkInitError::InvalidRateLimit {
261 direction: NetworkRateLimitDirection::Ingress,
262 source,
263 })?;
264 config
265 .rate_limiter
266 .as_ref()
267 .and_then(|rate_limiter| rate_limiter.egress.as_ref())
268 .map(RateLimiterConfig::validate)
269 .transpose()
270 .map_err(|source| NetworkInitError::InvalidRateLimit {
271 direction: NetworkRateLimitDirection::Egress,
272 source,
273 })?;
274 let backend = SmoltcpBackend::new(shared.clone());
275
276 let secrets = SecretsHandle::new(config.secrets.clone());
277 let tls_state = if config.tls.enabled {
278 Some(Arc::new(TlsState::new(
279 config.tls.clone(),
280 secrets.clone(),
281 )?))
282 } else {
283 None
284 };
285
286 Ok(Self {
287 config,
288 deployment_profile,
289 shared,
290 backend: Some(backend),
291 poll_handle: None,
292 guest_mac,
293 gateway_mac,
294 mtu,
295 guest_ipv4,
296 gateway_ipv4,
297 guest_ipv6,
298 gateway_ipv6,
299 tls_state,
300 secrets,
301 })
302 }
303
304 fn gateway_ips(&self) -> GatewayIps {
306 GatewayIps {
307 ipv4: self.gateway_ipv4,
308 ipv6: self.gateway_ipv6,
309 }
310 }
311
312 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
317 let shared = self.shared.clone();
318 let poll_config = PollLoopConfig {
319 gateway_mac: self.gateway_mac,
320 guest_mac: self.guest_mac,
321 gateway: self.gateway_ips(),
322 guest_ipv4: self.guest_ipv4,
323 guest_ipv6: self.guest_ipv6,
324 mtu: self.mtu as usize,
325 };
326 let network_policy = self.config.policy.clone();
327 let platform_policy = match self.deployment_profile {
328 DeploymentProfile::SingleTenant => None,
329 DeploymentProfile::MultiTenant => {
330 Some(NetworkPolicy::from_profiles([NetworkProfile::Public]))
331 }
332 };
333 let dns_config = self.config.dns.clone();
334 let tls_state = self.tls_state.clone();
335 let published_ports = self.config.ports.clone();
336 let max_connections = self.config.max_connections;
337 let secrets = self.secrets.clone();
338
339 self.poll_handle = Some(
340 std::thread::Builder::new()
341 .name("smoltcp-poll".into())
342 .spawn(move || {
343 stack::smoltcp_poll_loop(
344 shared,
345 poll_config,
346 network_policy,
347 platform_policy,
348 dns_config,
349 tls_state,
350 published_ports,
351 max_connections,
352 tokio_handle,
353 secrets,
354 );
355 })
356 .expect("failed to spawn smoltcp poll thread"),
357 );
358 }
359
360 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
362 Box::new(self.backend.take().expect("backend already taken"))
363 }
364
365 pub fn guest_mac(&self) -> [u8; 6] {
367 self.guest_mac
368 }
369
370 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
375 let mut vars = vec![
376 (
377 ENV_NET.into(),
378 format!(
379 "iface=eth0,mac={},mtu={}",
380 format_mac(self.guest_mac),
381 self.mtu,
382 ),
383 ),
384 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
385 ];
386
387 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
388 vars.push((
389 ENV_NET_IPV4.into(),
390 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
391 ));
392 }
393
394 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
395 vars.push((
396 ENV_NET_IPV6.into(),
397 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
398 ));
399 }
400
401 for secret in &self.config.secrets.secrets {
403 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
404 }
405
406 vars
407 }
408
409 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
413 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
414 }
415
416 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
424 if !self.config.trust_host_cas {
425 return None;
426 }
427 crate::tls::host_cas::collect_host_cas()
428 }
429
430 pub fn termination_handle(&self) -> TerminationHandle {
432 TerminationHandle {
433 shared: self.shared.clone(),
434 }
435 }
436
437 pub fn metrics_handle(&self) -> MetricsHandle {
439 MetricsHandle {
440 shared: self.shared.clone(),
441 }
442 }
443
444 pub fn secrets_handle(&self) -> SecretsHandle {
448 self.secrets.clone()
449 }
450}
451
452impl TerminationHandle {
453 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
455 self.shared.set_termination_hook(hook);
456 }
457}
458
459impl MetricsHandle {
460 pub fn tx_bytes(&self) -> u64 {
462 self.shared.tx_bytes()
463 }
464
465 pub fn rx_bytes(&self) -> u64 {
467 self.shared.rx_bytes()
468 }
469}
470
471fn enforce_deployment_profile(config: &mut NetworkConfig, profile: DeploymentProfile) {
482 if profile == DeploymentProfile::SingleTenant {
483 return;
484 }
485
486 let interface_overridden = config.interface.mac.is_some()
487 || config.interface.mtu.is_some()
488 || config.interface.ipv4_address.is_some()
489 || config.interface.ipv4_pool.is_some()
490 || config.interface.ipv6_address.is_some()
491 || config.interface.ipv6_pool.is_some();
492 let had_published_ports = !config.ports.is_empty();
493 let had_custom_nameservers = !config.dns.nameservers.is_empty();
494 let disabled_rebind_protection = !config.dns.rebind_protection;
495 let trusted_host_cas = config.trust_host_cas;
496 let connection_limit_clamped = config
497 .max_connections
498 .is_some_and(|limit| limit > MULTI_TENANT_MAX_CONNECTIONS);
499
500 config.interface = Default::default();
501 config.ports.clear();
502 config.dns.nameservers.clear();
503 config.dns.rebind_protection = true;
504 config.trust_host_cas = false;
505 config.max_connections = Some(
506 config
507 .max_connections
508 .unwrap_or(MULTI_TENANT_MAX_CONNECTIONS)
509 .min(MULTI_TENANT_MAX_CONNECTIONS),
510 );
511
512 if interface_overridden
513 || had_published_ports
514 || had_custom_nameservers
515 || disabled_rebind_protection
516 || trusted_host_cas
517 || connection_limit_clamped
518 {
519 tracing::warn!(
520 interface_overridden,
521 had_published_ports,
522 had_custom_nameservers,
523 disabled_rebind_protection,
524 trusted_host_cas,
525 connection_limit_clamped,
526 "multi-tenant deployment profile overrode unsafe network configuration"
527 );
528 }
529}
530
531fn derive_guest_mac(slot: u64) -> [u8; 6] {
535 let s = slot.to_be_bytes();
536 [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
537}
538
539fn derive_gateway_mac(slot: u64) -> [u8; 6] {
543 let s = slot.to_be_bytes();
544 [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
545}
546
547fn derive_guest_ipv4(pool: Ipv4Network, slot: u64) -> Ipv4Addr {
552 assert!(
553 pool.prefix() <= 30,
554 "IPv4 pool {pool} must be large enough to contain at least one /30 block"
555 );
556
557 let capacity = 1u64 << (30 - pool.prefix());
558 assert!(
559 slot < capacity,
560 "sandbox slot {slot} exceeds IPv4 pool {pool} capacity ({capacity} /30 blocks)"
561 );
562
563 let base = u32::from(pool.network());
564 let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
566}
567
568fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
570 Ipv4Addr::from(u32::from(guest) - 1)
571}
572
573fn default_guest_ipv4_pool() -> Ipv4Network {
574 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
575 .expect("default IPv4 pool must be a valid network")
576}
577
578fn derive_guest_ipv6(pool: Ipv6Network, slot: u64) -> Ipv6Addr {
583 assert!(
584 pool.prefix() <= 64,
585 "IPv6 pool {pool} must be large enough to contain at least one /64 prefix"
586 );
587
588 let capacity = 1u128 << (64 - pool.prefix());
589 assert!(
590 (slot as u128) < capacity,
591 "sandbox slot {slot} exceeds IPv6 pool {pool} capacity ({capacity} /64 prefixes)"
592 );
593
594 let base = u128::from(pool.network());
595 let offset = (slot as u128) << 64;
596 Ipv6Addr::from(base + offset + 2)
597}
598
599fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
601 let segs = guest.segments();
602 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
603}
604
605fn default_guest_ipv6_pool() -> Ipv6Network {
606 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
607 .expect("default IPv6 pool must be a valid network")
608}
609
610fn format_mac(mac: [u8; 6]) -> String {
612 format!(
613 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
614 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
615 )
616}
617
618fn host_has_ipv4_route() -> bool {
624 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
625 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
626 .is_ok()
627}
628
629fn host_has_ipv6_route() -> bool {
633 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
634 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
635 .is_ok()
636}
637
638#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::config::{PortProtocol, PublishedPort};
646 use crate::dns::Nameserver;
647
648 #[test]
649 fn derive_addresses_slot_0() {
650 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
651 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
652 assert_eq!(
653 derive_guest_ipv4(default_guest_ipv4_pool(), 0),
654 Ipv4Addr::new(172, 16, 0, 2)
655 );
656 assert_eq!(
657 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
658 Ipv4Addr::new(172, 16, 0, 1)
659 );
660 }
661
662 #[test]
663 fn multi_tenant_profile_sanitizes_host_owned_network_controls() {
664 let mut config = NetworkConfig::default();
665 config.interface.mac = Some([2, 3, 4, 5, 6, 7]);
666 config.interface.mtu = Some(9000);
667 config.ports.push(PublishedPort {
668 host_port: 8080,
669 guest_port: 80,
670 protocol: PortProtocol::Tcp,
671 host_bind: Ipv4Addr::UNSPECIFIED.into(),
672 });
673 config.dns.nameservers = vec!["10.0.0.53".parse::<Nameserver>().unwrap()];
674 config.dns.rebind_protection = false;
675 config.trust_host_cas = true;
676 config.max_connections = Some(MULTI_TENANT_MAX_CONNECTIONS + 1);
677 config.policy = NetworkPolicy::allow_all();
678
679 enforce_deployment_profile(&mut config, DeploymentProfile::MultiTenant);
680
681 assert!(config.interface.mac.is_none());
682 assert!(config.interface.mtu.is_none());
683 assert!(config.ports.is_empty());
684 assert!(config.dns.nameservers.is_empty());
685 assert!(config.dns.rebind_protection);
686 assert!(!config.trust_host_cas);
687 assert_eq!(config.max_connections, Some(MULTI_TENANT_MAX_CONNECTIONS));
688 assert!(config.policy.default_egress.is_allow());
691 }
692
693 #[test]
694 fn single_tenant_profile_preserves_requested_network_controls() {
695 let mut config = NetworkConfig::default();
696 config.interface.mtu = Some(9000);
697 config.dns.rebind_protection = false;
698 config.trust_host_cas = true;
699
700 enforce_deployment_profile(&mut config, DeploymentProfile::SingleTenant);
701
702 assert_eq!(config.interface.mtu, Some(9000));
703 assert!(!config.dns.rebind_protection);
704 assert!(config.trust_host_cas);
705 }
706
707 #[test]
708 fn derive_addresses_slot_1() {
709 assert_eq!(
710 derive_guest_ipv4(default_guest_ipv4_pool(), 1),
711 Ipv4Addr::new(172, 16, 0, 6)
712 );
713 assert_eq!(
714 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
715 Ipv4Addr::new(172, 16, 0, 5)
716 );
717 }
718
719 #[test]
720 fn derive_addresses_custom_ipv4_pool() {
721 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
722 assert_eq!(derive_guest_ipv4(pool, 0), Ipv4Addr::new(172, 31, 240, 2));
723 assert_eq!(
724 derive_guest_ipv4(pool, 63),
725 Ipv4Addr::new(172, 31, 240, 254)
726 );
727 }
728
729 #[test]
730 fn derive_ipv6_slot_0() {
731 assert_eq!(
732 derive_guest_ipv6(default_guest_ipv6_pool(), 0),
733 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
734 );
735 assert_eq!(
736 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0)),
737 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
738 );
739 }
740
741 #[test]
742 fn derive_addresses_custom_ipv6_pool() {
743 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
744 assert_eq!(
745 derive_guest_ipv6(pool, 0),
746 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
747 );
748 assert_eq!(
749 derive_guest_ipv6(pool, 3),
750 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
751 );
752 }
753
754 #[test]
755 fn format_mac_address() {
756 assert_eq!(
757 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
758 "02:6d:73:00:00:01"
759 );
760 }
761
762 #[test]
763 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
764 let net =
765 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false).unwrap();
766 let vars = net.guest_env_vars();
767
768 assert_eq!(vars.len(), 3);
769 assert_eq!(vars[0].0, ENV_NET);
770 assert!(vars[0].1.contains("iface=eth0"));
771 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
772 assert_eq!(vars[1].1, crate::HOST_ALIAS);
773 assert_eq!(vars[2].0, ENV_NET_IPV4);
774 assert!(vars[2].1.contains("/30"));
775 }
776
777 #[test]
778 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
779 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, true).unwrap();
780 let vars = net.guest_env_vars();
781
782 assert_eq!(vars.len(), 4);
783 assert_eq!(vars[0].0, ENV_NET);
784 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
785 assert_eq!(vars[2].0, ENV_NET_IPV4);
786 assert_eq!(vars[3].0, ENV_NET_IPV6);
787 assert!(vars[3].1.contains("/64"));
788 }
789
790 #[test]
791 fn guest_env_vars_omit_ipv6_without_host_route() {
792 let net =
793 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false).unwrap();
794 let vars = net.guest_env_vars();
795
796 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
797 }
798
799 #[test]
800 fn guest_env_vars_omit_ipv4_without_host_route() {
801 let net =
802 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, true).unwrap();
803 let vars = net.guest_env_vars();
804
805 assert_eq!(vars.len(), 3);
806 assert_eq!(vars[0].0, ENV_NET);
807 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
808 assert_eq!(vars[2].0, ENV_NET_IPV6);
809 }
810
811 #[test]
812 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
813 let mut config = NetworkConfig::default();
814 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
815 let net = SmoltcpNetwork::new_with_routes(config, 0, true, false).unwrap();
816 let vars = net.guest_env_vars();
817
818 let v6 = vars
819 .iter()
820 .find(|(k, _)| k == ENV_NET_IPV6)
821 .expect("explicit ipv6 should publish env var even without host route");
822 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
823 }
824
825 #[test]
826 fn neither_family_active_emits_only_base_env_vars() {
827 let net =
828 SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false).unwrap();
829 let vars = net.guest_env_vars();
830
831 assert_eq!(vars.len(), 2);
832 assert_eq!(vars[0].0, ENV_NET);
833 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
834 }
835
836 #[test]
837 fn new_with_routes_rejects_excessive_max_connections() {
838 let mut config = NetworkConfig {
839 max_connections: Some(MAX_NETWORK_CONNECTIONS + 1),
840 ..NetworkConfig::default()
841 };
842 config.tls.enabled = false;
843
844 let err = match SmoltcpNetwork::new_with_routes(config, 0, true, false) {
845 Ok(_) => panic!("excessive max_connections should fail"),
846 Err(err) => err,
847 };
848
849 assert!(matches!(
850 err,
851 NetworkInitError::MaxConnectionsExceeded {
852 configured,
853 limit: MAX_NETWORK_CONNECTIONS
854 } if configured == MAX_NETWORK_CONNECTIONS + 1
855 ));
856 }
857
858 #[test]
861 fn new_with_routes_rejects_invalid_rate_limiter() {
862 let mut config = NetworkConfig {
863 rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
864 egress: None,
865 ingress: Some(microsandbox_types::RateLimiterConfig {
866 bandwidth: None,
867 ops: None,
868 }),
869 }),
870 ..NetworkConfig::default()
871 };
872 config.tls.enabled = false;
873
874 let err = match SmoltcpNetwork::new_with_routes(config, 0, true, false) {
875 Ok(_) => panic!("empty rate limiter should fail"),
876 Err(err) => err,
877 };
878
879 assert!(matches!(
880 err,
881 NetworkInitError::InvalidRateLimit {
882 direction: NetworkRateLimitDirection::Ingress,
883 source: RateLimitConfigError::EmptyLimiter,
884 }
885 ));
886 }
887}