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 let secrets = Arc::new(self.config.secrets.clone());
199
200 self.poll_handle = Some(
201 std::thread::Builder::new()
202 .name("smoltcp-poll".into())
203 .spawn(move || {
204 stack::smoltcp_poll_loop(
205 shared,
206 poll_config,
207 network_policy,
208 dns_config,
209 tls_state,
210 published_ports,
211 max_connections,
212 tokio_handle,
213 secrets,
214 );
215 })
216 .expect("failed to spawn smoltcp poll thread"),
217 );
218 }
219
220 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
222 Box::new(self.backend.take().expect("backend already taken"))
223 }
224
225 pub fn guest_mac(&self) -> [u8; 6] {
227 self.guest_mac
228 }
229
230 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
235 let mut vars = vec![
236 (
237 ENV_NET.into(),
238 format!(
239 "iface=eth0,mac={},mtu={}",
240 format_mac(self.guest_mac),
241 self.mtu,
242 ),
243 ),
244 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
245 ];
246
247 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
248 vars.push((
249 ENV_NET_IPV4.into(),
250 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
251 ));
252 }
253
254 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
255 vars.push((
256 ENV_NET_IPV6.into(),
257 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
258 ));
259 }
260
261 for secret in &self.config.secrets.secrets {
263 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
264 }
265
266 vars
267 }
268
269 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
273 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
274 }
275
276 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
284 if !self.config.trust_host_cas {
285 return None;
286 }
287 crate::tls::host_cas::collect_host_cas()
288 }
289
290 pub fn termination_handle(&self) -> TerminationHandle {
292 TerminationHandle {
293 shared: self.shared.clone(),
294 }
295 }
296
297 pub fn metrics_handle(&self) -> MetricsHandle {
299 MetricsHandle {
300 shared: self.shared.clone(),
301 }
302 }
303}
304
305impl TerminationHandle {
306 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
308 self.shared.set_termination_hook(hook);
309 }
310}
311
312impl MetricsHandle {
313 pub fn tx_bytes(&self) -> u64 {
315 self.shared.tx_bytes()
316 }
317
318 pub fn rx_bytes(&self) -> u64 {
320 self.shared.rx_bytes()
321 }
322}
323
324fn derive_guest_mac(slot: u64) -> [u8; 6] {
332 let s = slot.to_be_bytes();
333 [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
334}
335
336fn derive_gateway_mac(slot: u64) -> [u8; 6] {
340 let s = slot.to_be_bytes();
341 [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
342}
343
344fn derive_guest_ipv4(pool: Ipv4Network, slot: u64) -> Ipv4Addr {
349 assert!(
350 pool.prefix() <= 30,
351 "IPv4 pool {pool} must be large enough to contain at least one /30 block"
352 );
353
354 let capacity = 1u64 << (30 - pool.prefix());
355 assert!(
356 slot < capacity,
357 "sandbox slot {slot} exceeds IPv4 pool {pool} capacity ({capacity} /30 blocks)"
358 );
359
360 let base = u32::from(pool.network());
361 let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
363}
364
365fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
367 Ipv4Addr::from(u32::from(guest) - 1)
368}
369
370fn default_guest_ipv4_pool() -> Ipv4Network {
371 Ipv4Network::new(Ipv4Addr::new(172, 16, 0, 0), 12)
372 .expect("default IPv4 pool must be a valid network")
373}
374
375fn derive_guest_ipv6(pool: Ipv6Network, slot: u64) -> Ipv6Addr {
380 assert!(
381 pool.prefix() <= 64,
382 "IPv6 pool {pool} must be large enough to contain at least one /64 prefix"
383 );
384
385 let capacity = 1u128 << (64 - pool.prefix());
386 assert!(
387 (slot as u128) < capacity,
388 "sandbox slot {slot} exceeds IPv6 pool {pool} capacity ({capacity} /64 prefixes)"
389 );
390
391 let base = u128::from(pool.network());
392 let offset = (slot as u128) << 64;
393 Ipv6Addr::from(base + offset + 2)
394}
395
396fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
398 let segs = guest.segments();
399 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
400}
401
402fn default_guest_ipv6_pool() -> Ipv6Network {
403 Ipv6Network::new(Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, 0, 0, 0, 0, 0), 48)
404 .expect("default IPv6 pool must be a valid network")
405}
406
407fn format_mac(mac: [u8; 6]) -> String {
409 format!(
410 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
411 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
412 )
413}
414
415fn host_has_ipv4_route() -> bool {
421 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
422 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
423 .is_ok()
424}
425
426fn host_has_ipv6_route() -> bool {
430 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
431 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
432 .is_ok()
433}
434
435#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn derive_addresses_slot_0() {
445 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
446 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
447 assert_eq!(
448 derive_guest_ipv4(default_guest_ipv4_pool(), 0),
449 Ipv4Addr::new(172, 16, 0, 2)
450 );
451 assert_eq!(
452 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 2)),
453 Ipv4Addr::new(172, 16, 0, 1)
454 );
455 }
456
457 #[test]
458 fn derive_addresses_slot_1() {
459 assert_eq!(
460 derive_guest_ipv4(default_guest_ipv4_pool(), 1),
461 Ipv4Addr::new(172, 16, 0, 6)
462 );
463 assert_eq!(
464 gateway_from_guest_ipv4(Ipv4Addr::new(172, 16, 0, 6)),
465 Ipv4Addr::new(172, 16, 0, 5)
466 );
467 }
468
469 #[test]
470 fn derive_addresses_custom_ipv4_pool() {
471 let pool = "172.31.240.0/24".parse::<Ipv4Network>().unwrap();
472 assert_eq!(derive_guest_ipv4(pool, 0), Ipv4Addr::new(172, 31, 240, 2));
473 assert_eq!(
474 derive_guest_ipv4(pool, 63),
475 Ipv4Addr::new(172, 31, 240, 254)
476 );
477 }
478
479 #[test]
480 fn derive_ipv6_slot_0() {
481 assert_eq!(
482 derive_guest_ipv6(default_guest_ipv6_pool(), 0),
483 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
484 );
485 assert_eq!(
486 gateway_from_guest_ipv6(derive_guest_ipv6(default_guest_ipv6_pool(), 0)),
487 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
488 );
489 }
490
491 #[test]
492 fn derive_addresses_custom_ipv6_pool() {
493 let pool = "fd7a:115c:a1e0:100::/56".parse::<Ipv6Network>().unwrap();
494 assert_eq!(
495 derive_guest_ipv6(pool, 0),
496 "fd7a:115c:a1e0:100::2".parse::<Ipv6Addr>().unwrap()
497 );
498 assert_eq!(
499 derive_guest_ipv6(pool, 3),
500 "fd7a:115c:a1e0:103::2".parse::<Ipv6Addr>().unwrap()
501 );
502 }
503
504 #[test]
505 fn format_mac_address() {
506 assert_eq!(
507 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
508 "02:6d:73:00:00:01"
509 );
510 }
511
512 #[test]
513 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
514 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
515 let vars = net.guest_env_vars();
516
517 assert_eq!(vars.len(), 3);
518 assert_eq!(vars[0].0, ENV_NET);
519 assert!(vars[0].1.contains("iface=eth0"));
520 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
521 assert_eq!(vars[1].1, crate::HOST_ALIAS);
522 assert_eq!(vars[2].0, ENV_NET_IPV4);
523 assert!(vars[2].1.contains("/30"));
524 }
525
526 #[test]
527 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
528 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, true);
529 let vars = net.guest_env_vars();
530
531 assert_eq!(vars.len(), 4);
532 assert_eq!(vars[0].0, ENV_NET);
533 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
534 assert_eq!(vars[2].0, ENV_NET_IPV4);
535 assert_eq!(vars[3].0, ENV_NET_IPV6);
536 assert!(vars[3].1.contains("/64"));
537 }
538
539 #[test]
540 fn guest_env_vars_omit_ipv6_without_host_route() {
541 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
542 let vars = net.guest_env_vars();
543
544 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
545 }
546
547 #[test]
548 fn guest_env_vars_omit_ipv4_without_host_route() {
549 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, true);
550 let vars = net.guest_env_vars();
551
552 assert_eq!(vars.len(), 3);
553 assert_eq!(vars[0].0, ENV_NET);
554 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
555 assert_eq!(vars[2].0, ENV_NET_IPV6);
556 }
557
558 #[test]
559 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
560 let mut config = NetworkConfig::default();
561 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
562 let net = SmoltcpNetwork::new_with_routes(config, 0, true, false);
563 let vars = net.guest_env_vars();
564
565 let v6 = vars
566 .iter()
567 .find(|(k, _)| k == ENV_NET_IPV6)
568 .expect("explicit ipv6 should publish env var even without host route");
569 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
570 }
571
572 #[test]
573 fn neither_family_active_emits_only_base_env_vars() {
574 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false);
575 let vars = net.guest_env_vars();
576
577 assert_eq!(vars.len(), 2);
578 assert_eq!(vars[0].0, ENV_NET);
579 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
580 }
581}