1use std::collections::HashSet;
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12
13#[cfg(windows)]
14use msb_krun_utils::event::{EventSet, EventSource, WaitContext, WaitEvent};
15use smoltcp::iface::{Config, Interface, SocketSet};
16use smoltcp::time::Instant;
17
18use smoltcp::wire::{
19 EthernetAddress, EthernetFrame, EthernetProtocol, HardwareAddress, Icmpv4Packet, Icmpv4Repr,
20 Icmpv6Packet, Icmpv6Repr, IpAddress, IpCidr, IpProtocol, Ipv4Packet, Ipv4Repr, Ipv6Packet,
21 Ipv6Repr, TcpPacket, UdpPacket,
22};
23
24use crate::config::{DnsConfig, PublishedPort};
25use crate::dns::common::ports::DnsPortType;
26use crate::dns::{
27 interceptor::DnsInterceptor,
28 proxies::{dot::DotProxy, tcp::DnsTcpProxy},
29};
30use crate::icmp::relay::IcmpRelay;
31use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
32use crate::ports::PortPublisher;
33use crate::proxy::ResolvedOutboundProxy;
34use crate::secrets::handle::SecretsHandle;
35use crate::tcp::{connection::ConnectionTracker, proxy::TcpProxy, upstream::UpstreamTcpTarget};
36use crate::tls::{proxy::TlsProxy, state::TlsState};
37use crate::udp::fragments::{
38 Ipv4UdpFragmentReassembler, Ipv6UdpFragmentReassembler, ReassembledUdpDatagram,
39 is_ipv4_udp_fragment, is_ipv6_fragment, is_ipv6_udp_fragment,
40};
41use crate::udp::relay::UdpRelay;
42
43use super::{device::SmoltcpDevice, shared::SharedState};
44
45#[cfg(windows)]
50const TX_WAKE_TOKEN: u64 = 1;
51
52#[cfg(windows)]
53const PROXY_WAKE_TOKEN: u64 = 2;
54
55pub enum FrameAction {
66 TcpSyn { src: SocketAddr, dst: SocketAddr },
69
70 UdpRelay { src: SocketAddr, dst: SocketAddr },
73
74 Dns,
76
77 Ipv4UdpFragment,
79
80 Ipv6UdpFragment,
82
83 Ipv6UnsupportedFragment,
85
86 Passthrough,
89}
90
91struct GatewayIcmpReply {
93 dst: IpAddr,
94 protocol: Protocol,
95 frame: Vec<u8>,
96}
97
98pub struct PollLoopConfig {
101 pub gateway_mac: [u8; 6],
103 pub guest_mac: [u8; 6],
105 pub gateway: GatewayIps,
109 pub guest_ipv4: Option<Ipv4Addr>,
111 pub guest_ipv6: Option<Ipv6Addr>,
113 pub mtu: usize,
115}
116
117#[derive(Debug, Clone, Copy)]
122pub struct GatewayIps {
123 pub ipv4: Option<Ipv4Addr>,
125 pub ipv6: Option<Ipv6Addr>,
127}
128
129pub fn classify_frame(frame: &[u8]) -> FrameAction {
139 let Ok(eth) = EthernetFrame::new_checked(frame) else {
140 return FrameAction::Passthrough;
141 };
142
143 match eth.ethertype() {
144 EthernetProtocol::Ipv4 => classify_ipv4(eth.payload()),
145 EthernetProtocol::Ipv6 => classify_ipv6(eth.payload()),
146 _ => FrameAction::Passthrough, }
148}
149
150pub fn create_interface(device: &mut SmoltcpDevice, config: &PollLoopConfig) -> Interface {
157 let hw_addr = HardwareAddress::Ethernet(EthernetAddress(config.gateway_mac));
158 let iface_config = Config::new(hw_addr);
159 let mut iface = Interface::new(iface_config, device, smoltcp_now());
160
161 iface.update_ip_addrs(|addrs| {
163 if let Some(ipv4) = config.gateway.ipv4 {
164 addrs
165 .push(IpCidr::new(IpAddress::Ipv4(ipv4), 30)) .expect("failed to add gateway IPv4 address");
167 }
168 if let Some(ipv6) = config.gateway.ipv6 {
169 addrs
170 .push(IpCidr::new(IpAddress::Ipv6(ipv6), 64))
171 .expect("failed to add gateway IPv6 address");
172 }
173 });
174
175 if let Some(ipv4) = config.gateway.ipv4 {
177 iface
178 .routes_mut()
179 .add_default_ipv4_route(ipv4)
180 .expect("failed to add default IPv4 route");
181 }
182 if let Some(ipv6) = config.gateway.ipv6 {
183 iface
184 .routes_mut()
185 .add_default_ipv6_route(ipv6)
186 .expect("failed to add default IPv6 route");
187 }
188
189 iface.set_any_ip(true);
191
192 iface
193}
194
195#[allow(clippy::too_many_arguments)]
227pub fn smoltcp_poll_loop(
228 shared: Arc<SharedState>,
229 config: PollLoopConfig,
230 network_policy: NetworkPolicy,
231 platform_policy: Option<NetworkPolicy>,
232 dns_config: DnsConfig,
233 tls_state: Option<Arc<TlsState>>,
234 published_ports: Vec<PublishedPort>,
235 max_connections: Option<usize>,
236 tokio_handle: tokio::runtime::Handle,
237 secrets: SecretsHandle,
238 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
239) {
240 let mut device = SmoltcpDevice::new(shared.clone(), config.mtu);
241 let mut iface = create_interface(&mut device, &config);
242 let mut sockets = SocketSet::new(vec![]);
243 let mut conn_tracker = ConnectionTracker::new(max_connections);
244
245 let gateway_ips: Arc<HashSet<IpAddr>> = Arc::new(
251 config
252 .gateway
253 .ipv4
254 .map(IpAddr::V4)
255 .into_iter()
256 .chain(config.gateway.ipv6.map(IpAddr::V6))
257 .collect(),
258 );
259 shared.set_gateway_ips(config.gateway.ipv4, config.gateway.ipv6);
262 let network_policy = Arc::new(network_policy);
263 let platform_policy = platform_policy.map(Arc::new);
264
265 let (mut dns_interceptor, dns_forwarder_handle) = DnsInterceptor::new(
266 &mut sockets,
267 dns_config,
268 shared.clone(),
269 &tokio_handle,
270 gateway_ips,
271 network_policy.clone(),
272 platform_policy.clone(),
273 config.gateway,
274 config.gateway_mac,
275 config.guest_mac,
276 );
277 let mut port_publisher = PortPublisher::new(
278 &published_ports,
279 config.guest_ipv4,
280 config.guest_ipv6,
281 config.gateway.ipv4,
282 config.gateway.ipv6,
283 config.gateway_mac,
284 config.guest_mac,
285 network_policy.clone(),
286 shared.clone(),
287 &tokio_handle,
288 );
289 let mut udp_relay = UdpRelay::new(
290 shared.clone(),
291 config.gateway_mac,
292 config.guest_mac,
293 config.mtu,
294 tokio_handle.clone(),
295 outbound_proxy.clone(),
296 );
297 udp_relay.attach_dns_forwarder(dns_forwarder_handle.clone());
298 let mut udp_fragments = Ipv4UdpFragmentReassembler::new();
299 let mut ipv6_udp_fragments = Ipv6UdpFragmentReassembler::new();
300 let icmp_relay = IcmpRelay::new(
301 shared.clone(),
302 config.gateway_mac,
303 config.guest_mac,
304 tokio_handle.clone(),
305 );
306
307 let mut last_cleanup = std::time::Instant::now();
309
310 #[cfg(unix)]
312 let mut poll_fds = [
313 libc::pollfd {
314 fd: shared.tx_wake.as_raw_fd(),
315 events: libc::POLLIN,
316 revents: 0,
317 },
318 libc::pollfd {
319 fd: shared.proxy_wake.as_raw_fd(),
320 events: libc::POLLIN,
321 revents: 0,
322 },
323 ];
324 #[cfg(windows)]
325 let wait_context = match windows_stack_wait_context(&shared) {
326 Ok(context) => context,
327 Err(err) => {
328 tracing::error!(error = %err, "network poll loop: failed to create wait context");
329 return;
330 }
331 };
332
333 loop {
334 let now = smoltcp_now();
335
336 while let Some(frame) = device.stage_next_frame() {
338 if handle_gateway_icmp_echo(
339 frame,
340 &config,
341 &shared,
342 &network_policy,
343 platform_policy.as_deref(),
344 ) {
345 device.drop_staged_frame();
346 continue;
347 }
348
349 if icmp_relay.relay_outbound_if_echo(
350 frame,
351 &config,
352 &network_policy,
353 platform_policy.as_deref(),
354 ) {
355 device.drop_staged_frame();
356 continue;
357 }
358
359 match classify_frame(frame) {
360 FrameAction::TcpSyn { src, dst } => {
361 let allow = match DnsPortType::from_tcp(dst.port()) {
362 DnsPortType::Dns => true,
366 DnsPortType::EncryptedDns => {
375 if tls_state.is_some() {
376 true
377 } else {
378 tracing::debug!(%dst, "DoT port refused (TLS interception not configured); stub should fall back to TCP/53");
379 false
380 }
381 }
382 DnsPortType::AlternativeDns => {
388 tracing::debug!(%dst, "alternative-DNS TCP port refused; stub should fall back to TCP/53");
389 false
390 }
391 DnsPortType::Other => {
394 let platform_allows = platform_policy.as_deref().is_none_or(|policy| {
395 policy
396 .evaluate_egress(dst, Protocol::Tcp, &shared)
397 .is_allow()
398 });
399 platform_allows
400 && matches!(
401 network_policy.evaluate_egress_with_source(
402 dst,
403 Protocol::Tcp,
404 &shared,
405 HostnameSource::Deferred,
406 ),
407 EgressEvaluation::Allow | EgressEvaluation::DeferUntilHostname
408 )
409 }
410 };
411 if allow && !conn_tracker.has_socket_for(&src, &dst) {
412 conn_tracker.create_tcp_socket(src, dst, &mut sockets);
413 }
414 iface.poll_ingress_single(now, &mut device, &mut sockets);
417 }
418
419 FrameAction::UdpRelay { src, dst } => {
420 relay_udp_frame(
421 frame,
422 src,
423 dst,
424 &config,
425 &network_policy,
426 platform_policy.as_deref(),
427 &shared,
428 &mut port_publisher,
429 tls_state.as_deref(),
430 &mut udp_relay,
431 );
432 device.drop_staged_frame();
433 }
434
435 FrameAction::Ipv4UdpFragment => {
436 if let Some(datagram) = udp_fragments.push(frame) {
437 handle_reassembled_udp_datagram(
438 datagram,
439 &mut device,
440 &mut iface,
441 now,
442 &mut sockets,
443 &config,
444 &network_policy,
445 platform_policy.as_deref(),
446 &shared,
447 &mut port_publisher,
448 tls_state.as_deref(),
449 &mut udp_relay,
450 );
451 } else {
452 device.drop_staged_frame();
453 }
454 }
455
456 FrameAction::Ipv6UdpFragment => {
457 if let Some(datagram) = ipv6_udp_fragments.push(frame) {
458 handle_reassembled_udp_datagram(
459 datagram,
460 &mut device,
461 &mut iface,
462 now,
463 &mut sockets,
464 &config,
465 &network_policy,
466 platform_policy.as_deref(),
467 &shared,
468 &mut port_publisher,
469 tls_state.as_deref(),
470 &mut udp_relay,
471 );
472 } else {
473 device.drop_staged_frame();
474 }
475 }
476
477 FrameAction::Ipv6UnsupportedFragment => {
478 device.drop_staged_frame();
482 }
483
484 FrameAction::Dns | FrameAction::Passthrough => {
485 iface.poll_ingress_single(now, &mut device, &mut sockets);
487 }
488 }
489 }
490
491 loop {
495 let result = iface.poll_egress(now, &mut device, &mut sockets);
496 if matches!(result, smoltcp::iface::PollResult::None) {
497 break;
498 }
499 }
500 iface.poll_maintenance(now);
501
502 if device.frames_emitted.swap(false, Ordering::Relaxed) {
505 shared.rx_wake.wake();
506 }
507
508 conn_tracker.relay_data(&mut sockets);
513 dns_interceptor.process(&mut sockets);
514
515 port_publisher.accept_inbound(&mut iface, &mut sockets, &shared, &tokio_handle);
517 port_publisher.relay_data(&mut sockets);
518
519 let new_conns = conn_tracker.take_new_connections(&mut sockets);
521 for conn in new_conns {
522 if let Some(ref tls_state) = tls_state
523 && tls_state
524 .config
525 .intercepted_ports
526 .contains(&conn.dst.port())
527 {
528 let connect_target = resolve_tcp_host_target(conn.dst, config.gateway);
530 let connection_outbound_proxy = ResolvedOutboundProxy::select_for_destination(
531 &outbound_proxy,
532 conn.dst,
533 connect_target.primary(),
534 );
535 let proxy = TlsProxy::new(
536 conn.dst,
537 connect_target,
538 conn.from_smoltcp,
539 conn.to_smoltcp,
540 shared.clone(),
541 tls_state.clone(),
542 network_policy.clone(),
543 conn.proxy_connect,
544 connection_outbound_proxy,
545 );
546 tokio_handle.spawn(proxy.run());
547 continue;
548 }
549 if conn.dst.port() == 53 {
550 conn.proxy_connect.mark_connected();
557
558 let proxy = DnsTcpProxy::new(
568 conn.dst,
569 conn.from_smoltcp,
570 conn.to_smoltcp,
571 dns_forwarder_handle.clone(),
572 shared.clone(),
573 );
574 tokio_handle.spawn(proxy.run());
575 continue;
576 }
577 if conn.dst.port() == 853
578 && let Some(ref tls_state) = tls_state
579 {
580 conn.proxy_connect.mark_connected();
582
583 let proxy = DotProxy::new(
589 conn.dst,
590 conn.from_smoltcp,
591 conn.to_smoltcp,
592 dns_forwarder_handle.clone(),
593 tls_state.clone(),
594 shared.clone(),
595 );
596 tokio_handle.spawn(proxy.run());
597 continue;
598 }
599 let connect_target = resolve_tcp_host_target(conn.dst, config.gateway);
601 let connection_outbound_proxy = ResolvedOutboundProxy::select_for_destination(
602 &outbound_proxy,
603 conn.dst,
604 connect_target.primary(),
605 );
606 let proxy = TcpProxy::new(
607 conn.dst,
608 connect_target,
609 conn.from_smoltcp,
610 conn.to_smoltcp,
611 shared.clone(),
612 network_policy.clone(),
613 secrets.load(),
616 tls_state.clone(),
617 conn.proxy_connect,
618 connection_outbound_proxy,
619 );
620 tokio_handle.spawn(proxy.run());
621 }
622
623 if last_cleanup.elapsed() >= std::time::Duration::from_secs(1) {
626 conn_tracker.cleanup_closed(&mut sockets);
627 port_publisher.cleanup_closed(&mut sockets);
628 udp_relay.cleanup_expired();
629 udp_fragments.cleanup_expired();
630 ipv6_udp_fragments.cleanup_expired();
631 shared.cleanup_resolved_hostnames();
632 last_cleanup = std::time::Instant::now();
633 }
634
635 loop {
638 let result = iface.poll_egress(now, &mut device, &mut sockets);
639 if matches!(result, smoltcp::iface::PollResult::None) {
640 break;
641 }
642 }
643
644 if device.frames_emitted.swap(false, Ordering::Relaxed) {
646 shared.rx_wake.wake();
647 }
648
649 let timeout_ms = iface
650 .poll_delay(now, &sockets)
651 .map(|d| d.total_millis().min(i32::MAX as u64) as i32)
652 .unwrap_or(100); #[cfg(unix)]
655 sleep_until_stack_wake(&shared, timeout_ms, &mut poll_fds);
656 #[cfg(windows)]
657 sleep_until_stack_wake_windows(&shared, timeout_ms, &wait_context);
658 }
659}
660
661#[cfg(unix)]
666fn sleep_until_stack_wake(shared: &SharedState, timeout_ms: i32, poll_fds: &mut [libc::pollfd; 2]) {
667 unsafe {
669 libc::poll(
670 poll_fds.as_mut_ptr(),
671 poll_fds.len() as libc::nfds_t,
672 timeout_ms,
673 );
674 }
675
676 if poll_fds[0].revents & libc::POLLIN != 0 {
677 shared.tx_wake.drain();
678 }
679 if poll_fds[1].revents & libc::POLLIN != 0 {
680 shared.proxy_wake.drain();
681 }
682}
683
684#[cfg(windows)]
685fn windows_stack_wait_context(shared: &SharedState) -> std::io::Result<WaitContext> {
686 let mut context = WaitContext::new();
687 context.add(
688 EventSource::waitable_handle(shared.tx_wake.as_raw_handle(), TX_WAKE_TOKEN),
689 EventSet::IN,
690 )?;
691 context.add(
692 EventSource::waitable_handle(shared.proxy_wake.as_raw_handle(), PROXY_WAKE_TOKEN),
693 EventSet::IN,
694 )?;
695 Ok(context)
696}
697
698#[cfg(windows)]
699fn sleep_until_stack_wake_windows(
700 shared: &SharedState,
701 timeout_ms: i32,
702 wait_context: &WaitContext,
703) {
704 let mut events = [WaitEvent::default(); 2];
705 let count = match wait_context.wait(timeout_ms, &mut events) {
706 Ok(count) => count,
707 Err(err) => {
708 tracing::warn!(error = %err, "network poll loop: wait failed");
709 return;
710 }
711 };
712
713 for event in events.iter().take(count) {
714 match event.token() {
715 TX_WAKE_TOKEN => shared.tx_wake.drain(),
716 PROXY_WAKE_TOKEN => shared.proxy_wake.drain(),
717 token => tracing::warn!(token, "network poll loop: unknown wake token"),
718 }
719 }
720}
721
722#[allow(clippy::too_many_arguments)]
724fn relay_udp_frame(
725 frame: &[u8],
726 src: SocketAddr,
727 dst: SocketAddr,
728 config: &PollLoopConfig,
729 network_policy: &NetworkPolicy,
730 platform_policy: Option<&NetworkPolicy>,
731 shared: &Arc<SharedState>,
732 port_publisher: &mut PortPublisher,
733 tls_state: Option<&TlsState>,
734 udp_relay: &mut UdpRelay,
735) {
736 if port_publisher.relay_udp_outbound(frame, src, dst) {
737 return;
738 }
739
740 if let Some(tls) = tls_state
742 && tls.config.intercepted_ports.contains(&dst.port())
743 && tls.config.block_quic_on_intercept
744 {
745 return;
746 }
747
748 match DnsPortType::from_udp(dst.port()) {
749 DnsPortType::Dns | DnsPortType::EncryptedDns => return,
752 DnsPortType::AlternativeDns => {
755 tracing::debug!(%dst, "alternative-DNS UDP port dropped; stub should fall back to UDP/53");
756 return;
757 }
758 DnsPortType::Other => {}
759 }
760
761 if platform_policy
763 .is_some_and(|policy| policy.evaluate_egress(dst, Protocol::Udp, shared).is_deny())
764 || network_policy
765 .evaluate_egress(dst, Protocol::Udp, shared)
766 .is_deny()
767 {
768 return;
769 }
770
771 let host_dst = resolve_host_dst(dst, config.gateway);
774 udp_relay.relay_outbound(frame, src, dst, host_dst);
775}
776
777#[allow(clippy::too_many_arguments)]
779fn handle_reassembled_udp_datagram(
780 datagram: ReassembledUdpDatagram,
781 device: &mut SmoltcpDevice,
782 iface: &mut Interface,
783 now: Instant,
784 sockets: &mut SocketSet<'_>,
785 config: &PollLoopConfig,
786 network_policy: &NetworkPolicy,
787 platform_policy: Option<&NetworkPolicy>,
788 shared: &Arc<SharedState>,
789 port_publisher: &mut PortPublisher,
790 tls_state: Option<&TlsState>,
791 udp_relay: &mut UdpRelay,
792) {
793 if DnsPortType::from_udp(datagram.dst.port()) == DnsPortType::Dns {
794 device.replace_staged_frame(datagram.frame);
795 iface.poll_ingress_single(now, device, sockets);
796 return;
797 }
798
799 relay_udp_frame(
800 &datagram.frame,
801 datagram.src,
802 datagram.dst,
803 config,
804 network_policy,
805 platform_policy,
806 shared,
807 port_publisher,
808 tls_state,
809 udp_relay,
810 );
811 device.drop_staged_frame();
812}
813
814fn resolve_tcp_host_target(dst: SocketAddr, gateway: GatewayIps) -> UpstreamTcpTarget {
820 let port = dst.port();
821 match dst.ip() {
822 IpAddr::V4(v4) if gateway.ipv4 == Some(v4) => UpstreamTcpTarget::with_fallback(
823 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
824 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port),
825 ),
826 IpAddr::V6(v6) if gateway.ipv6 == Some(v6) => UpstreamTcpTarget::with_fallback(
827 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port),
828 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
829 ),
830 _ => UpstreamTcpTarget::direct(dst),
831 }
832}
833
834pub(crate) fn resolve_host_dst(dst: SocketAddr, gateway: GatewayIps) -> SocketAddr {
844 match dst.ip() {
845 IpAddr::V4(v4) if gateway.ipv4 == Some(v4) => {
846 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), dst.port())
847 }
848 IpAddr::V6(v6) if gateway.ipv6 == Some(v6) => {
849 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), dst.port())
850 }
851 _ => dst,
852 }
853}
854
855fn smoltcp_now() -> Instant {
861 static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
862 let epoch = EPOCH.get_or_init(std::time::Instant::now);
863 let elapsed = epoch.elapsed();
864 Instant::from_millis(elapsed.as_millis() as i64)
865}
866
867fn handle_gateway_icmp_echo(
874 frame: &[u8],
875 config: &PollLoopConfig,
876 shared: &SharedState,
877 network_policy: &NetworkPolicy,
878 platform_policy: Option<&NetworkPolicy>,
879) -> bool {
880 let Ok(eth) = EthernetFrame::new_checked(frame) else {
881 return false;
882 };
883
884 let reply = match eth.ethertype() {
885 EthernetProtocol::Ipv4 => gateway_icmpv4_echo_reply(ð, config),
886 EthernetProtocol::Ipv6 => gateway_icmpv6_echo_reply(ð, config),
887 _ => None,
888 };
889 let Some(reply) = reply else {
890 return false;
891 };
892
893 let tenant_denied = network_policy
896 .evaluate_egress_ip(reply.dst, reply.protocol, shared)
897 .is_deny();
898 let platform_denied = platform_policy.is_some_and(|policy| {
899 policy
900 .evaluate_egress_ip(reply.dst, reply.protocol, shared)
901 .is_deny()
902 });
903 if tenant_denied || platform_denied {
904 tracing::debug!(
905 dst = %reply.dst,
906 tenant_denied,
907 platform_denied,
908 "gateway ICMP echo denied by policy",
909 );
910 return true;
911 }
912
913 shared.push_rx_frame_and_wake(reply.frame);
914
915 true
916}
917
918fn gateway_icmpv4_echo_reply(
920 eth: &EthernetFrame<&[u8]>,
921 config: &PollLoopConfig,
922) -> Option<GatewayIcmpReply> {
923 let gateway_ipv4 = config.gateway.ipv4?;
924 let ipv4 = Ipv4Packet::new_checked(eth.payload()).ok()?;
925 if ipv4.dst_addr() != gateway_ipv4 || ipv4.next_header() != IpProtocol::Icmp {
926 return None;
927 }
928
929 let icmp = Icmpv4Packet::new_checked(ipv4.payload()).ok()?;
930 let Icmpv4Repr::EchoRequest {
931 ident,
932 seq_no,
933 data,
934 } = Icmpv4Repr::parse(&icmp, &smoltcp::phy::ChecksumCapabilities::default()).ok()?
935 else {
936 return None;
937 };
938
939 let ipv4_repr = Ipv4Repr {
940 src_addr: gateway_ipv4,
941 dst_addr: ipv4.src_addr(),
942 next_header: IpProtocol::Icmp,
943 payload_len: 8 + data.len(),
944 hop_limit: 64,
945 };
946 let icmp_repr = Icmpv4Repr::EchoReply {
947 ident,
948 seq_no,
949 data,
950 };
951 let mut reply = vec![0u8; 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len()];
952
953 let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
954 reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
955 reply_eth.set_dst_addr(eth.src_addr());
956 reply_eth.set_ethertype(EthernetProtocol::Ipv4);
957
958 ipv4_repr.emit(
959 &mut Ipv4Packet::new_unchecked(&mut reply[14..34]),
960 &smoltcp::phy::ChecksumCapabilities::default(),
961 );
962 icmp_repr.emit(
963 &mut Icmpv4Packet::new_unchecked(&mut reply[34..]),
964 &smoltcp::phy::ChecksumCapabilities::default(),
965 );
966
967 Some(GatewayIcmpReply {
968 dst: IpAddr::V4(gateway_ipv4),
969 protocol: Protocol::Icmpv4,
970 frame: reply,
971 })
972}
973
974fn gateway_icmpv6_echo_reply(
976 eth: &EthernetFrame<&[u8]>,
977 config: &PollLoopConfig,
978) -> Option<GatewayIcmpReply> {
979 let gateway_ipv6 = config.gateway.ipv6?;
980 let ipv6 = Ipv6Packet::new_checked(eth.payload()).ok()?;
981 if ipv6.dst_addr() != gateway_ipv6 || ipv6.next_header() != IpProtocol::Icmpv6 {
982 return None;
983 }
984
985 let icmp = Icmpv6Packet::new_checked(ipv6.payload()).ok()?;
986 let Icmpv6Repr::EchoRequest {
987 ident,
988 seq_no,
989 data,
990 } = Icmpv6Repr::parse(
991 &ipv6.src_addr(),
992 &ipv6.dst_addr(),
993 &icmp,
994 &smoltcp::phy::ChecksumCapabilities::default(),
995 )
996 .ok()?
997 else {
998 return None;
999 };
1000
1001 let ipv6_repr = Ipv6Repr {
1002 src_addr: gateway_ipv6,
1003 dst_addr: ipv6.src_addr(),
1004 next_header: IpProtocol::Icmpv6,
1005 payload_len: icmp_repr_buffer_len_v6(data),
1006 hop_limit: 64,
1007 };
1008 let icmp_repr = Icmpv6Repr::EchoReply {
1009 ident,
1010 seq_no,
1011 data,
1012 };
1013 let ipv6_hdr_len = 40;
1014 let mut reply = vec![0u8; 14 + ipv6_hdr_len + icmp_repr.buffer_len()];
1015
1016 let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
1017 reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
1018 reply_eth.set_dst_addr(eth.src_addr());
1019 reply_eth.set_ethertype(EthernetProtocol::Ipv6);
1020
1021 ipv6_repr.emit(&mut Ipv6Packet::new_unchecked(&mut reply[14..54]));
1022 icmp_repr.emit(
1023 &gateway_ipv6,
1024 &ipv6.src_addr(),
1025 &mut Icmpv6Packet::new_unchecked(&mut reply[54..]),
1026 &smoltcp::phy::ChecksumCapabilities::default(),
1027 );
1028
1029 Some(GatewayIcmpReply {
1030 dst: IpAddr::V6(gateway_ipv6),
1031 protocol: Protocol::Icmpv6,
1032 frame: reply,
1033 })
1034}
1035
1036fn icmp_repr_buffer_len_v6(data: &[u8]) -> usize {
1037 Icmpv6Repr::EchoReply {
1038 ident: 0,
1039 seq_no: 0,
1040 data,
1041 }
1042 .buffer_len()
1043}
1044
1045fn classify_ipv4(payload: &[u8]) -> FrameAction {
1047 let Ok(ipv4) = Ipv4Packet::new_checked(payload) else {
1048 return FrameAction::Passthrough;
1049 };
1050 if is_ipv4_udp_fragment(&ipv4) {
1051 return FrameAction::Ipv4UdpFragment;
1052 }
1053 classify_transport(
1054 ipv4.next_header(),
1055 ipv4.src_addr().into(),
1056 ipv4.dst_addr().into(),
1057 ipv4.payload(),
1058 )
1059}
1060
1061fn classify_ipv6(payload: &[u8]) -> FrameAction {
1063 let Ok(ipv6) = Ipv6Packet::new_checked(payload) else {
1064 return FrameAction::Passthrough;
1065 };
1066 if is_ipv6_udp_fragment(&ipv6) {
1067 return FrameAction::Ipv6UdpFragment;
1068 }
1069 if is_ipv6_fragment(&ipv6) {
1070 return FrameAction::Ipv6UnsupportedFragment;
1071 }
1072 classify_transport(
1073 ipv6.next_header(),
1074 ipv6.src_addr().into(),
1075 ipv6.dst_addr().into(),
1076 ipv6.payload(),
1077 )
1078}
1079
1080fn classify_transport(
1082 protocol: IpProtocol,
1083 src_ip: std::net::IpAddr,
1084 dst_ip: std::net::IpAddr,
1085 transport_payload: &[u8],
1086) -> FrameAction {
1087 match protocol {
1088 IpProtocol::Tcp => {
1089 let Ok(tcp) = TcpPacket::new_checked(transport_payload) else {
1090 return FrameAction::Passthrough;
1091 };
1092 if tcp.syn() && !tcp.ack() {
1093 FrameAction::TcpSyn {
1094 src: SocketAddr::new(src_ip, tcp.src_port()),
1095 dst: SocketAddr::new(dst_ip, tcp.dst_port()),
1096 }
1097 } else {
1098 FrameAction::Passthrough
1099 }
1100 }
1101 IpProtocol::Udp => {
1102 let Ok(udp) = UdpPacket::new_checked(transport_payload) else {
1103 return FrameAction::Passthrough;
1104 };
1105 if DnsPortType::from_udp(udp.dst_port()) == DnsPortType::Dns {
1109 FrameAction::Dns
1110 } else {
1111 FrameAction::UdpRelay {
1112 src: SocketAddr::new(src_ip, udp.src_port()),
1113 dst: SocketAddr::new(dst_ip, udp.dst_port()),
1114 }
1115 }
1116 }
1117 _ => FrameAction::Passthrough, }
1119}
1120
1121#[cfg(test)]
1126mod tests {
1127 use super::*;
1128 use std::sync::Arc;
1129
1130 use smoltcp::phy::ChecksumCapabilities;
1131 use smoltcp::wire::{
1132 ArpOperation, ArpPacket, ArpRepr, EthernetRepr, Icmpv4Packet, Icmpv4Repr, Ipv4Repr,
1133 };
1134
1135 use super::super::{device::SmoltcpDevice, shared::SharedState};
1136 use crate::tcp::connection::NewConnection;
1137
1138 fn build_tcp_syn_frame(
1140 src_ip: [u8; 4],
1141 dst_ip: [u8; 4],
1142 src_port: u16,
1143 dst_port: u16,
1144 ) -> Vec<u8> {
1145 let mut frame = vec![0u8; 14 + 20 + 20]; frame[12] = 0x08; frame[13] = 0x00;
1150
1151 let ip = &mut frame[14..34];
1153 ip[0] = 0x45; let total_len = 40u16; ip[2..4].copy_from_slice(&total_len.to_be_bytes());
1156 ip[6] = 0x40; ip[8] = 64; ip[9] = 6; ip[12..16].copy_from_slice(&src_ip);
1160 ip[16..20].copy_from_slice(&dst_ip);
1161
1162 let tcp = &mut frame[34..54];
1164 tcp[0..2].copy_from_slice(&src_port.to_be_bytes());
1165 tcp[2..4].copy_from_slice(&dst_port.to_be_bytes());
1166 tcp[12] = 0x50; tcp[13] = 0x02; frame
1170 }
1171
1172 fn build_udp_frame(src_ip: [u8; 4], dst_ip: [u8; 4], src_port: u16, dst_port: u16) -> Vec<u8> {
1174 let mut frame = vec![0u8; 14 + 20 + 8]; frame[12] = 0x08;
1178 frame[13] = 0x00;
1179
1180 let ip = &mut frame[14..34];
1182 ip[0] = 0x45;
1183 let total_len = 28u16; ip[2..4].copy_from_slice(&total_len.to_be_bytes());
1185 ip[8] = 64;
1186 ip[9] = 17; ip[12..16].copy_from_slice(&src_ip);
1188 ip[16..20].copy_from_slice(&dst_ip);
1189
1190 let udp = &mut frame[34..42];
1192 udp[0..2].copy_from_slice(&src_port.to_be_bytes());
1193 udp[2..4].copy_from_slice(&dst_port.to_be_bytes());
1194 let udp_len = 8u16;
1195 udp[4..6].copy_from_slice(&udp_len.to_be_bytes());
1196
1197 frame
1198 }
1199
1200 fn build_icmpv4_echo_frame(
1202 src_mac: [u8; 6],
1203 dst_mac: [u8; 6],
1204 src_ip: [u8; 4],
1205 dst_ip: [u8; 4],
1206 ident: u16,
1207 seq_no: u16,
1208 data: &[u8],
1209 ) -> Vec<u8> {
1210 let ipv4_repr = Ipv4Repr {
1211 src_addr: Ipv4Addr::from(src_ip),
1212 dst_addr: Ipv4Addr::from(dst_ip),
1213 next_header: IpProtocol::Icmp,
1214 payload_len: 8 + data.len(),
1215 hop_limit: 64,
1216 };
1217 let icmp_repr = Icmpv4Repr::EchoRequest {
1218 ident,
1219 seq_no,
1220 data,
1221 };
1222 let frame_len = 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
1223 let mut frame = vec![0u8; frame_len];
1224
1225 let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
1226 EthernetRepr {
1227 src_addr: EthernetAddress(src_mac),
1228 dst_addr: EthernetAddress(dst_mac),
1229 ethertype: EthernetProtocol::Ipv4,
1230 }
1231 .emit(&mut eth_frame);
1232
1233 ipv4_repr.emit(
1234 &mut Ipv4Packet::new_unchecked(&mut frame[14..34]),
1235 &ChecksumCapabilities::default(),
1236 );
1237 icmp_repr.emit(
1238 &mut Icmpv4Packet::new_unchecked(&mut frame[34..]),
1239 &ChecksumCapabilities::default(),
1240 );
1241
1242 frame
1243 }
1244
1245 fn build_arp_request_frame(src_mac: [u8; 6], src_ip: [u8; 4], target_ip: [u8; 4]) -> Vec<u8> {
1247 let mut frame = vec![0u8; 14 + 28];
1248
1249 let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
1250 EthernetRepr {
1251 src_addr: EthernetAddress(src_mac),
1252 dst_addr: EthernetAddress([0xff; 6]),
1253 ethertype: EthernetProtocol::Arp,
1254 }
1255 .emit(&mut eth_frame);
1256
1257 ArpRepr::EthernetIpv4 {
1258 operation: ArpOperation::Request,
1259 source_hardware_addr: EthernetAddress(src_mac),
1260 source_protocol_addr: Ipv4Addr::from(src_ip),
1261 target_hardware_addr: EthernetAddress([0x00; 6]),
1262 target_protocol_addr: Ipv4Addr::from(target_ip),
1263 }
1264 .emit(&mut ArpPacket::new_unchecked(&mut frame[14..]));
1265
1266 frame
1267 }
1268
1269 #[test]
1270 fn classify_tcp_syn() {
1271 let frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
1272 match classify_frame(&frame) {
1273 FrameAction::TcpSyn { src, dst } => {
1274 assert_eq!(
1275 src,
1276 SocketAddr::new(Ipv4Addr::new(10, 0, 0, 2).into(), 54321)
1277 );
1278 assert_eq!(
1279 dst,
1280 SocketAddr::new(Ipv4Addr::new(93, 184, 216, 34).into(), 443)
1281 );
1282 }
1283 _ => panic!("expected TcpSyn"),
1284 }
1285 }
1286
1287 #[test]
1288 fn classify_tcp_ack_is_passthrough() {
1289 let mut frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
1290 frame[34 + 13] = 0x10; assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
1293 }
1294
1295 #[test]
1296 fn classify_udp_dns() {
1297 let frame = build_udp_frame([10, 0, 0, 2], [10, 0, 0, 1], 12345, 53);
1298 assert!(matches!(classify_frame(&frame), FrameAction::Dns));
1299 }
1300
1301 #[test]
1302 fn classify_udp_non_dns() {
1303 let frame = build_udp_frame([10, 0, 0, 2], [8, 8, 8, 8], 12345, 443);
1304 match classify_frame(&frame) {
1305 FrameAction::UdpRelay { src, dst } => {
1306 assert_eq!(src.port(), 12345);
1307 assert_eq!(dst.port(), 443);
1308 }
1309 _ => panic!("expected UdpRelay"),
1310 }
1311 }
1312
1313 #[test]
1314 fn classify_ipv4_udp_fragment() {
1315 let mut frame = build_udp_frame([10, 0, 0, 2], [8, 8, 8, 8], 12345, 443);
1316 frame[14 + 6] = 0x20; assert!(matches!(
1318 classify_frame(&frame),
1319 FrameAction::Ipv4UdpFragment
1320 ));
1321 }
1322
1323 #[test]
1324 fn classify_ipv6_udp_fragment() {
1325 let mut frame = vec![0u8; 14 + 40 + 8];
1326
1327 frame[12] = 0x86;
1328 frame[13] = 0xdd;
1329
1330 let ip = &mut frame[14..54];
1331 ip[0] = 0x60;
1332 ip[4..6].copy_from_slice(&8u16.to_be_bytes());
1333 ip[6] = u8::from(IpProtocol::Ipv6Frag);
1334 ip[7] = 64;
1335 ip[8..24].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1336 ip[24..40].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1337
1338 let fragment = &mut frame[54..62];
1339 fragment[0] = u8::from(IpProtocol::Udp);
1340 fragment[3] = 1; assert!(matches!(
1343 classify_frame(&frame),
1344 FrameAction::Ipv6UdpFragment
1345 ));
1346 }
1347
1348 #[test]
1349 fn classify_ipv6_non_udp_fragment_is_unsupported() {
1350 let mut frame = vec![0u8; 14 + 40 + 8];
1351
1352 frame[12] = 0x86;
1353 frame[13] = 0xdd;
1354
1355 let ip = &mut frame[14..54];
1356 ip[0] = 0x60;
1357 ip[4..6].copy_from_slice(&8u16.to_be_bytes());
1358 ip[6] = u8::from(IpProtocol::Ipv6Frag);
1359 ip[7] = 64;
1360 ip[8..24].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1361 ip[24..40].copy_from_slice(&Ipv6Addr::LOCALHOST.octets());
1362
1363 let fragment = &mut frame[54..62];
1364 fragment[0] = u8::from(IpProtocol::Tcp);
1365 fragment[3] = 1; assert!(matches!(
1368 classify_frame(&frame),
1369 FrameAction::Ipv6UnsupportedFragment
1370 ));
1371 }
1372
1373 #[test]
1374 fn classify_arp_is_passthrough() {
1375 let mut frame = vec![0u8; 42]; frame[12] = 0x08;
1377 frame[13] = 0x06; assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
1379 }
1380
1381 #[test]
1382 fn classify_garbage_is_passthrough() {
1383 assert!(matches!(classify_frame(&[]), FrameAction::Passthrough));
1384 assert!(matches!(classify_frame(&[0; 5]), FrameAction::Passthrough));
1385 }
1386
1387 #[test]
1388 fn gateway_replies_to_icmp_echo_requests() {
1389 fn drive_one_frame(
1390 device: &mut SmoltcpDevice,
1391 iface: &mut Interface,
1392 sockets: &mut SocketSet<'_>,
1393 shared: &Arc<SharedState>,
1394 poll_config: &PollLoopConfig,
1395 now: Instant,
1396 ) {
1397 let frame = device.stage_next_frame().expect("expected staged frame");
1398 if handle_gateway_icmp_echo(
1399 frame,
1400 poll_config,
1401 shared,
1402 &NetworkPolicy::allow_all(),
1403 None,
1404 ) {
1405 device.drop_staged_frame();
1406 return;
1407 }
1408 let _ = iface.poll_ingress_single(now, device, sockets);
1409 let _ = iface.poll_egress(now, device, sockets);
1410 }
1411
1412 let shared = Arc::new(SharedState::new(4));
1413 let poll_config = PollLoopConfig {
1414 gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
1415 guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
1416 gateway: GatewayIps {
1417 ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1418 ipv6: Some(Ipv6Addr::LOCALHOST),
1419 },
1420 guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
1421 guest_ipv6: None,
1422 mtu: 1500,
1423 };
1424 let guest_ipv4 = poll_config.guest_ipv4.unwrap();
1425 let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
1426 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
1427 let mut iface = create_interface(&mut device, &poll_config);
1428 let mut sockets = SocketSet::new(vec![]);
1429 let now = smoltcp_now();
1430
1431 shared
1434 .tx_ring
1435 .push(build_arp_request_frame(
1436 poll_config.guest_mac,
1437 guest_ipv4.octets(),
1438 gateway_ipv4.octets(),
1439 ))
1440 .unwrap();
1441 shared
1442 .tx_ring
1443 .push(build_icmpv4_echo_frame(
1444 poll_config.guest_mac,
1445 poll_config.gateway_mac,
1446 guest_ipv4.octets(),
1447 gateway_ipv4.octets(),
1448 0x1234,
1449 0xABCD,
1450 b"ping",
1451 ))
1452 .unwrap();
1453
1454 drive_one_frame(
1455 &mut device,
1456 &mut iface,
1457 &mut sockets,
1458 &shared,
1459 &poll_config,
1460 now,
1461 );
1462 let _ = shared.rx_ring.pop().expect("expected ARP reply");
1463
1464 drive_one_frame(
1465 &mut device,
1466 &mut iface,
1467 &mut sockets,
1468 &shared,
1469 &poll_config,
1470 now,
1471 );
1472
1473 let reply = shared.rx_ring.pop().expect("expected ICMP echo reply");
1474 let eth = EthernetFrame::new_checked(&reply).expect("valid ethernet frame");
1475 assert_eq!(eth.src_addr(), EthernetAddress(poll_config.gateway_mac));
1476 assert_eq!(eth.dst_addr(), EthernetAddress(poll_config.guest_mac));
1477 assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
1478
1479 let ipv4 = Ipv4Packet::new_checked(eth.payload()).expect("valid IPv4 packet");
1480 assert_eq!(ipv4.src_addr(), gateway_ipv4);
1481 assert_eq!(ipv4.dst_addr(), guest_ipv4);
1482 assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
1483
1484 let icmp = Icmpv4Packet::new_checked(ipv4.payload()).expect("valid ICMP packet");
1485 let icmp_repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default())
1486 .expect("valid ICMP echo reply");
1487 assert_eq!(
1488 icmp_repr,
1489 Icmpv4Repr::EchoReply {
1490 ident: 0x1234,
1491 seq_no: 0xABCD,
1492 data: b"ping",
1493 }
1494 );
1495 }
1496
1497 #[test]
1498 fn gateway_icmp_echo_respects_deny_policy() {
1499 let shared = SharedState::new(4);
1500 let poll_config = PollLoopConfig {
1501 gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
1502 guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
1503 gateway: GatewayIps {
1504 ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1505 ipv6: None,
1506 },
1507 guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
1508 guest_ipv6: None,
1509 mtu: 1500,
1510 };
1511 let policy = NetworkPolicy::builder().default_deny().build().unwrap();
1512 let frame = build_icmpv4_echo_frame(
1513 poll_config.guest_mac,
1514 poll_config.gateway_mac,
1515 poll_config.guest_ipv4.unwrap().octets(),
1516 poll_config.gateway.ipv4.unwrap().octets(),
1517 0x1234,
1518 0xABCD,
1519 b"ping",
1520 );
1521
1522 assert!(handle_gateway_icmp_echo(
1523 &frame,
1524 &poll_config,
1525 &shared,
1526 &policy,
1527 None,
1528 ));
1529 assert!(
1530 shared.rx_ring.pop().is_none(),
1531 "denied gateway ICMP should not queue a reply"
1532 );
1533 }
1534
1535 #[test]
1536 fn platform_public_floor_consumes_gateway_echo_without_replying() {
1537 let shared = SharedState::new(4);
1538 let gateway = Ipv4Addr::new(100, 96, 0, 1);
1539 let guest = Ipv4Addr::new(100, 96, 0, 2);
1540 shared.set_gateway_ips(Some(gateway), None);
1541 let config = PollLoopConfig {
1542 gateway_mac: [0x02, 0, 0, 0, 0, 1],
1543 guest_mac: [0x02, 0, 0, 0, 0, 2],
1544 gateway: GatewayIps {
1545 ipv4: Some(gateway),
1546 ipv6: None,
1547 },
1548 guest_ipv4: Some(guest),
1549 guest_ipv6: None,
1550 mtu: 1500,
1551 };
1552 let frame = build_icmpv4_echo_frame(
1553 config.guest_mac,
1554 config.gateway_mac,
1555 guest.octets(),
1556 gateway.octets(),
1557 1,
1558 1,
1559 b"ping",
1560 );
1561 let platform = NetworkPolicy::from_profiles([crate::policy::NetworkProfile::Public]);
1562
1563 assert!(handle_gateway_icmp_echo(
1564 &frame,
1565 &config,
1566 &shared,
1567 &NetworkPolicy::allow_all(),
1568 Some(&platform),
1569 ));
1570 assert!(shared.rx_ring.pop().is_none());
1571 }
1572
1573 fn test_gateway() -> GatewayIps {
1574 GatewayIps {
1575 ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1576 ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
1577 }
1578 }
1579
1580 #[test]
1581 fn resolve_tcp_host_target_ipv4_can_fall_back_to_ipv6() {
1582 let gw = test_gateway();
1583 let dst = SocketAddr::new(IpAddr::V4(gw.ipv4.unwrap()), 8080);
1584
1585 assert_eq!(
1586 resolve_tcp_host_target(dst, gw),
1587 UpstreamTcpTarget::with_fallback(
1588 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
1589 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080),
1590 )
1591 );
1592 }
1593
1594 #[test]
1595 fn resolve_tcp_host_target_ipv6_can_fall_back_to_ipv4() {
1596 let gw = test_gateway();
1597 let dst = SocketAddr::new(IpAddr::V6(gw.ipv6.unwrap()), 8080);
1598
1599 assert_eq!(
1600 resolve_tcp_host_target(dst, gw),
1601 UpstreamTcpTarget::with_fallback(
1602 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080),
1603 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080),
1604 )
1605 );
1606 }
1607
1608 #[test]
1609 fn resolve_tcp_host_target_external_has_no_fallback() {
1610 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 443);
1611
1612 assert_eq!(
1613 resolve_tcp_host_target(dst, test_gateway()),
1614 UpstreamTcpTarget::direct(dst)
1615 );
1616 }
1617
1618 #[test]
1619 fn resolve_host_dst_matches_ipv4() {
1620 let gw = test_gateway();
1621 let dst = SocketAddr::new(IpAddr::V4(gw.ipv4.unwrap()), 8080);
1622 assert_eq!(
1623 resolve_host_dst(dst, gw),
1624 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
1625 );
1626 }
1627
1628 #[test]
1629 fn resolve_host_dst_matches_ipv6() {
1630 let gw = test_gateway();
1631 let dst = SocketAddr::new(IpAddr::V6(gw.ipv6.unwrap()), 8080);
1632 assert_eq!(
1633 resolve_host_dst(dst, gw),
1634 SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080)
1635 );
1636 }
1637
1638 #[test]
1639 fn resolve_host_dst_passes_through_when_family_absent() {
1640 let gw = GatewayIps {
1641 ipv4: None,
1642 ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
1643 };
1644 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1)), 8080);
1646 assert_eq!(resolve_host_dst(dst, gw), dst);
1647 }
1648
1649 #[test]
1650 fn resolve_host_dst_passes_through_non_gateway() {
1651 let gw = test_gateway();
1652 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 443);
1653 assert_eq!(resolve_host_dst(dst, gw), dst);
1654 }
1655
1656 #[test]
1657 fn outbound_proxy_is_skipped_for_host_destination() {
1658 let gw = test_gateway();
1659 let guest_dst = SocketAddr::new(IpAddr::V4(gw.ipv4.unwrap()), 8080);
1660 let connect_target = resolve_tcp_host_target(guest_dst, gw);
1661 let proxy = Some(Arc::new(ResolvedOutboundProxy::Socks5 {
1662 address: "192.0.2.1:1080".parse().unwrap(),
1663 credentials: None,
1664 }));
1665
1666 assert!(
1667 ResolvedOutboundProxy::select_for_destination(
1668 &proxy,
1669 guest_dst,
1670 connect_target.primary(),
1671 )
1672 .is_none()
1673 );
1674 }
1675
1676 #[test]
1677 fn outbound_proxy_is_preserved_for_external_destination() {
1678 let gw = test_gateway();
1679 let guest_dst = "198.51.100.10:443".parse().unwrap();
1680 let connect_target = resolve_tcp_host_target(guest_dst, gw);
1681 let proxy = Some(Arc::new(ResolvedOutboundProxy::Socks5 {
1682 address: "192.0.2.1:1080".parse().unwrap(),
1683 credentials: None,
1684 }));
1685
1686 assert!(
1687 ResolvedOutboundProxy::select_for_destination(
1688 &proxy,
1689 guest_dst,
1690 connect_target.primary(),
1691 )
1692 .is_some()
1693 );
1694 }
1695
1696 #[test]
1697 fn external_icmp_echo_requests_are_not_answered_locally() {
1698 fn drive_one_frame(
1699 device: &mut SmoltcpDevice,
1700 iface: &mut Interface,
1701 sockets: &mut SocketSet<'_>,
1702 shared: &Arc<SharedState>,
1703 poll_config: &PollLoopConfig,
1704 now: Instant,
1705 ) {
1706 let frame = device.stage_next_frame().expect("expected staged frame");
1707 if handle_gateway_icmp_echo(
1708 frame,
1709 poll_config,
1710 shared,
1711 &NetworkPolicy::allow_all(),
1712 None,
1713 ) {
1714 device.drop_staged_frame();
1715 return;
1716 }
1717 let _ = iface.poll_ingress_single(now, device, sockets);
1718 let _ = iface.poll_egress(now, device, sockets);
1719 }
1720
1721 let shared = Arc::new(SharedState::new(4));
1722 let poll_config = PollLoopConfig {
1723 gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
1724 guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
1725 gateway: GatewayIps {
1726 ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
1727 ipv6: Some(Ipv6Addr::LOCALHOST),
1728 },
1729 guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
1730 guest_ipv6: None,
1731 mtu: 1500,
1732 };
1733 let guest_ipv4 = poll_config.guest_ipv4.unwrap();
1734 let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
1735 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
1736 let mut iface = create_interface(&mut device, &poll_config);
1737 let mut sockets = SocketSet::new(vec![]);
1738 let now = smoltcp_now();
1739
1740 shared
1741 .tx_ring
1742 .push(build_arp_request_frame(
1743 poll_config.guest_mac,
1744 guest_ipv4.octets(),
1745 gateway_ipv4.octets(),
1746 ))
1747 .unwrap();
1748 shared
1749 .tx_ring
1750 .push(build_icmpv4_echo_frame(
1751 poll_config.guest_mac,
1752 poll_config.gateway_mac,
1753 guest_ipv4.octets(),
1754 [142, 251, 216, 46],
1755 0x1234,
1756 0xABCD,
1757 b"ping",
1758 ))
1759 .unwrap();
1760
1761 drive_one_frame(
1762 &mut device,
1763 &mut iface,
1764 &mut sockets,
1765 &shared,
1766 &poll_config,
1767 now,
1768 );
1769 let _ = shared.rx_ring.pop().expect("expected ARP reply");
1770
1771 drive_one_frame(
1772 &mut device,
1773 &mut iface,
1774 &mut sockets,
1775 &shared,
1776 &poll_config,
1777 now,
1778 );
1779 assert!(
1780 shared.rx_ring.pop().is_none(),
1781 "external ICMP should not be answered locally"
1782 );
1783 }
1784
1785 use smoltcp::socket::tcp;
1801 use smoltcp::wire::{TcpControl, TcpPacket, TcpRepr, TcpSeqNumber};
1802
1803 const GUEST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
1804 const GATEWAY_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1805 const GUEST_IP: [u8; 4] = [100, 96, 0, 2];
1806 const GATEWAY_IP: [u8; 4] = [100, 96, 0, 1];
1807 const SERVER_IP: [u8; 4] = [93, 184, 216, 34];
1809
1810 fn leak_poll_config() -> PollLoopConfig {
1811 PollLoopConfig {
1812 gateway_mac: GATEWAY_MAC,
1813 guest_mac: GUEST_MAC,
1814 gateway: GatewayIps {
1815 ipv4: Some(Ipv4Addr::from(GATEWAY_IP)),
1816 ipv6: None,
1817 },
1818 guest_ipv4: Some(Ipv4Addr::from(GUEST_IP)),
1819 guest_ipv6: None,
1820 mtu: 1500,
1821 }
1822 }
1823
1824 #[allow(clippy::too_many_arguments)]
1826 fn build_tcp_frame(
1827 src_port: u16,
1828 dst_port: u16,
1829 control: TcpControl,
1830 seq: i32,
1831 ack: Option<i32>,
1832 payload: &[u8],
1833 ) -> Vec<u8> {
1834 let src_ip = Ipv4Addr::from(GUEST_IP);
1835 let dst_ip = Ipv4Addr::from(SERVER_IP);
1836
1837 let tcp_repr = TcpRepr {
1838 src_port,
1839 dst_port,
1840 control,
1841 seq_number: TcpSeqNumber(seq),
1842 ack_number: ack.map(TcpSeqNumber),
1843 window_len: 65535,
1844 window_scale: None,
1845 max_seg_size: None,
1846 sack_permitted: false,
1847 sack_ranges: [None, None, None],
1848 timestamp: None,
1849 payload,
1850 };
1851 let ipv4_repr = Ipv4Repr {
1852 src_addr: src_ip,
1853 dst_addr: dst_ip,
1854 next_header: IpProtocol::Tcp,
1855 payload_len: tcp_repr.buffer_len(),
1856 hop_limit: 64,
1857 };
1858
1859 let frame_len = 14 + ipv4_repr.buffer_len() + tcp_repr.buffer_len();
1860 let mut frame = vec![0u8; frame_len];
1861
1862 let mut eth = EthernetFrame::new_unchecked(&mut frame);
1863 EthernetRepr {
1864 src_addr: EthernetAddress(GUEST_MAC),
1865 dst_addr: EthernetAddress(GATEWAY_MAC),
1866 ethertype: EthernetProtocol::Ipv4,
1867 }
1868 .emit(&mut eth);
1869
1870 let ip_end = 14 + ipv4_repr.buffer_len();
1871 ipv4_repr.emit(
1872 &mut Ipv4Packet::new_unchecked(&mut frame[14..ip_end]),
1873 &ChecksumCapabilities::default(),
1874 );
1875 tcp_repr.emit(
1876 &mut TcpPacket::new_unchecked(&mut frame[ip_end..]),
1877 &IpAddress::Ipv4(src_ip),
1878 &IpAddress::Ipv4(dst_ip),
1879 &ChecksumCapabilities::default(),
1880 );
1881
1882 frame
1883 }
1884
1885 fn ingress(
1887 frame: Vec<u8>,
1888 device: &mut SmoltcpDevice,
1889 iface: &mut Interface,
1890 sockets: &mut SocketSet<'_>,
1891 shared: &Arc<SharedState>,
1892 now: Instant,
1893 ) {
1894 shared.tx_ring.push(frame).unwrap();
1895 device.stage_next_frame().expect("frame should stage");
1896 iface.poll_ingress_single(now, device, sockets);
1897 loop {
1898 let r = iface.poll_egress(now, device, sockets);
1899 if matches!(r, smoltcp::iface::PollResult::None) {
1900 break;
1901 }
1902 }
1903 }
1904
1905 fn last_tcp_reply(shared: &Arc<SharedState>) -> Option<(i32, i32, bool, bool, bool)> {
1908 let mut out = None;
1909 while let Some(frame) = shared.rx_ring.pop() {
1910 if frame.len() < 34 {
1911 continue;
1912 }
1913 if frame[23] != 6 {
1915 continue; }
1917 let tcp = match TcpPacket::new_checked(&frame[34..]) {
1918 Ok(p) => p,
1919 Err(_) => continue,
1920 };
1921 out = Some((
1922 tcp.seq_number().0,
1923 tcp.ack_number().0,
1924 tcp.syn(),
1925 tcp.fin(),
1926 tcp.rst(),
1927 ));
1928 }
1929 out
1930 }
1931
1932 fn only_tcp_state(sockets: &SocketSet<'_>) -> Option<tcp::State> {
1934 for (_h, sock) in sockets.iter() {
1935 if let smoltcp::socket::Socket::Tcp(s) = sock {
1936 return Some(s.state());
1937 }
1938 }
1939 None
1940 }
1941
1942 fn handshake(
1945 tracker: &mut ConnectionTracker,
1946 device: &mut SmoltcpDevice,
1947 iface: &mut Interface,
1948 sockets: &mut SocketSet<'_>,
1949 shared: &Arc<SharedState>,
1950 now: Instant,
1951 guest_port: u16,
1952 ) -> (i32, i32) {
1953 let src = SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), guest_port);
1954 let dst = SocketAddr::new(Ipv4Addr::from(SERVER_IP).into(), 443);
1955
1956 ingress(
1959 build_arp_request_frame(GUEST_MAC, GUEST_IP, GATEWAY_IP),
1960 device,
1961 iface,
1962 sockets,
1963 shared,
1964 now,
1965 );
1966 let _ = shared.rx_ring.pop(); let guest_isn = 1000i32;
1969
1970 assert!(
1973 tracker.create_tcp_socket(src, dst, sockets),
1974 "socket creation should succeed under the limit"
1975 );
1976 ingress(
1977 build_tcp_frame(guest_port, 443, TcpControl::Syn, guest_isn, None, &[]),
1978 device,
1979 iface,
1980 sockets,
1981 shared,
1982 now,
1983 );
1984 let (server_isn, ack, is_syn, _, _) =
1985 last_tcp_reply(shared).expect("expected SYN-ACK from smoltcp");
1986 assert!(is_syn, "expected SYN flag on handshake reply");
1987 assert_eq!(ack, guest_isn + 1, "SYN-ACK should ack guest ISN+1");
1988
1989 ingress(
1991 build_tcp_frame(
1992 guest_port,
1993 443,
1994 TcpControl::None,
1995 guest_isn + 1,
1996 Some(server_isn + 1),
1997 &[],
1998 ),
1999 device,
2000 iface,
2001 sockets,
2002 shared,
2003 now,
2004 );
2005 assert_eq!(
2006 only_tcp_state(sockets),
2007 Some(tcp::State::Established),
2008 "socket should be ESTABLISHED after handshake",
2009 );
2010
2011 (server_isn, guest_isn + 1)
2012 }
2013
2014 fn establish(
2016 tracker: &mut ConnectionTracker,
2017 device: &mut SmoltcpDevice,
2018 iface: &mut Interface,
2019 sockets: &mut SocketSet<'_>,
2020 shared: &Arc<SharedState>,
2021 now: Instant,
2022 guest_port: u16,
2023 ) -> (i32, i32, Vec<NewConnection>) {
2024 let (server_isn, guest_seq) =
2025 handshake(tracker, device, iface, sockets, shared, now, guest_port);
2026
2027 let new_conns = tracker.take_new_connections(sockets);
2029 assert_eq!(
2030 new_conns.len(),
2031 1,
2032 "one new connection should be handed off"
2033 );
2034
2035 (server_isn, guest_seq, new_conns)
2036 }
2037
2038 #[test]
2039 fn guest_fin_propagates_half_close_without_killing_the_connection() {
2040 let shared = Arc::new(SharedState::new(64));
2041 let poll_config = leak_poll_config();
2042 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
2043 let mut iface = create_interface(&mut device, &poll_config);
2044 let mut sockets = SocketSet::new(vec![]);
2045 let mut tracker = ConnectionTracker::new(None);
2046 let now = smoltcp_now();
2047
2048 let (server_isn, guest_seq, mut new_conns) = establish(
2049 &mut tracker,
2050 &mut device,
2051 &mut iface,
2052 &mut sockets,
2053 &shared,
2054 now,
2055 54321,
2056 );
2057 let conn = new_conns.remove(0);
2060 let mut from_smoltcp = conn.from_smoltcp;
2061 let to_smoltcp = conn.to_smoltcp;
2062
2063 ingress(
2066 build_tcp_frame(
2067 54321,
2068 443,
2069 TcpControl::Fin,
2070 guest_seq,
2071 Some(server_isn + 1),
2072 &[],
2073 ),
2074 &mut device,
2075 &mut iface,
2076 &mut sockets,
2077 &shared,
2078 now,
2079 );
2080 assert_eq!(
2081 only_tcp_state(&sockets),
2082 Some(tcp::State::CloseWait),
2083 "guest FIN should move the smoltcp socket to CLOSE_WAIT",
2084 );
2085
2086 tracker.relay_data(&mut sockets);
2090 assert!(
2091 matches!(
2092 from_smoltcp.try_recv(),
2093 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
2094 ),
2095 "guest FIN must propagate EOF to the proxy task",
2096 );
2097
2098 assert!(
2101 tracker.has_socket_for(
2102 &SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), 54321),
2103 &SocketAddr::new(Ipv4Addr::from(SERVER_IP).into(), 443),
2104 ),
2105 "half-closed connection must stay tracked while the proxy runs",
2106 );
2107 let payload = b"pending server response";
2108 to_smoltcp
2109 .try_send(bytes::Bytes::from_static(payload))
2110 .expect("server → guest channel should accept data");
2111 while shared.rx_ring.pop().is_some() {} tracker.relay_data(&mut sockets);
2113 loop {
2114 let r = iface.poll_egress(now, &mut device, &mut sockets);
2115 if matches!(r, smoltcp::iface::PollResult::None) {
2116 break;
2117 }
2118 }
2119 let mut delivered = false;
2120 while let Some(frame) = shared.rx_ring.pop() {
2121 if frame.windows(payload.len()).any(|w| w == payload) {
2122 delivered = true;
2123 }
2124 }
2125 assert!(
2126 delivered,
2127 "server data must still reach a half-closed guest",
2128 );
2129 assert_eq!(
2130 only_tcp_state(&sockets),
2131 Some(tcp::State::CloseWait),
2132 "socket must stay open (CLOSE_WAIT) while the proxy is alive",
2133 );
2134 }
2135
2136 #[test]
2137 fn guest_fin_connection_is_reaped_after_proxy_exit() {
2138 let shared = Arc::new(SharedState::new(64));
2139 let poll_config = leak_poll_config();
2140 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
2141 let mut iface = create_interface(&mut device, &poll_config);
2142 let mut sockets = SocketSet::new(vec![]);
2143 let mut tracker = ConnectionTracker::new(None);
2144 let now = smoltcp_now();
2145
2146 let (server_isn, guest_seq, mut new_conns) = establish(
2147 &mut tracker,
2148 &mut device,
2149 &mut iface,
2150 &mut sockets,
2151 &shared,
2152 now,
2153 54321,
2154 );
2155 let conn = new_conns.remove(0);
2156 let from_smoltcp = conn.from_smoltcp;
2157 let to_smoltcp = conn.to_smoltcp;
2158
2159 ingress(
2161 build_tcp_frame(
2162 54321,
2163 443,
2164 TcpControl::Fin,
2165 guest_seq,
2166 Some(server_isn + 1),
2167 &[],
2168 ),
2169 &mut device,
2170 &mut iface,
2171 &mut sockets,
2172 &shared,
2173 now,
2174 );
2175 tracker.relay_data(&mut sockets);
2176
2177 drop(from_smoltcp);
2180 drop(to_smoltcp);
2181
2182 while shared.rx_ring.pop().is_some() {} tracker.relay_data(&mut sockets);
2186 loop {
2187 let r = iface.poll_egress(now, &mut device, &mut sockets);
2188 if matches!(r, smoltcp::iface::PollResult::None) {
2189 break;
2190 }
2191 }
2192 let (fin_seq, _, _, is_fin, _) =
2193 last_tcp_reply(&shared).expect("expected FIN toward the guest");
2194 assert!(is_fin, "proxy exit after guest FIN must FIN the guest side");
2195
2196 ingress(
2198 build_tcp_frame(
2199 54321,
2200 443,
2201 TcpControl::None,
2202 guest_seq + 1,
2203 Some(fin_seq + 1),
2204 &[],
2205 ),
2206 &mut device,
2207 &mut iface,
2208 &mut sockets,
2209 &shared,
2210 now,
2211 );
2212 tracker.relay_data(&mut sockets);
2213 tracker.cleanup_closed(&mut sockets);
2214
2215 assert!(
2218 !tracker.has_socket_for(
2219 &SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), 54321),
2220 &SocketAddr::new(Ipv4Addr::from(SERVER_IP).into(), 443),
2221 ),
2222 "connection must be evicted after FIN + proxy exit",
2223 );
2224 assert_eq!(
2225 only_tcp_state(&sockets),
2226 None,
2227 "socket must be removed from the socket set",
2228 );
2229 }
2230
2231 #[test]
2232 fn guest_rst_is_cleaned_up() {
2233 let shared = Arc::new(SharedState::new(64));
2234 let poll_config = leak_poll_config();
2235 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
2236 let mut iface = create_interface(&mut device, &poll_config);
2237 let mut sockets = SocketSet::new(vec![]);
2238 let mut tracker = ConnectionTracker::new(None);
2239 let now = smoltcp_now();
2240
2241 let (server_isn, guest_seq, mut new_conns) = establish(
2242 &mut tracker,
2243 &mut device,
2244 &mut iface,
2245 &mut sockets,
2246 &shared,
2247 now,
2248 54322,
2249 );
2250 let conn = new_conns.remove(0);
2251 let mut from_smoltcp = conn.from_smoltcp;
2252 let _to_smoltcp = conn.to_smoltcp;
2253
2254 ingress(
2256 build_tcp_frame(
2257 54322,
2258 443,
2259 TcpControl::Rst,
2260 guest_seq,
2261 Some(server_isn + 1),
2262 &[],
2263 ),
2264 &mut device,
2265 &mut iface,
2266 &mut sockets,
2267 &shared,
2268 now,
2269 );
2270
2271 for _ in 0..8 {
2273 tracker.relay_data(&mut sockets);
2274 tracker.cleanup_closed(&mut sockets);
2275 let _ = iface.poll_egress(now, &mut device, &mut sockets);
2276 }
2277
2278 assert!(
2279 !tracker.has_socket_for(
2280 &SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), 54322),
2281 &SocketAddr::new(Ipv4Addr::from(SERVER_IP).into(), 443),
2282 ),
2283 "RST connection should be evicted from the tracker",
2284 );
2285 assert!(
2288 matches!(
2289 from_smoltcp.try_recv(),
2290 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected)
2291 ),
2292 "RST teardown must close the proxy channel (clean, no orphan)",
2293 );
2294 }
2295
2296 #[test]
2297 fn full_connection_table_refuses_new_sockets() {
2298 let mut tracker = ConnectionTracker::new(Some(4));
2302 let mut sockets = SocketSet::new(vec![]);
2303 let dst = SocketAddr::new(Ipv4Addr::from(SERVER_IP).into(), 443);
2304
2305 for port in 40000u16..40004 {
2306 let src = SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), port);
2307 assert!(
2308 tracker.create_tcp_socket(src, dst, &mut sockets),
2309 "creation under the limit must succeed",
2310 );
2311 }
2312 let src = SocketAddr::new(Ipv4Addr::from(GUEST_IP).into(), 40004);
2316 assert!(
2317 !tracker.create_tcp_socket(src, dst, &mut sockets),
2318 "creation at the limit must be refused",
2319 );
2320 }
2321
2322 #[test]
2323 fn guest_fin_before_proxy_spawn_is_handed_off() {
2324 let shared = Arc::new(SharedState::new(64));
2325 let poll_config = leak_poll_config();
2326 let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
2327 let mut iface = create_interface(&mut device, &poll_config);
2328 let mut sockets = SocketSet::new(vec![]);
2329 let mut tracker = ConnectionTracker::new(None);
2330 let now = smoltcp_now();
2331 let guest_port = 54323;
2332 let (server_isn, guest_seq) = handshake(
2333 &mut tracker,
2334 &mut device,
2335 &mut iface,
2336 &mut sockets,
2337 &shared,
2338 now,
2339 guest_port,
2340 );
2341
2342 ingress(
2346 build_tcp_frame(
2347 guest_port,
2348 443,
2349 TcpControl::Fin,
2350 guest_seq,
2351 Some(server_isn + 1),
2352 &[],
2353 ),
2354 &mut device,
2355 &mut iface,
2356 &mut sockets,
2357 &shared,
2358 now,
2359 );
2360 assert_eq!(
2361 only_tcp_state(&sockets),
2362 Some(tcp::State::CloseWait),
2363 "guest FIN should arrive before the proxy handoff",
2364 );
2365
2366 let new_conns = tracker.take_new_connections(&mut sockets);
2367 assert_eq!(
2368 new_conns.len(),
2369 1,
2370 "a connection that reached CLOSE_WAIT still needs a proxy task",
2371 );
2372 }
2373}