1use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
9use std::sync::Arc;
10use std::thread::JoinHandle;
11
12use microsandbox_protocol::{ENV_HOST_ALIAS, ENV_NET, ENV_NET_IPV4, ENV_NET_IPV6};
13use msb_krun::backends::net::NetBackend;
14
15use crate::backend::SmoltcpBackend;
16use crate::config::NetworkConfig;
17use crate::shared::{DEFAULT_QUEUE_CAPACITY, SharedState};
18use crate::stack::{self, GatewayIps, PollLoopConfig};
19use crate::tls::state::TlsState;
20
21const MAX_SLOT: u64 = u16::MAX as u64;
30
31pub struct SmoltcpNetwork {
42 config: NetworkConfig,
43 shared: Arc<SharedState>,
44 backend: Option<SmoltcpBackend>,
45 poll_handle: Option<JoinHandle<()>>,
46
47 guest_mac: [u8; 6],
49 gateway_mac: [u8; 6],
50 mtu: u16,
51 guest_ipv4: Option<Ipv4Addr>,
54 gateway_ipv4: Option<Ipv4Addr>,
55 guest_ipv6: Option<Ipv6Addr>,
56 gateway_ipv6: Option<Ipv6Addr>,
57
58 tls_state: Option<Arc<TlsState>>,
60}
61
62#[derive(Clone)]
64pub struct TerminationHandle {
65 shared: Arc<SharedState>,
66}
67
68#[derive(Clone)]
70pub struct MetricsHandle {
71 shared: Arc<SharedState>,
72}
73
74impl SmoltcpNetwork {
79 pub fn new(config: NetworkConfig, slot: u64) -> Self {
92 Self::new_with_routes(config, slot, host_has_ipv4_route(), host_has_ipv6_route())
93 }
94
95 fn new_with_routes(
96 config: NetworkConfig,
97 slot: u64,
98 host_has_ipv4: bool,
99 host_has_ipv6: bool,
100 ) -> Self {
101 assert!(
102 slot <= MAX_SLOT,
103 "sandbox slot {slot} exceeds address pool capacity (max {MAX_SLOT})"
104 );
105
106 let guest_mac = config
107 .interface
108 .mac
109 .unwrap_or_else(|| derive_guest_mac(slot));
110 let gateway_mac = derive_gateway_mac(slot);
111 let mtu = config.interface.mtu.unwrap_or(1500);
112
113 let guest_ipv4 = config
114 .interface
115 .ipv4_address
116 .or_else(|| host_has_ipv4.then(|| derive_guest_ipv4(slot)));
117 let gateway_ipv4 = guest_ipv4.map(gateway_from_guest_ipv4);
118 let guest_ipv6 = config
119 .interface
120 .ipv6_address
121 .or_else(|| host_has_ipv6.then(|| derive_guest_ipv6(slot)));
122 let gateway_ipv6 = guest_ipv6.map(gateway_from_guest_ipv6);
123
124 let queue_capacity = config
125 .max_connections
126 .unwrap_or(DEFAULT_QUEUE_CAPACITY)
127 .max(DEFAULT_QUEUE_CAPACITY);
128 let shared = Arc::new(SharedState::new(queue_capacity));
129 let backend = SmoltcpBackend::new(shared.clone());
130
131 let tls_state = if config.tls.enabled {
132 Some(Arc::new(TlsState::new(
133 config.tls.clone(),
134 config.secrets.clone(),
135 )))
136 } else {
137 None
138 };
139
140 Self {
141 config,
142 shared,
143 backend: Some(backend),
144 poll_handle: None,
145 guest_mac,
146 gateway_mac,
147 mtu,
148 guest_ipv4,
149 gateway_ipv4,
150 guest_ipv6,
151 gateway_ipv6,
152 tls_state,
153 }
154 }
155
156 fn gateway_ips(&self) -> GatewayIps {
158 GatewayIps {
159 ipv4: self.gateway_ipv4,
160 ipv6: self.gateway_ipv6,
161 }
162 }
163
164 pub fn start(&mut self, tokio_handle: tokio::runtime::Handle) {
169 let shared = self.shared.clone();
170 let poll_config = PollLoopConfig {
171 gateway_mac: self.gateway_mac,
172 guest_mac: self.guest_mac,
173 gateway: self.gateway_ips(),
174 guest_ipv4: self.guest_ipv4,
175 guest_ipv6: self.guest_ipv6,
176 mtu: self.mtu as usize,
177 };
178 let network_policy = self.config.policy.clone();
179 let dns_config = self.config.dns.clone();
180 let tls_state = self.tls_state.clone();
181 let published_ports = self.config.ports.clone();
182 let max_connections = self.config.max_connections;
183
184 self.poll_handle = Some(
185 std::thread::Builder::new()
186 .name("smoltcp-poll".into())
187 .spawn(move || {
188 stack::smoltcp_poll_loop(
189 shared,
190 poll_config,
191 network_policy,
192 dns_config,
193 tls_state,
194 published_ports,
195 max_connections,
196 tokio_handle,
197 );
198 })
199 .expect("failed to spawn smoltcp poll thread"),
200 );
201 }
202
203 pub fn take_backend(&mut self) -> Box<dyn NetBackend + Send> {
205 Box::new(self.backend.take().expect("backend already taken"))
206 }
207
208 pub fn guest_mac(&self) -> [u8; 6] {
210 self.guest_mac
211 }
212
213 pub fn guest_env_vars(&self) -> Vec<(String, String)> {
218 let mut vars = vec![
219 (
220 ENV_NET.into(),
221 format!(
222 "iface=eth0,mac={},mtu={}",
223 format_mac(self.guest_mac),
224 self.mtu,
225 ),
226 ),
227 (ENV_HOST_ALIAS.into(), crate::HOST_ALIAS.into()),
228 ];
229
230 if let (Some(guest), Some(gateway)) = (self.guest_ipv4, self.gateway_ipv4) {
231 vars.push((
232 ENV_NET_IPV4.into(),
233 format!("addr={guest}/30,gw={gateway},dns={gateway}"),
234 ));
235 }
236
237 if let (Some(guest), Some(gateway)) = (self.guest_ipv6, self.gateway_ipv6) {
238 vars.push((
239 ENV_NET_IPV6.into(),
240 format!("addr={guest}/64,gw={gateway},dns={gateway}"),
241 ));
242 }
243
244 for secret in &self.config.secrets.secrets {
246 vars.push((secret.env_var.clone(), secret.placeholder.clone()));
247 }
248
249 vars
250 }
251
252 pub fn ca_cert_pem(&self) -> Option<Vec<u8>> {
256 self.tls_state.as_ref().map(|s| s.ca_cert_pem())
257 }
258
259 pub fn host_cas_cert_pem(&self) -> Option<Vec<u8>> {
267 if !self.config.trust_host_cas {
268 return None;
269 }
270 crate::tls::host_cas::collect_host_cas()
271 }
272
273 pub fn termination_handle(&self) -> TerminationHandle {
275 TerminationHandle {
276 shared: self.shared.clone(),
277 }
278 }
279
280 pub fn metrics_handle(&self) -> MetricsHandle {
282 MetricsHandle {
283 shared: self.shared.clone(),
284 }
285 }
286}
287
288impl TerminationHandle {
289 pub fn set_hook(&self, hook: Arc<dyn Fn() + Send + Sync>) {
291 self.shared.set_termination_hook(hook);
292 }
293}
294
295impl MetricsHandle {
296 pub fn tx_bytes(&self) -> u64 {
298 self.shared.tx_bytes()
299 }
300
301 pub fn rx_bytes(&self) -> u64 {
303 self.shared.rx_bytes()
304 }
305}
306
307fn derive_guest_mac(slot: u64) -> [u8; 6] {
315 let s = slot.to_be_bytes();
316 [0x02, 0x6d, 0x73, s[6], s[7], 0x02]
317}
318
319fn derive_gateway_mac(slot: u64) -> [u8; 6] {
323 let s = slot.to_be_bytes();
324 [0x02, 0x6d, 0x73, s[6], s[7], 0x01]
325}
326
327fn derive_guest_ipv4(slot: u64) -> Ipv4Addr {
332 let base: u32 = u32::from(Ipv4Addr::new(100, 96, 0, 0));
333 let offset = (slot as u32) * 4 + 2; Ipv4Addr::from(base + offset)
335}
336
337fn gateway_from_guest_ipv4(guest: Ipv4Addr) -> Ipv4Addr {
339 Ipv4Addr::from(u32::from(guest) - 1)
340}
341
342fn derive_guest_ipv6(slot: u64) -> Ipv6Addr {
347 Ipv6Addr::new(0xfd42, 0x6d73, 0x0062, slot as u16, 0, 0, 0, 2)
348}
349
350fn gateway_from_guest_ipv6(guest: Ipv6Addr) -> Ipv6Addr {
352 let segs = guest.segments();
353 Ipv6Addr::new(segs[0], segs[1], segs[2], segs[3], 0, 0, 0, 1)
354}
355
356fn format_mac(mac: [u8; 6]) -> String {
358 format!(
359 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
360 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
361 )
362}
363
364fn host_has_ipv4_route() -> bool {
370 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))
371 .and_then(|socket| socket.connect((Ipv4Addr::new(192, 0, 2, 1), 443)))
372 .is_ok()
373}
374
375fn host_has_ipv6_route() -> bool {
379 UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0))
380 .and_then(|socket| socket.connect((Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1), 443)))
381 .is_ok()
382}
383
384#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn derive_addresses_slot_0() {
394 assert_eq!(derive_guest_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x02]);
395 assert_eq!(derive_gateway_mac(0), [0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]);
396 assert_eq!(derive_guest_ipv4(0), Ipv4Addr::new(100, 96, 0, 2));
397 assert_eq!(
398 gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 2)),
399 Ipv4Addr::new(100, 96, 0, 1)
400 );
401 }
402
403 #[test]
404 fn derive_addresses_slot_1() {
405 assert_eq!(derive_guest_ipv4(1), Ipv4Addr::new(100, 96, 0, 6));
406 assert_eq!(
407 gateway_from_guest_ipv4(Ipv4Addr::new(100, 96, 0, 6)),
408 Ipv4Addr::new(100, 96, 0, 5)
409 );
410 }
411
412 #[test]
413 fn derive_ipv6_slot_0() {
414 assert_eq!(
415 derive_guest_ipv6(0),
416 "fd42:6d73:62:0::2".parse::<Ipv6Addr>().unwrap()
417 );
418 assert_eq!(
419 gateway_from_guest_ipv6(derive_guest_ipv6(0)),
420 "fd42:6d73:62:0::1".parse::<Ipv6Addr>().unwrap()
421 );
422 }
423
424 #[test]
425 fn format_mac_address() {
426 assert_eq!(
427 format_mac([0x02, 0x6d, 0x73, 0x00, 0x00, 0x01]),
428 "02:6d:73:00:00:01"
429 );
430 }
431
432 #[test]
433 fn guest_env_vars_includes_ipv4_when_host_has_v4_route() {
434 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
435 let vars = net.guest_env_vars();
436
437 assert_eq!(vars.len(), 3);
438 assert_eq!(vars[0].0, ENV_NET);
439 assert!(vars[0].1.contains("iface=eth0"));
440 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
441 assert_eq!(vars[1].1, crate::HOST_ALIAS);
442 assert_eq!(vars[2].0, ENV_NET_IPV4);
443 assert!(vars[2].1.contains("/30"));
444 }
445
446 #[test]
447 fn guest_env_vars_includes_ipv6_when_host_has_v6_route() {
448 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, true);
449 let vars = net.guest_env_vars();
450
451 assert_eq!(vars.len(), 4);
452 assert_eq!(vars[0].0, ENV_NET);
453 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
454 assert_eq!(vars[2].0, ENV_NET_IPV4);
455 assert_eq!(vars[3].0, ENV_NET_IPV6);
456 assert!(vars[3].1.contains("/64"));
457 }
458
459 #[test]
460 fn guest_env_vars_omit_ipv6_without_host_route() {
461 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, true, false);
462 let vars = net.guest_env_vars();
463
464 assert!(!vars.iter().any(|(k, _)| k == ENV_NET_IPV6));
465 }
466
467 #[test]
468 fn guest_env_vars_omit_ipv4_without_host_route() {
469 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, true);
470 let vars = net.guest_env_vars();
471
472 assert_eq!(vars.len(), 3);
473 assert_eq!(vars[0].0, ENV_NET);
474 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
475 assert_eq!(vars[2].0, ENV_NET_IPV6);
476 }
477
478 #[test]
479 fn explicit_ipv6_address_overrides_missing_host_v6_route() {
480 let mut config = NetworkConfig::default();
481 config.interface.ipv6_address = Some("fd42:6d73:62:99::2".parse().unwrap());
482 let net = SmoltcpNetwork::new_with_routes(config, 0, true, false);
483 let vars = net.guest_env_vars();
484
485 let v6 = vars
486 .iter()
487 .find(|(k, _)| k == ENV_NET_IPV6)
488 .expect("explicit ipv6 should publish env var even without host route");
489 assert!(v6.1.contains("fd42:6d73:62:99::2/64"));
490 }
491
492 #[test]
493 fn neither_family_active_emits_only_base_env_vars() {
494 let net = SmoltcpNetwork::new_with_routes(NetworkConfig::default(), 0, false, false);
495 let vars = net.guest_env_vars();
496
497 assert_eq!(vars.len(), 2);
498 assert_eq!(vars[0].0, ENV_NET);
499 assert_eq!(vars[1].0, ENV_HOST_ALIAS);
500 }
501}