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