Skip to main content

microsandbox_network/engine/
network.rs

1//! `SmoltcpNetwork` — orchestration type that ties [`crate::config::NetworkConfig`] to the
2//! smoltcp engine.
3//!
4//! This is the networking analog to `PassthroughFs`/`MemFs` on the filesystem side — the single
5//! type the runtime creates from config, wires into the VM builder, and starts
6//! the networking stack.
7
8use 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
33//--------------------------------------------------------------------------------------------------
34// Constants
35//--------------------------------------------------------------------------------------------------
36
37/// Default TCP cap for multi-tenant deployments; explicit settings override it.
38const DEFAULT_MULTI_TENANT_MAX_TCP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
39
40/// Default UDP cap for multi-tenant deployments; explicit settings override it.
41const DEFAULT_MULTI_TENANT_MAX_UDP_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap();
42
43//--------------------------------------------------------------------------------------------------
44// Types
45//--------------------------------------------------------------------------------------------------
46
47/// The networking engine. Created from [`crate::config::NetworkConfig`] by the runtime.
48///
49/// Owns the smoltcp poll thread and provides:
50/// - [`take_backend()`](Self::take_backend) — the `NetBackend` for `VmBuilder::net()`
51/// - [`guest_bootstrap_network()`](Self::guest_bootstrap_network) — typed guest network setup
52/// - [`ca_cert_pem()`](Self::ca_cert_pem) — CA certificate for TLS interception
53pub struct SmoltcpNetwork {
54    config: ResolvedNetworkConfig,
55    /// Host-owned policy floor derived from the deployment profile and
56    /// enforced in addition to the sandbox's configured network policy.
57    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    // Resolved from config + slot.
64    guest_mac: [u8; 6],
65    gateway_mac: [u8; 6],
66    mtu: u16,
67    // IPv4 / IPv6 are `Some` when active for this sandbox: the user supplied
68    // an explicit address, or the host has a route for that family.
69    guest_ipv4: Option<Ipv4Addr>,
70    gateway_ipv4: Option<Ipv4Addr>,
71    guest_ipv6: Option<Ipv6Addr>,
72    gateway_ipv6: Option<Ipv6Addr>,
73
74    // TLS state (if enabled). Created in new(), used for ca_cert_pem().
75    tls_state: Option<Arc<TlsState>>,
76
77    // Live-swappable secrets view shared with the poll loop and TLS state.
78    secrets: SecretsHandle,
79}
80
81#[derive(Clone, Copy)]
82struct HostRoutes {
83    ipv4: bool,
84    ipv6: bool,
85}
86
87/// Errors that prevent the smoltcp network from being created safely.
88#[derive(Debug, thiserror::Error)]
89pub enum NetworkInitError {
90    /// A checkpoint supplied an unusable virtual gateway Ethernet address.
91    #[error("captured gateway MAC must be a nonzero unicast address distinct from the guest")]
92    InvalidGatewayMac,
93    /// The configured IPv4 pool cannot provide a `/30` for this slot.
94    #[error("IPv4 pool {pool} cannot assign network slot {slot}")]
95    Ipv4PoolCapacity {
96        /// Configured IPv4 pool.
97        pool: Ipv4Network,
98        /// Requested sandbox slot.
99        slot: u16,
100    },
101
102    /// The configured IPv6 pool cannot provide a `/64` for this slot.
103    #[error("IPv6 pool {pool} cannot assign network slot {slot}")]
104    Ipv6PoolCapacity {
105        /// Configured IPv6 pool.
106        pool: Ipv6Network,
107        /// Requested sandbox slot.
108        slot: u16,
109    },
110
111    /// TLS interception state failed to initialize.
112    #[error("TLS initialization failed: {0}")]
113    Tls(#[from] TlsStateError),
114
115    /// A stored rate limiter configuration failed validation.
116    #[error("invalid {direction} rate limiter: {source}")]
117    InvalidRateLimit {
118        /// Which limiter is invalid: `egress` or `ingress`.
119        direction: NetworkRateLimitDirection,
120        /// Underlying validation error.
121        #[source]
122        source: RateLimitConfigError,
123    },
124
125    /// A stored network rate limiter has neither direction configured.
126    #[error("invalid network rate limiter: at least one of egress or ingress is required")]
127    EmptyNetworkRateLimiter,
128}
129
130/// Handle for installing host-side termination behavior into the network stack.
131#[derive(Clone)]
132pub struct TerminationHandle {
133    shared: Arc<SharedState>,
134}
135
136/// Read-only view of aggregate network byte counters.
137#[derive(Clone)]
138pub struct MetricsHandle {
139    shared: Arc<SharedState>,
140}
141
142/// One-shot handle that permits a deferred network stack to publish listeners and process traffic.
143#[derive(Clone)]
144pub struct NetworkActivationHandle {
145    gate: Arc<NetworkActivationGate>,
146}
147
148struct NetworkActivationGate {
149    active: Mutex<bool>,
150    changed: Condvar,
151}
152
153//--------------------------------------------------------------------------------------------------
154// Methods
155//--------------------------------------------------------------------------------------------------
156
157impl 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    /// Gateway identity for an ordinary cold boot in the given host slot.
168    pub fn default_gateway_mac(slot: u16) -> [u8; 6] {
169        derive_gateway_mac(slot)
170    }
171
172    /// Preserve a captured gateway before starting this fresh network backend.
173    ///
174    /// Host sockets, policy, queues and port ownership remain child-owned. Only
175    /// the Ethernet identity visible to captured ARP/ND caches is retained.
176    /// Panics if called after the network poll thread has started.
177    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    /// Creates the network backend from a fully resolved runtime configuration.
190    ///
191    /// `MultiTenant` applies platform-owned configuration floors before any
192    /// sockets, resolvers, or TLS state are created. The requested tenant policy
193    /// remains separate and is intersected with the platform's public-network
194    /// policy by the poll loop.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error when the effective network configuration would allocate
199    /// unsafe resources or TLS interception cannot initialize.
200    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        // Packet queue capacity is independent of the optional connection cap:
252        // a large cap must not allocate a correspondingly large packet queue.
253        let shared = Arc::new(SharedState::new(DEFAULT_QUEUE_CAPACITY));
254        // Every write path validates rate limiters (`NetworkBuilder::build`),
255        // but a stored config bypasses the builder: fail startup cleanly
256        // instead of panicking on a corrupted spec.
257        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    /// Hold network processing and published-port creation behind an explicit one-shot gate.
314    ///
315    /// This must be selected before [`start`](Self::start). It is used by checkpoint restore so
316    /// ingress cannot reach the child until guest activation has completed. The gate is consumed
317    /// once at poll-thread startup and adds no checks to steady-state packet processing.
318    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    /// Get the gateway IPs for virtio-net configuration and domain-based policy rules.
341    fn gateway_ips(&self) -> GatewayIps {
342        GatewayIps {
343            ipv4: self.gateway_ipv4,
344            ipv6: self.gateway_ipv6,
345        }
346    }
347
348    /// Start the smoltcp poll thread.
349    ///
350    /// Must be called before VM boot. Requires a tokio runtime handle for
351    /// spawning proxy tasks, DNS resolution, and published port listeners.
352    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    /// Take the `NetBackend` for `VmBuilder::net()`. One-shot.
403    pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
404        Box::new(self.backend.take().expect("backend already taken"))
405    }
406
407    /// Guest MAC address for `VmBuilder::net().mac()`.
408    pub fn guest_mac(&self) -> [u8; 6] {
409        self.guest_mac
410    }
411
412    /// Generate `MSB_NET*` environment variables for the guest.
413    ///
414    /// The guest init (`agentd`) reads these to configure the network
415    /// interface via ioctls + netlink.
416    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        // Auto-expose secret placeholders as environment variables.
444        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    /// Build the typed network payload consumed by agentd during bootstrap.
452    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    /// Return the stable hostname used by guests to address the host gateway.
479    pub fn guest_host_alias(&self) -> &'static str {
480        crate::HOST_ALIAS
481    }
482
483    /// Return guest-visible secret placeholders for the baseline environment.
484    ///
485    /// Real secret values stay in the host-side network handler and never
486    /// enter this payload.
487    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    /// CA certificate PEM bytes if TLS interception is enabled.
501    ///
502    /// Write to the runtime mount before VM boot so the guest can trust it.
503    pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
504        self.tls_state.as_ref().map(|s| s.ca_cert_pem())
505    }
506
507    /// Host-trusted CA bundle to ship into the guest, if
508    /// [`crate::config::NetworkConfig::trust_host_cas`] is enabled.
509    ///
510    /// Returned PEM may concatenate CAs that the Mozilla root bundle in
511    /// the guest already trusts; duplicates are harmless and saved the
512    /// cost of computing a delta. Returns `None` when the host store is
513    /// empty or the feature is disabled.
514    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    /// Create a handle for wiring runtime termination into the network stack.
522    pub fn termination_handle(&self) -> TerminationHandle {
523        TerminationHandle {
524            shared: self.shared.clone(),
525        }
526    }
527
528    /// Create a handle for reading aggregate network byte counters.
529    pub fn metrics_handle(&self) -> MetricsHandle {
530        MetricsHandle {
531            shared: self.shared.clone(),
532        }
533    }
534
535    /// Live-swappable view of the secrets configuration. The runtime control
536    /// socket uses it to apply secret rotation, removal, and allowed-host
537    /// updates without restarting the sandbox.
538    pub fn secrets_handle(&self) -> SecretsHandle {
539        self.secrets.clone()
540    }
541}
542
543impl NetworkActivationHandle {
544    /// Release a deferred network exactly once. Repeated calls are harmless.
545    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    /// Install the termination hook.
565    pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
566        self.shared.set_termination_hook(hook);
567    }
568}
569
570impl MetricsHandle {
571    /// Total guest -> runtime bytes observed at the virtio-net boundary.
572    pub fn tx_bytes(&self) -> u64 {
573        self.shared.tx_bytes()
574    }
575
576    /// Total runtime -> guest bytes observed at the virtio-net boundary.
577    pub fn rx_bytes(&self) -> u64 {
578        self.shared.rx_bytes()
579    }
580}
581
582//--------------------------------------------------------------------------------------------------
583// Functions
584//--------------------------------------------------------------------------------------------------
585
586/// Apply the platform-owned configuration floor before network resources are created.
587///
588/// Policy rules are deliberately not flattened here. The poll loop evaluates
589/// the platform public-network policy and the tenant policy independently so a
590/// broad tenant allow can never outrank the platform floor, while a tenant deny
591/// still remains effective.
592fn 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
645/// Derive a guest MAC address from the sandbox slot.
646///
647/// Format: `02:ms:bx:SS:SS:02` where SS:SS encodes the slot.
648fn 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
653/// Derive a gateway MAC address from the sandbox slot.
654///
655/// Format: `02:ms:bx:SS:SS:01`.
656fn 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
661/// Derive a guest IPv4 address from the sandbox slot.
662///
663/// Pool: `172.16.0.0/12` by default. Each slot gets a `/30` block (4 IPs).
664/// Guest is at offset +2 in the block.
665fn 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; // +2 = guest within /30
676    Ok(Ipv4Addr::from(base + offset))
677}
678
679/// Gateway IPv4 from guest IPv4: guest - 1 (offset +1 in the /30 block).
680fn 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
689/// Derive a guest IPv6 address from the sandbox slot.
690///
691/// Pool: `fd42:6d73:62::/48`. Each slot gets a `/64` prefix.
692/// Guest is `::2` in its prefix.
693fn 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
707/// Gateway IPv6 from guest IPv6: `::1` in the same prefix.
708fn 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
718/// Format a MAC address as `xx:xx:xx:xx:xx:xx`.
719fn 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
726/// Returns true if the host kernel can select an IPv4 route.
727///
728/// `UdpSocket::connect` performs a local routing-table lookup against the
729/// TEST-NET-1 (`192.0.2.1`) address; it does not send packets or wait on
730/// the network.
731fn 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
737/// Returns true if the host kernel can select an IPv6 route. Probes a
738/// `2001:db8::/32` documentation address via `UdpSocket::connect` (no packet
739/// is sent).
740fn 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//--------------------------------------------------------------------------------------------------
747// Tests
748//--------------------------------------------------------------------------------------------------
749
750#[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        // A user-supplied guest MAC cannot reveal the source gateway's slot.
768        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        // Tenant policy stays intact and is intersected with the platform
896        // policy at evaluation time instead of being reordered or flattened.
897        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                // Exercise the serialized runtime launch boundary as well.
912                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                // Applying the profile again must preserve the resolved value.
928                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    /// A stored config bypasses the builder's validation, so an invalid
1250    /// limiter must fail startup cleanly instead of panicking.
1251    #[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}