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 msb_krun::backends::net::NetBackend;
15
16use crate::backend::SmoltcpBackend;
17use crate::config::NetworkConfig;
18use crate::shared::{DEFAULT_QUEUE_CAPACITY, SharedState};
19use crate::stack::{self, GatewayIps, PollLoopConfig};
20use crate::tls::state::TlsState;
21
22const MAX_SLOT: u64 = u16::MAX as u64;
31
32pub struct SmoltcpNetwork {
43 config: NetworkConfig,
44 shared: Arc<SharedState>,
45 backend: Option<SmoltcpBackend>,
46 poll_handle: Option<JoinHandle<()>>,
47
48 guest_mac: [u8; 6],
50 gateway_mac: [u8; 6],
51 mtu: u16,
52 guest_ipv4: Option<Ipv4Addr>,
55 gateway_ipv4: Option<Ipv4Addr>,
56 guest_ipv6: Option<Ipv6Addr>,
57 gateway_ipv6: Option<Ipv6Addr>,
58
59 tls_state: Option<Arc<TlsState>>,
61}
62
63#[derive(Clone)]
65pub struct TerminationHandle {
66 shared: Arc<SharedState>,
67}
68
69#[derive(Clone)]
71pub struct MetricsHandle {
72 shared: Arc<SharedState>,
73}
74
75impl SmoltcpNetwork {
80 pub fn new(config: NetworkConfig, slot: u64) -> Self {
93 Self::new_with_routes(config, slot, host_has_ipv4_route(), host_has_ipv6_route())
94 }
95
96 fn new_with_routes(
97 config: NetworkConfig,
98 slot: u64,
99 host_has_ipv4: bool,
100 host_has_ipv6: bool,
101 ) -> Self {
102 assert!(
103 slot <= MAX_SLOT,
104 "sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
105 );
106
107 let guest_mac = config
108 .interface
109 .mac
110 .unwrap_or_else(|| derive_guest_mac(slot));
111 let gateway_mac = derive_gateway_mac(slot);
112 let mtu = config.interface.mtu.unwrap_or(1500);
113
114 let guest_ipv4 = config.interface.ipv4_address.or_else(|| {
115 host_has_ipv4.then(|| {
116 derive_guest_ipv4(
117 config
118 .interface
119 .ipv4_pool
120 .unwrap_or_else(default_guest_ipv4_pool),
121 slot,
122 )
123 })
124 });
125 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
126 let guest_ipv6 = config.interface.ipv6_address.or_else(|| {
127 host_has_ipv6.then(|| {
128 derive_guest_ipv6(
129 config
130 .interface
131 .ipv6_pool
132 .unwrap_or_else(default_guest_ipv6_pool),
133 slot,
134 )
135 })
136 });
137 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
138
139 let queue_capacity = config
140 .max_connections
141 .unwrap_or(DEFAULT_QUEUE_CAPACITY)
142 .max(DEFAULT_QUEUE_CAPACITY);
143 let shared = Arc::new(SharedState::new(queue_capacity));
144 let backend = SmoltcpBackend::new(shared.clone());
145
146 let tls_state = if config.tls.enabled {
147 Some(Arc::new(TlsState::new(
148 config.tls.clone(),
149 config.secrets.clone(),
150 )))
151 } else {
152 None
153 };
154
155 Self {
156 config,
157 shared,
158 backend: Some(backend),
159 poll_handle: None,
160 guest_mac,
161 gateway_mac,
162 mtu,
163 guest_ipv4,
164 gateway_ipv4,
165 guest_ipv6,
166 gateway_ipv6,
167 tls_state,
168 }
169 }
170
171 fn gateway_ips(&self) -> GatewayIps {
173 GatewayIps {
174 ipv4: self.gateway_ipv4,
175 ipv6: self.gateway_ipv6,
176 }
177 }
178
179 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
184 let shared = self.shared.clone();
185 let poll_config = PollLoopConfig {
186 gateway_mac: self.gateway_mac,
187 guest_mac: self.guest_mac,
188 gateway: self.gateway_ips(),
189 guest_ipv4: self.guest_ipv4,
190 guest_ipv6: self.guest_ipv6,
191 mtu: self.mtu as usize,
192 };
193 let network_policy = self.config.policy.clone();
194 let dns_config = self.config.dns.clone();
195 let tls_state = self.tls_state.clone();
196 let published_ports = self.config.ports.clone();
197 let max_connections = self.config.max_connections;
198
199 self.poll_handle = Some(
200 std::thread::Builder::new()
201 .name("smoltcp-poll".into())
202 .spawn(move || {
203 stack::smoltcp_poll_loop(
204 shared,
205 poll_config,
206 network_policy,
207 dns_config,
208 tls_state,
209 published_ports,
210 max_connections,
211 tokio_handle,
212 );
213 })
214 .expect("failed to spawn smoltcp poll thread"),
215 );
216 }
217
218 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
220 Box::new(self.backend.take().expect("backend already taken"))
221 }
222
223 pub fn guest_mac(&self) -> [u8; 6] {
225 self.guest_mac
226 }
227
228 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
233 let mut vars = vec![
234 (
235 ENV_NET.into(),
236 format!(
237 "iface=eth0,mac={},mtu={}",
238 format_mac(self.guest_mac),
239 self.mtu,
240 ),
241 ),
242 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
243 ];
244
245 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
246 vars.push((
247 ENV_NET_IPV4.into(),
248 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
249 ));
250 }
251
252 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
253 vars.push((
254 ENV_NET_IPV6.into(),
255 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
256 ));
257 }
258
259 for secret in &self.config.secrets.secrets {
261 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
262 }
263
264 vars
265 }
266
267 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
271 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
272 }
273
274 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
282 if !self.config.trust_host_cas {
283 return None;
284 }
285 crate::tls::host_cas::collect_host_cas()
286 }
287
288 pub fn termination_handle(&self) -> TerminationHandle {
290 TerminationHandle {
291 shared: self.shared.clone(),
292 }
293 }
294
295 pub fn metrics_handle(&self) -> MetricsHandle {
297 MetricsHandle {
298 shared: self.shared.clone(),
299 }
300 }
301}
302
303impl TerminationHandle {
304 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
306 self.shared.set_termination_hook(hook);
307 }
308}
309
310impl MetricsHandle {
311 pub fn tx_bytes(&self) -> u64 {
313 self.shared.tx_bytes()
314 }
315
316 pub fn rx_bytes(&self) -> u64 {
318 self.shared.rx_bytes()
319 }
320}
321
322fn derive_guest_mac(slot: u64) -> [u8; 6] {
330 let s = slot.to_be_bytes();
331 [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
332}
333
334fn derive_gateway_mac(slot: u64) -> [u8; 6] {
338 let s = slot.to_be_bytes();
339 [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
340}
341
342fn derive_guest_ipv4(pool: Ipv4Network, slot: u64) -> Ipv4Addr {
347 assert!(
348 pool.prefix() <= 30,
349 "IPv4 pool {pool} must be large enough to contain at least one /30 block"
350 );
351
352 let capacity = 1u64 << (30 - pool.prefix());
353 assert!(
354 slot < capacity,
355 "sandbox slot {slot} exceeds IPv4 pool {pool} capacity ({capacity} /30 blocks)"
356 );
357
358 let base = u32::from(pool.network());
359 let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
361}
362
363fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
365 Ipv4Addr::from(u32::from(guest) - 1)
366}
367
368fn default_guest_ipv4_pool() -> Ipv4Network {
369 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
370 .expect("default IPv4 pool must be a valid network")
371}
372
373fn derive_guest_ipv6(pool: Ipv6Network, slot: u64) -> Ipv6Addr {
378 assert!(
379 pool.prefix() <= 64,
380 "IPv6 pool {pool} must be large enough to contain at least one /64 prefix"
381 );
382
383 let capacity = 1u128 << (64 - pool.prefix());
384 assert!(
385 (slot as u128) < capacity,
386 "sandbox slot {slot} exceeds IPv6 pool {pool} capacity ({capacity} /64 prefixes)"
387 );
388
389 let base = u128::from(pool.network());
390 let offset = (slot as u128) << 64;
391 Ipv6Addr::from(base + offset + 2)
392}
393
394fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
396 let segs = guest.segments();
397 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
398}
399
400fn default_guest_ipv6_pool() -> Ipv6Network {
401 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
402 .expect("default IPv6 pool must be a valid network")
403}
404
405fn format_mac(mac: [u8; 6]) -> String {
407 format!(
408 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
409 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
410 )
411}
412
413fn host_has_ipv4_route() -> bool {
419 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
420 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
421 .is_ok()
422}
423
424fn host_has_ipv6_route() -> bool {
428 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
429 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
430 .is_ok()
431}
432
433#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn derive_addresses_slot_0() {
443 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
444 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
445 assert_eq!(
446 derive_guest_ipv4(default_guest_ipv4_pool(), 0),
447 Ipv4Addr::new(172, 16, 0, 2)
448 );
449 assert_eq!(
450 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
451 Ipv4Addr::new(172, 16, 0, 1)
452 );
453 }
454
455 #[test]
456 fn derive_addresses_slot_1() {
457 assert_eq!(
458 derive_guest_ipv4(default_guest_ipv4_pool(), 1),
459 Ipv4Addr::new(172, 16, 0, 6)
460 );
461 assert_eq!(
462 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
463 Ipv4Addr::new(172, 16, 0, 5)
464 );
465 }
466
467 #[test]
468 fn derive_addresses_custom_ipv4_pool() {
469 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
470 assert_eq!(derive_guest_ipv4(pool, 0), Ipv4Addr::new(172, 31, 240, 2));
471 assert_eq!(
472 derive_guest_ipv4(pool, 63),
473 Ipv4Addr::new(172, 31, 240, 254)
474 );
475 }
476
477 #[test]
478 fn derive_ipv6_slot_0() {
479 assert_eq!(
480 derive_guest_ipv6(default_guest_ipv6_pool(), 0),
481 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
482 );
483 assert_eq!(
484 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0)),
485 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
486 );
487 }
488
489 #[test]
490 fn derive_addresses_custom_ipv6_pool() {
491 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
492 assert_eq!(
493 derive_guest_ipv6(pool, 0),
494 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
495 );
496 assert_eq!(
497 derive_guest_ipv6(pool, 3),
498 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
499 );
500 }
501
502 #[test]
503 fn format_mac_address() {
504 assert_eq!(
505 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
506 "02:6d:73:00:00:01"
507 );
508 }
509
510 #[test]
511 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
512 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
513 let vars = net.guest_env_vars();
514
515 assert_eq!(vars.len(), 3);
516 assert_eq!(vars[0].0, ENV_NET);
517 assert!(vars[0].1.contains("iface=eth0"));
518 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
519 assert_eq!(vars[1].1, crate::HOST_ALIAS);
520 assert_eq!(vars[2].0, ENV_NET_IPV4);
521 assert!(vars[2].1.contains("/30"));
522 }
523
524 #[test]
525 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
526 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, true);
527 let vars = net.guest_env_vars();
528
529 assert_eq!(vars.len(), 4);
530 assert_eq!(vars[0].0, ENV_NET);
531 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
532 assert_eq!(vars[2].0, ENV_NET_IPV4);
533 assert_eq!(vars[3].0, ENV_NET_IPV6);
534 assert!(vars[3].1.contains("/64"));
535 }
536
537 #[test]
538 fn guest_env_vars_omit_ipv6_without_host_route() {
539 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
540 let vars = net.guest_env_vars();
541
542 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
543 }
544
545 #[test]
546 fn guest_env_vars_omit_ipv4_without_host_route() {
547 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, true);
548 let vars = net.guest_env_vars();
549
550 assert_eq!(vars.len(), 3);
551 assert_eq!(vars[0].0, ENV_NET);
552 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
553 assert_eq!(vars[2].0, ENV_NET_IPV6);
554 }
555
556 #[test]
557 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
558 let mut config = NetworkConfig::default();
559 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
560 let net = SmoltcpNetwork::new_with_routes(config, 0, true, false);
561 let vars = net.guest_env_vars();
562
563 let v6 = vars
564 .iter()
565 .find(|(k, _)| k == ENV_NET_IPV6)
566 .expect("explicit ipv6 should publish env var even without host route");
567 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
568 }
569
570 #[test]
571 fn neither_family_active_emits_only_base_env_vars() {
572 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false);
573 let vars = net.guest_env_vars();
574
575 assert_eq!(vars.len(), 2);
576 assert_eq!(vars[0].0, ENV_NET);
577 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
578 }
579}