#![cfg(target_os = "macos")]
mod helpers;
use std::net::Ipv4Addr;
use std::os::fd::AsRawFd;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use arcbox_dhcp::DhcpConfig;
use arcbox_net::darwin::datapath_loop::NetworkDatapath;
use arcbox_net::darwin::egress::HostEgress;
use arcbox_net::dns::{DnsConfig, DnsForwarder};
use splicetcp::{FdFrameSource, FrameSource};
use helpers::frames::{
build_arp_request, build_dhcp_discover, build_dhcp_request, build_icmp_echo, build_tcp_syn,
extract_dhcp_payload, parse_dhcp,
};
use helpers::mock_nic::{fd_write, mock_guest_nic, recv_frames_timeout, set_nonblocking};
const GATEWAY_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 1);
const GUEST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 64, 2);
const GATEWAY_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
const CLIENT_MAC: [u8; 6] = [0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
fn create_test_datapath() -> (NetworkDatapath, std::os::fd::OwnedFd, CancellationToken) {
let (host_fd, guest_fd) = mock_guest_nic();
set_nonblocking(guest_fd.as_raw_fd());
let cancel = CancellationToken::new();
let (reply_tx, reply_rx) = mpsc::channel(256);
let (_cmd_tx, cmd_rx) = mpsc::channel(64);
let egress = HostEgress::new(GATEWAY_IP, GATEWAY_MAC, GUEST_IP, reply_tx, cancel.clone());
let dhcp_config = DhcpConfig::new(GATEWAY_IP, Ipv4Addr::new(255, 255, 255, 0));
let dhcp_server = arcbox_dhcp::DhcpServer::new(dhcp_config);
let dns_config = DnsConfig::new(GATEWAY_IP);
let dns_forwarder = DnsForwarder::new(dns_config);
let datapath = NetworkDatapath::new(
host_fd,
egress,
reply_rx,
cmd_rx,
dhcp_server,
dns_forwarder,
GATEWAY_IP,
GUEST_IP,
GATEWAY_MAC,
cancel.clone(),
1500, );
(datapath, guest_fd, cancel)
}
fn spawn_datapath(datapath: NetworkDatapath) -> tokio::task::JoinHandle<std::io::Result<()>> {
tokio::spawn(async move { datapath.run().await })
}
#[tokio::test]
async fn test_dhcp_full_cycle() {
let (datapath, guest_fd, cancel) = create_test_datapath();
let handle = spawn_datapath(datapath);
let guest_raw = guest_fd.as_raw_fd();
tokio::time::sleep(Duration::from_millis(20)).await;
let xid: u32 = 0xDEAD_BEEF;
let discover = build_dhcp_discover(CLIENT_MAC, xid);
fd_write(guest_raw, &discover).expect("failed to write DISCOVER");
let frames = recv_frames_timeout(guest_raw, Duration::from_secs(2)).await;
assert!(!frames.is_empty(), "expected DHCP OFFER, got no frames");
let offer_frame = frames
.iter()
.find(|f| {
extract_dhcp_payload(f)
.and_then(parse_dhcp)
.is_some_and(|p| p.message_type == Some(2)) })
.expect("no DHCP OFFER frame found");
let offer = parse_dhcp(extract_dhcp_payload(offer_frame).unwrap()).unwrap();
assert_eq!(offer.op, 2, "OFFER op should be BOOTREPLY");
assert_eq!(offer.xid, xid, "OFFER xid should match");
assert_eq!(offer.message_type, Some(2), "should be OFFER (type 2)");
assert!(
!offer.yiaddr.is_unspecified(),
"OFFER should assign an IP address"
);
let offered_ip = offer.yiaddr;
let offered_octets = offered_ip.octets();
assert_eq!(
&offered_octets[..3],
&[192, 168, 64],
"offered IP should be in 192.168.64.0/24"
);
let request = build_dhcp_request(CLIENT_MAC, xid, offered_ip, GATEWAY_IP);
fd_write(guest_raw, &request).expect("failed to write REQUEST");
let frames = recv_frames_timeout(guest_raw, Duration::from_secs(2)).await;
assert!(!frames.is_empty(), "expected DHCP ACK, got no frames");
let ack_frame = frames
.iter()
.find(|f| {
extract_dhcp_payload(f)
.and_then(parse_dhcp)
.is_some_and(|p| p.message_type == Some(5)) })
.expect("no DHCP ACK frame found");
let ack = parse_dhcp(extract_dhcp_payload(ack_frame).unwrap()).unwrap();
assert_eq!(ack.op, 2, "ACK op should be BOOTREPLY");
assert_eq!(ack.xid, xid, "ACK xid should match");
assert_eq!(ack.message_type, Some(5), "should be ACK (type 5)");
assert_eq!(ack.yiaddr, offered_ip, "ACK should confirm the offered IP");
assert!(ack.lease_time.is_some(), "ACK should include lease time");
assert!(ack.subnet_mask.is_some(), "ACK should include subnet mask");
cancel.cancel();
handle.await.unwrap().unwrap();
}
#[tokio::test]
async fn test_frame_classification_arp() {
use arcbox_net::darwin::classifier::FrameClassifier;
let (host_fd, guest_fd) = mock_guest_nic();
set_nonblocking(host_fd.as_raw_fd());
let mut device = FrameClassifier::new(GATEWAY_IP, 1500);
let mut source = FdFrameSource::new(host_fd.as_raw_fd());
device.set_gateway_mac(GATEWAY_MAC);
let mut guest_mac = None;
let arp = build_arp_request(CLIENT_MAC, GUEST_IP, GATEWAY_IP);
fd_write(guest_fd.as_raw_fd(), &arp).expect("write ARP");
source.drain(|frame| device.classify_frame(frame, &mut guest_mac));
let intercepted = device.take_intercepted();
assert!(intercepted.is_empty(), "ARP should not be intercepted");
assert_eq!(
guest_mac,
Some(CLIENT_MAC),
"guest MAC should be learned from ARP"
);
let replies = device.take_arp_replies();
assert_eq!(replies.len(), 1, "expected exactly one ARP reply");
let reply = &replies[0];
assert!(
reply.len() >= 42,
"ARP reply must be at least 42 bytes (Ethernet + ARP header), got {}",
reply.len()
);
assert_eq!(
&reply[0..6],
&CLIENT_MAC,
"reply dst MAC should be the requester"
);
assert_eq!(
&reply[6..12],
&GATEWAY_MAC,
"reply src MAC should be the gateway"
);
assert_eq!(
&reply[12..14],
&[0x08, 0x06],
"reply EtherType should be 0x0806"
);
assert_eq!(
&reply[20..22],
&[0x00, 0x02],
"ARP opcode should be 2 (reply)"
);
}
#[tokio::test]
async fn test_frame_classification_tcp_syn() {
use arcbox_net::darwin::classifier::FrameClassifier;
let (host_fd, guest_fd) = mock_guest_nic();
set_nonblocking(host_fd.as_raw_fd());
let mut device = FrameClassifier::new(GATEWAY_IP, 1500);
let mut source = FdFrameSource::new(host_fd.as_raw_fd());
let mut guest_mac = None;
let syn = build_tcp_syn(
CLIENT_MAC,
GATEWAY_MAC,
GUEST_IP,
12345,
Ipv4Addr::new(1, 1, 1, 1),
443,
);
fd_write(guest_fd.as_raw_fd(), &syn).expect("write SYN");
source.drain(|frame| device.classify_frame(frame, &mut guest_mac));
let gated = device.take_gated_syns();
assert_eq!(gated.len(), 1, "TCP SYN should be gated");
assert_eq!(gated[0].dst_port, 443);
assert_eq!(gated[0].src_port, 12345);
assert_eq!(gated[0].dst_ip, Ipv4Addr::new(1, 1, 1, 1));
}
#[tokio::test]
async fn test_frame_classification_icmp() {
use arcbox_net::darwin::classifier::{FrameClassifier, InterceptedKind};
let (host_fd, guest_fd) = mock_guest_nic();
set_nonblocking(host_fd.as_raw_fd());
let mut device = FrameClassifier::new(GATEWAY_IP, 1500);
let mut source = FdFrameSource::new(host_fd.as_raw_fd());
let mut guest_mac = None;
let icmp = build_icmp_echo(
CLIENT_MAC,
GATEWAY_MAC,
GUEST_IP,
Ipv4Addr::new(8, 8, 8, 8),
1,
1,
);
fd_write(guest_fd.as_raw_fd(), &icmp).expect("write ICMP");
source.drain(|frame| device.classify_frame(frame, &mut guest_mac));
let intercepted = device.take_intercepted();
assert_eq!(intercepted.len(), 1);
assert_eq!(intercepted[0].kind, InterceptedKind::Icmp);
}
#[tokio::test]
async fn test_frame_classification_dhcp() {
use arcbox_net::darwin::classifier::{FrameClassifier, InterceptedKind};
let (host_fd, guest_fd) = mock_guest_nic();
set_nonblocking(host_fd.as_raw_fd());
let mut device = FrameClassifier::new(GATEWAY_IP, 1500);
let mut source = FdFrameSource::new(host_fd.as_raw_fd());
let mut guest_mac = None;
let discover = build_dhcp_discover(CLIENT_MAC, 0x1234);
fd_write(guest_fd.as_raw_fd(), &discover).expect("write DHCP DISCOVER");
source.drain(|frame| device.classify_frame(frame, &mut guest_mac));
let intercepted = device.take_intercepted();
assert_eq!(intercepted.len(), 1);
assert_eq!(intercepted[0].kind, InterceptedKind::Dhcp);
}