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