use std::io;
use std::net::Ipv4Addr;
use std::os::fd::{AsRawFd, OwnedFd};
use std::time::Duration;
use tokio::io::unix::AsyncFd;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use splicetcp::{FdFrameSource, FrameSource};
use crate::darwin::classifier::FrameClassifier;
use crate::darwin::egress::HostEgress;
use crate::darwin::inbound_relay::InboundCommand;
use crate::darwin::tcp_bridge::TcpBridge;
use crate::dhcp::DhcpServer;
use crate::dns::DnsForwarder;
mod fd;
mod guest_tx;
mod intercept;
#[cfg(test)]
mod tests;
use fd::{FdWrapper, set_nonblocking};
use guest_tx::{DeliveryClass, GuestTx, NOBUFS_RETRY_DELAY, drain_cmd_rx, drain_reply_rx};
use intercept::{handle_intercepted_frame, process_inbound_cmd};
pub struct NetworkDatapath {
pub guest_fd: OwnedFd,
pub egress: HostEgress,
pub reply_rx: mpsc::Receiver<Vec<u8>>,
pub cmd_rx: mpsc::Receiver<InboundCommand>,
pub dhcp_server: DhcpServer,
pub dns_forwarder: DnsForwarder,
pub dns_log: super::dns_log::DnsResolutionLog,
pub gateway_mac: [u8; 6],
pub gateway_ip: Ipv4Addr,
pub guest_ip: Ipv4Addr,
pub cancel: CancellationToken,
pub mtu: usize,
pub frame_sink: Option<std::sync::Arc<dyn crate::direct_rx::FrameSink>>,
pub conn_sink: Option<std::sync::Arc<dyn crate::direct_rx::ConnSink>>,
}
impl NetworkDatapath {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
guest_fd: OwnedFd,
egress: HostEgress,
reply_rx: mpsc::Receiver<Vec<u8>>,
cmd_rx: mpsc::Receiver<InboundCommand>,
dhcp_server: DhcpServer,
dns_forwarder: DnsForwarder,
gateway_ip: Ipv4Addr,
guest_ip: Ipv4Addr,
gateway_mac: [u8; 6],
cancel: CancellationToken,
mtu: usize,
) -> Self {
Self {
guest_fd,
egress,
reply_rx,
cmd_rx,
dhcp_server,
dns_forwarder,
dns_log: super::dns_log::DnsResolutionLog::new(),
gateway_mac,
gateway_ip,
guest_ip,
cancel,
mtu,
frame_sink: None,
conn_sink: None,
}
}
pub fn set_frame_sink(&mut self, sink: std::sync::Arc<dyn crate::direct_rx::FrameSink>) {
self.frame_sink = Some(sink);
}
pub fn set_conn_sink(&mut self, sink: std::sync::Arc<dyn crate::direct_rx::ConnSink>) {
self.conn_sink = Some(sink);
}
pub async fn run(self) -> io::Result<()> {
let Self {
guest_fd,
mut egress,
mut reply_rx,
mut cmd_rx,
mut dhcp_server,
dns_forwarder,
dns_log,
gateway_mac,
gateway_ip,
guest_ip,
cancel,
mtu,
frame_sink,
conn_sink,
} = self;
let guest_raw_fd = guest_fd.as_raw_fd();
set_nonblocking(guest_raw_fd)?;
let mut source = FdFrameSource::new(guest_raw_fd);
let mut device = FrameClassifier::new(gateway_ip, mtu);
device.set_gateway_mac(gateway_mac);
let mut tcp_bridge = TcpBridge::new(gateway_ip);
if frame_sink.is_some() {
tcp_bridge.enable_large_frames();
}
if let Some(ref sink) = conn_sink {
tcp_bridge.set_conn_sink(sink.clone());
}
let proxy_env = super::proxy_detect::ProxyEnvironment::detect();
egress.set_proxy_awareness(dns_log.clone(), proxy_env.clone());
tcp_bridge.set_proxy_awareness(dns_log.clone(), proxy_env);
let guest_async = AsyncFd::new(FdWrapper(guest_fd))?;
let dns_reply_tx = egress.reply_sender();
let mut guest_mac: Option<[u8; 6]> = None;
let mut rx_frames: u64 = 0;
let mut guest_tx = GuestTx::new(frame_sink);
let mut nobufs_retry = tokio::time::interval(NOBUFS_RETRY_DELAY);
nobufs_retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut timer_wheel =
crate::timer_wheel::TimerWheel::<std::net::SocketAddr>::new(Duration::from_secs(1));
let mut timer_wheel_tick = tokio::time::interval(Duration::from_secs(1));
timer_wheel_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut maintenance = tokio::time::interval(Duration::from_secs(30));
maintenance.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let handshake_waker = std::sync::Arc::new(tokio::sync::Notify::new());
tcp_bridge.set_handshake_waker(handshake_waker.clone());
let mut fastpath_tick = tokio::time::interval(Duration::from_millis(20));
fastpath_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
tracing::info!("Network datapath started (TCP shim + socket proxy mode)");
loop {
let awaits_writable = guest_tx.awaits_writable();
let awaits_retry = guest_tx.awaits_retry();
tokio::select! {
biased;
() = cancel.cancelled() => {
tracing::info!("Network datapath shutting down");
break;
}
writable = guest_async.writable(), if awaits_writable => {
let mut guard = writable?;
guest_tx.drain(guest_raw_fd);
if guest_tx.awaits_writable() {
guard.clear_ready();
}
}
_ = nobufs_retry.tick(), if awaits_retry => {
guest_tx.drain(guest_raw_fd);
}
readable = guest_async.readable() => {
let mut guard = readable?;
let prev_mac = guest_mac;
source.drain(|frame| {
rx_frames += 1;
device.classify_frame(frame, &mut guest_mac);
});
guard.clear_ready();
if prev_mac.is_none()
&& let Some(gmac) = guest_mac
{
tcp_bridge.set_fast_path_macs(gateway_mac, gmac);
}
let fast_acks = device.drain_fast_path(|frame_data| {
tcp_bridge.try_fast_path_intercept(frame_data)
});
for ack in fast_acks {
guest_tx.send(guest_raw_fd, &ack, DeliveryClass::Reliable);
}
let hs_replies = device.drain_handshake(|frame_data| {
tcp_bridge.try_complete_handshake(frame_data)
});
for reply in hs_replies {
guest_tx.send(guest_raw_fd, &reply, DeliveryClass::Reliable);
}
for reply in device.take_arp_replies() {
guest_tx.send(guest_raw_fd, &reply, DeliveryClass::Lossy);
}
device.clear_unmatched_rx();
let intercepted = device.take_intercepted();
for intercepted_frame in &intercepted {
handle_intercepted_frame(
intercepted_frame,
&mut guest_tx,
guest_raw_fd,
&mut egress,
&mut dhcp_server,
&dns_forwarder,
&dns_reply_tx,
&dns_log,
&cancel,
gateway_ip,
gateway_mac,
guest_mac.unwrap_or([0xFF; 6]),
mtu,
);
}
let gated_syns = device.take_gated_syns();
let gmac = guest_mac.unwrap_or([0xFF; 6]);
for syn in &gated_syns {
if let Some(rst) = tcp_bridge.handle_outbound_syn(&syn.frame, gateway_mac, gmac) {
guest_tx.send(guest_raw_fd, &rst, DeliveryClass::Reliable);
}
}
}
Some(reply_frame) = reply_rx.recv() => {
guest_tx.send(guest_raw_fd, &reply_frame, DeliveryClass::Lossy);
}
Some(cmd) = cmd_rx.recv() => {
process_inbound_cmd(
cmd,
&mut tcp_bridge,
&mut egress,
guest_ip,
gateway_ip,
guest_mac,
);
}
_ = timer_wheel_tick.tick() => {
let expired = timer_wheel.advance();
for entry in &expired {
tracing::trace!(
"Timer wheel expired: {:?} action={:?}",
entry.key,
entry.action
);
}
for entry in expired {
use crate::timer_wheel::TimerAction;
match entry.action {
TimerAction::UdpFlowExpiry | TimerAction::IcmpTimeout => {
egress.expire_flow(entry.key);
}
_ => {}
}
}
}
_ = maintenance.tick() => {
egress.maintenance();
guest_tx.log_stats(rx_frames);
}
() = handshake_waker.notified() => {}
_ = fastpath_tick.tick(), if tcp_bridge.fast_path_count() > 0 => {}
}
let hs_frames = tcp_bridge.poll_handshakes();
for frame in hs_frames {
guest_tx.send(guest_raw_fd, &frame, DeliveryClass::Reliable);
}
drain_cmd_rx(
&mut cmd_rx,
&mut tcp_bridge,
&mut egress,
guest_ip,
gateway_ip,
guest_mac,
);
if guest_tx.has_backlog() {
guest_tx.stats.gated_polls += 1;
} else {
for frame in tcp_bridge.poll_fast_path() {
guest_tx.send(guest_raw_fd, &frame, DeliveryClass::Reliable);
}
}
drain_reply_rx(&mut reply_rx, &mut guest_tx, guest_raw_fd);
tokio::task::yield_now().await;
}
Ok(())
}
}