use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use smoltcp::iface::{Config, Interface, SocketSet};
use smoltcp::time::Instant;
use smoltcp::wire::{
EthernetAddress, EthernetFrame, EthernetProtocol, HardwareAddress, Icmpv4Packet, Icmpv4Repr,
Icmpv6Packet, Icmpv6Repr, IpAddress, IpCidr, IpProtocol, Ipv4Packet, Ipv4Repr, Ipv6Packet,
Ipv6Repr, TcpPacket, UdpPacket,
};
use crate::config::{DnsConfig, PublishedPort};
use crate::conn::ConnectionTracker;
use crate::device::SmoltcpDevice;
use crate::dns::common::ports::DnsPortType;
use crate::dns::{
interceptor::DnsInterceptor,
proxies::{dot::DotProxy, tcp::DnsTcpProxy},
};
use crate::icmp_relay::IcmpRelay;
use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
use crate::proxy;
use crate::publisher::PortPublisher;
use crate::shared::SharedState;
use crate::tls::{proxy as tls_proxy, state::TlsState};
use crate::udp_relay::UdpRelay;
pub enum FrameAction {
TcpSyn { src: SocketAddr, dst: SocketAddr },
UdpRelay { src: SocketAddr, dst: SocketAddr },
Dns,
Passthrough,
}
pub struct PollLoopConfig {
pub gateway_mac: [u8; 6],
pub guest_mac: [u8; 6],
pub gateway: GatewayIps,
pub guest_ipv4: Option<Ipv4Addr>,
pub guest_ipv6: Option<Ipv6Addr>,
pub mtu: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct GatewayIps {
pub ipv4: Option<Ipv4Addr>,
pub ipv6: Option<Ipv6Addr>,
}
pub fn classify_frame(frame: &[u8]) -> FrameAction {
let Ok(eth) = EthernetFrame::new_checked(frame) else {
return FrameAction::Passthrough;
};
match eth.ethertype() {
EthernetProtocol::Ipv4 => classify_ipv4(eth.payload()),
EthernetProtocol::Ipv6 => classify_ipv6(eth.payload()),
_ => FrameAction::Passthrough, }
}
pub fn create_interface(device: &mut SmoltcpDevice, config: &PollLoopConfig) -> Interface {
let hw_addr = HardwareAddress::Ethernet(EthernetAddress(config.gateway_mac));
let iface_config = Config::new(hw_addr);
let mut iface = Interface::new(iface_config, device, smoltcp_now());
iface.update_ip_addrs(|addrs| {
if let Some(ipv4) = config.gateway.ipv4 {
addrs
.push(IpCidr::new(IpAddress::Ipv4(ipv4), 30)) .expect("failed to add gateway IPv4 address");
}
if let Some(ipv6) = config.gateway.ipv6 {
addrs
.push(IpCidr::new(IpAddress::Ipv6(ipv6), 64))
.expect("failed to add gateway IPv6 address");
}
});
if let Some(ipv4) = config.gateway.ipv4 {
iface
.routes_mut()
.add_default_ipv4_route(ipv4)
.expect("failed to add default IPv4 route");
}
if let Some(ipv6) = config.gateway.ipv6 {
iface
.routes_mut()
.add_default_ipv6_route(ipv6)
.expect("failed to add default IPv6 route");
}
iface.set_any_ip(true);
iface
}
#[allow(clippy::too_many_arguments)]
pub fn smoltcp_poll_loop(
shared: Arc<SharedState>,
config: PollLoopConfig,
network_policy: NetworkPolicy,
dns_config: DnsConfig,
tls_state: Option<Arc<TlsState>>,
published_ports: Vec<PublishedPort>,
max_connections: Option<usize>,
tokio_handle: tokio::runtime::Handle,
) {
let mut device = SmoltcpDevice::new(shared.clone(), config.mtu);
let mut iface = create_interface(&mut device, &config);
let mut sockets = SocketSet::new(vec![]);
let mut conn_tracker = ConnectionTracker::new(max_connections);
let gateway_ips: Arc<HashSet<IpAddr>> = Arc::new(
config
.gateway
.ipv4
.map(IpAddr::V4)
.into_iter()
.chain(config.gateway.ipv6.map(IpAddr::V6))
.collect(),
);
shared.set_gateway_ips(config.gateway.ipv4, config.gateway.ipv6);
let network_policy = Arc::new(network_policy);
let (mut dns_interceptor, dns_forwarder_handle) = DnsInterceptor::new(
&mut sockets,
dns_config,
shared.clone(),
&tokio_handle,
gateway_ips,
network_policy.clone(),
config.gateway,
);
let mut port_publisher = PortPublisher::new(
&published_ports,
config.guest_ipv4,
config.guest_ipv6,
&tokio_handle,
);
let mut udp_relay = UdpRelay::new(
shared.clone(),
config.gateway_mac,
config.guest_mac,
tokio_handle.clone(),
);
let icmp_relay = IcmpRelay::new(
shared.clone(),
config.gateway_mac,
config.guest_mac,
tokio_handle.clone(),
);
let mut last_cleanup = std::time::Instant::now();
let mut poll_fds = [
libc::pollfd {
fd: shared.tx_wake.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: shared.proxy_wake.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
];
loop {
let now = smoltcp_now();
while let Some(frame) = device.stage_next_frame() {
if handle_gateway_icmp_echo(frame, &config, &shared) {
device.drop_staged_frame();
continue;
}
if icmp_relay.relay_outbound_if_echo(frame, &config, &network_policy) {
device.drop_staged_frame();
continue;
}
match classify_frame(frame) {
FrameAction::TcpSyn { src, dst } => {
let allow = match DnsPortType::from_tcp(dst.port()) {
DnsPortType::Dns => true,
DnsPortType::EncryptedDns => {
if tls_state.is_some() {
true
} else {
tracing::debug!(%dst, "DoT port refused (TLS interception not configured); stub should fall back to TCP/53");
false
}
}
DnsPortType::AlternativeDns => {
tracing::debug!(%dst, "alternative-DNS TCP port refused; stub should fall back to TCP/53");
false
}
DnsPortType::Other => match network_policy.evaluate_egress_with_source(
dst,
Protocol::Tcp,
&shared,
HostnameSource::Deferred,
) {
EgressEvaluation::Allow | EgressEvaluation::DeferUntilHostname => true,
EgressEvaluation::Deny => false,
},
};
if allow && !conn_tracker.has_socket_for(&src, &dst) {
conn_tracker.create_tcp_socket(src, dst, &mut sockets);
}
iface.poll_ingress_single(now, &mut device, &mut sockets);
}
FrameAction::UdpRelay { src, dst } => {
if let Some(ref tls) = tls_state
&& tls.config.intercepted_ports.contains(&dst.port())
&& tls.config.block_quic_on_intercept
{
device.drop_staged_frame();
continue;
}
match DnsPortType::from_udp(dst.port()) {
DnsPortType::Dns => {
device.drop_staged_frame();
continue;
}
DnsPortType::EncryptedDns => {
device.drop_staged_frame();
continue;
}
DnsPortType::AlternativeDns => {
tracing::debug!(%dst, "alternative-DNS UDP port dropped; stub should fall back to UDP/53");
device.drop_staged_frame();
continue;
}
DnsPortType::Other => {}
}
if network_policy
.evaluate_egress(dst, Protocol::Udp, &shared)
.is_deny()
{
device.drop_staged_frame();
continue;
}
let host_dst = resolve_host_dst(dst, config.gateway);
udp_relay.relay_outbound(frame, src, dst, host_dst);
device.drop_staged_frame();
}
FrameAction::Dns | FrameAction::Passthrough => {
iface.poll_ingress_single(now, &mut device, &mut sockets);
}
}
}
loop {
let result = iface.poll_egress(now, &mut device, &mut sockets);
if matches!(result, smoltcp::iface::PollResult::None) {
break;
}
}
iface.poll_maintenance(now);
if device.frames_emitted.swap(false, Ordering::Relaxed) {
shared.rx_wake.wake();
}
conn_tracker.relay_data(&mut sockets);
dns_interceptor.process(&mut sockets);
port_publisher.accept_inbound(&mut iface, &mut sockets, &shared, &tokio_handle);
port_publisher.relay_data(&mut sockets);
let new_conns = conn_tracker.take_new_connections(&mut sockets);
for conn in new_conns {
if let Some(ref tls_state) = tls_state
&& tls_state
.config
.intercepted_ports
.contains(&conn.dst.port())
{
let conn_dst = resolve_host_dst(conn.dst, config.gateway);
tls_proxy::spawn_tls_proxy(
&tokio_handle,
conn_dst,
conn.from_smoltcp,
conn.to_smoltcp,
shared.clone(),
tls_state.clone(),
network_policy.clone(),
);
continue;
}
if conn.dst.port() == 53 {
DnsTcpProxy::spawn(
&tokio_handle,
conn.dst,
conn.from_smoltcp,
conn.to_smoltcp,
dns_forwarder_handle.clone(),
shared.clone(),
);
continue;
}
if conn.dst.port() == 853
&& let Some(ref tls_state) = tls_state
{
DotProxy::spawn(
&tokio_handle,
conn.dst,
conn.from_smoltcp,
conn.to_smoltcp,
dns_forwarder_handle.clone(),
tls_state.clone(),
shared.clone(),
);
continue;
}
let connect_dst = resolve_host_dst(conn.dst, config.gateway);
proxy::spawn_tcp_proxy(
&tokio_handle,
conn.dst,
connect_dst,
conn.from_smoltcp,
conn.to_smoltcp,
shared.clone(),
network_policy.clone(),
);
}
if last_cleanup.elapsed() >= std::time::Duration::from_secs(1) {
conn_tracker.cleanup_closed(&mut sockets);
port_publisher.cleanup_closed(&mut sockets);
udp_relay.cleanup_expired();
shared.cleanup_resolved_hostnames();
last_cleanup = std::time::Instant::now();
}
loop {
let result = iface.poll_egress(now, &mut device, &mut sockets);
if matches!(result, smoltcp::iface::PollResult::None) {
break;
}
}
if device.frames_emitted.swap(false, Ordering::Relaxed) {
shared.rx_wake.wake();
}
let timeout_ms = iface
.poll_delay(now, &sockets)
.map(|d| d.total_millis().min(i32::MAX as u64) as i32)
.unwrap_or(100);
unsafe {
libc::poll(
poll_fds.as_mut_ptr(),
poll_fds.len() as libc::nfds_t,
timeout_ms,
);
}
if poll_fds[0].revents & libc::POLLIN != 0 {
shared.tx_wake.drain();
}
if poll_fds[1].revents & libc::POLLIN != 0 {
shared.proxy_wake.drain();
}
}
}
pub(crate) fn resolve_host_dst(dst: SocketAddr, gateway: GatewayIps) -> SocketAddr {
match dst.ip() {
IpAddr::V4(v4) if gateway.ipv4 == Some(v4) => {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), dst.port())
}
IpAddr::V6(v6) if gateway.ipv6 == Some(v6) => {
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), dst.port())
}
_ => dst,
}
}
fn smoltcp_now() -> Instant {
static EPOCH: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
let epoch = EPOCH.get_or_init(std::time::Instant::now);
let elapsed = epoch.elapsed();
Instant::from_millis(elapsed.as_millis() as i64)
}
fn handle_gateway_icmp_echo(frame: &[u8], config: &PollLoopConfig, shared: &SharedState) -> bool {
let Ok(eth) = EthernetFrame::new_checked(frame) else {
return false;
};
let reply = match eth.ethertype() {
EthernetProtocol::Ipv4 => gateway_icmpv4_echo_reply(ð, config),
EthernetProtocol::Ipv6 => gateway_icmpv6_echo_reply(ð, config),
_ => None,
};
let Some(reply) = reply else {
return false;
};
let reply_len = reply.len();
if shared.rx_ring.push(reply).is_ok() {
shared.add_rx_bytes(reply_len);
shared.rx_wake.wake();
}
true
}
fn gateway_icmpv4_echo_reply(
eth: &EthernetFrame<&[u8]>,
config: &PollLoopConfig,
) -> Option<Vec<u8>> {
let gateway_ipv4 = config.gateway.ipv4?;
let ipv4 = Ipv4Packet::new_checked(eth.payload()).ok()?;
if ipv4.dst_addr() != gateway_ipv4 || ipv4.next_header() != IpProtocol::Icmp {
return None;
}
let icmp = Icmpv4Packet::new_checked(ipv4.payload()).ok()?;
let Icmpv4Repr::EchoRequest {
ident,
seq_no,
data,
} = Icmpv4Repr::parse(&icmp, &smoltcp::phy::ChecksumCapabilities::default()).ok()?
else {
return None;
};
let ipv4_repr = Ipv4Repr {
src_addr: gateway_ipv4,
dst_addr: ipv4.src_addr(),
next_header: IpProtocol::Icmp,
payload_len: 8 + data.len(),
hop_limit: 64,
};
let icmp_repr = Icmpv4Repr::EchoReply {
ident,
seq_no,
data,
};
let mut reply = vec![0u8; 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len()];
let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
reply_eth.set_dst_addr(eth.src_addr());
reply_eth.set_ethertype(EthernetProtocol::Ipv4);
ipv4_repr.emit(
&mut Ipv4Packet::new_unchecked(&mut reply[14..34]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut reply[34..]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
Some(reply)
}
fn gateway_icmpv6_echo_reply(
eth: &EthernetFrame<&[u8]>,
config: &PollLoopConfig,
) -> Option<Vec<u8>> {
let gateway_ipv6 = config.gateway.ipv6?;
let ipv6 = Ipv6Packet::new_checked(eth.payload()).ok()?;
if ipv6.dst_addr() != gateway_ipv6 || ipv6.next_header() != IpProtocol::Icmpv6 {
return None;
}
let icmp = Icmpv6Packet::new_checked(ipv6.payload()).ok()?;
let Icmpv6Repr::EchoRequest {
ident,
seq_no,
data,
} = Icmpv6Repr::parse(
&ipv6.src_addr(),
&ipv6.dst_addr(),
&icmp,
&smoltcp::phy::ChecksumCapabilities::default(),
)
.ok()?
else {
return None;
};
let ipv6_repr = Ipv6Repr {
src_addr: gateway_ipv6,
dst_addr: ipv6.src_addr(),
next_header: IpProtocol::Icmpv6,
payload_len: icmp_repr_buffer_len_v6(data),
hop_limit: 64,
};
let icmp_repr = Icmpv6Repr::EchoReply {
ident,
seq_no,
data,
};
let ipv6_hdr_len = 40;
let mut reply = vec![0u8; 14 + ipv6_hdr_len + icmp_repr.buffer_len()];
let mut reply_eth = EthernetFrame::new_unchecked(&mut reply);
reply_eth.set_src_addr(EthernetAddress(config.gateway_mac));
reply_eth.set_dst_addr(eth.src_addr());
reply_eth.set_ethertype(EthernetProtocol::Ipv6);
ipv6_repr.emit(&mut Ipv6Packet::new_unchecked(&mut reply[14..54]));
icmp_repr.emit(
&gateway_ipv6,
&ipv6.src_addr(),
&mut Icmpv6Packet::new_unchecked(&mut reply[54..]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
Some(reply)
}
fn icmp_repr_buffer_len_v6(data: &[u8]) -> usize {
Icmpv6Repr::EchoReply {
ident: 0,
seq_no: 0,
data,
}
.buffer_len()
}
fn classify_ipv4(payload: &[u8]) -> FrameAction {
let Ok(ipv4) = Ipv4Packet::new_checked(payload) else {
return FrameAction::Passthrough;
};
classify_transport(
ipv4.next_header(),
ipv4.src_addr().into(),
ipv4.dst_addr().into(),
ipv4.payload(),
)
}
fn classify_ipv6(payload: &[u8]) -> FrameAction {
let Ok(ipv6) = Ipv6Packet::new_checked(payload) else {
return FrameAction::Passthrough;
};
classify_transport(
ipv6.next_header(),
ipv6.src_addr().into(),
ipv6.dst_addr().into(),
ipv6.payload(),
)
}
fn classify_transport(
protocol: IpProtocol,
src_ip: std::net::IpAddr,
dst_ip: std::net::IpAddr,
transport_payload: &[u8],
) -> FrameAction {
match protocol {
IpProtocol::Tcp => {
let Ok(tcp) = TcpPacket::new_checked(transport_payload) else {
return FrameAction::Passthrough;
};
if tcp.syn() && !tcp.ack() {
FrameAction::TcpSyn {
src: SocketAddr::new(src_ip, tcp.src_port()),
dst: SocketAddr::new(dst_ip, tcp.dst_port()),
}
} else {
FrameAction::Passthrough
}
}
IpProtocol::Udp => {
let Ok(udp) = UdpPacket::new_checked(transport_payload) else {
return FrameAction::Passthrough;
};
if DnsPortType::from_udp(udp.dst_port()) == DnsPortType::Dns {
FrameAction::Dns
} else {
FrameAction::UdpRelay {
src: SocketAddr::new(src_ip, udp.src_port()),
dst: SocketAddr::new(dst_ip, udp.dst_port()),
}
}
}
_ => FrameAction::Passthrough, }
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
ArpOperation, ArpPacket, ArpRepr, EthernetRepr, Icmpv4Packet, Icmpv4Repr, Ipv4Repr,
};
use crate::device::SmoltcpDevice;
use crate::shared::SharedState;
fn build_tcp_syn_frame(
src_ip: [u8; 4],
dst_ip: [u8; 4],
src_port: u16,
dst_port: u16,
) -> Vec<u8> {
let mut frame = vec![0u8; 14 + 20 + 20];
frame[12] = 0x08; frame[13] = 0x00;
let ip = &mut frame[14..34];
ip[0] = 0x45; let total_len = 40u16; ip[2..4].copy_from_slice(&total_len.to_be_bytes());
ip[6] = 0x40; ip[8] = 64; ip[9] = 6; ip[12..16].copy_from_slice(&src_ip);
ip[16..20].copy_from_slice(&dst_ip);
let tcp = &mut frame[34..54];
tcp[0..2].copy_from_slice(&src_port.to_be_bytes());
tcp[2..4].copy_from_slice(&dst_port.to_be_bytes());
tcp[12] = 0x50; tcp[13] = 0x02;
frame
}
fn build_udp_frame(src_ip: [u8; 4], dst_ip: [u8; 4], src_port: u16, dst_port: u16) -> Vec<u8> {
let mut frame = vec![0u8; 14 + 20 + 8];
frame[12] = 0x08;
frame[13] = 0x00;
let ip = &mut frame[14..34];
ip[0] = 0x45;
let total_len = 28u16; ip[2..4].copy_from_slice(&total_len.to_be_bytes());
ip[8] = 64;
ip[9] = 17; ip[12..16].copy_from_slice(&src_ip);
ip[16..20].copy_from_slice(&dst_ip);
let udp = &mut frame[34..42];
udp[0..2].copy_from_slice(&src_port.to_be_bytes());
udp[2..4].copy_from_slice(&dst_port.to_be_bytes());
let udp_len = 8u16;
udp[4..6].copy_from_slice(&udp_len.to_be_bytes());
frame
}
fn build_icmpv4_echo_frame(
src_mac: [u8; 6],
dst_mac: [u8; 6],
src_ip: [u8; 4],
dst_ip: [u8; 4],
ident: u16,
seq_no: u16,
data: &[u8],
) -> Vec<u8> {
let ipv4_repr = Ipv4Repr {
src_addr: Ipv4Addr::from(src_ip),
dst_addr: Ipv4Addr::from(dst_ip),
next_header: IpProtocol::Icmp,
payload_len: 8 + data.len(),
hop_limit: 64,
};
let icmp_repr = Icmpv4Repr::EchoRequest {
ident,
seq_no,
data,
};
let frame_len = 14 + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
let mut frame = vec![0u8; frame_len];
let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
EthernetRepr {
src_addr: EthernetAddress(src_mac),
dst_addr: EthernetAddress(dst_mac),
ethertype: EthernetProtocol::Ipv4,
}
.emit(&mut eth_frame);
ipv4_repr.emit(
&mut Ipv4Packet::new_unchecked(&mut frame[14..34]),
&ChecksumCapabilities::default(),
);
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut frame[34..]),
&ChecksumCapabilities::default(),
);
frame
}
fn build_arp_request_frame(src_mac: [u8; 6], src_ip: [u8; 4], target_ip: [u8; 4]) -> Vec<u8> {
let mut frame = vec![0u8; 14 + 28];
let mut eth_frame = EthernetFrame::new_unchecked(&mut frame);
EthernetRepr {
src_addr: EthernetAddress(src_mac),
dst_addr: EthernetAddress([0xff; 6]),
ethertype: EthernetProtocol::Arp,
}
.emit(&mut eth_frame);
ArpRepr::EthernetIpv4 {
operation: ArpOperation::Request,
source_hardware_addr: EthernetAddress(src_mac),
source_protocol_addr: Ipv4Addr::from(src_ip),
target_hardware_addr: EthernetAddress([0x00; 6]),
target_protocol_addr: Ipv4Addr::from(target_ip),
}
.emit(&mut ArpPacket::new_unchecked(&mut frame[14..]));
frame
}
#[test]
fn classify_tcp_syn() {
let frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
match classify_frame(&frame) {
FrameAction::TcpSyn { src, dst } => {
assert_eq!(
src,
SocketAddr::new(Ipv4Addr::new(10, 0, 0, 2).into(), 54321)
);
assert_eq!(
dst,
SocketAddr::new(Ipv4Addr::new(93, 184, 216, 34).into(), 443)
);
}
_ => panic!("expected TcpSyn"),
}
}
#[test]
fn classify_tcp_ack_is_passthrough() {
let mut frame = build_tcp_syn_frame([10, 0, 0, 2], [93, 184, 216, 34], 54321, 443);
frame[34 + 13] = 0x10; assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
}
#[test]
fn classify_udp_dns() {
let frame = build_udp_frame([10, 0, 0, 2], [10, 0, 0, 1], 12345, 53);
assert!(matches!(classify_frame(&frame), FrameAction::Dns));
}
#[test]
fn classify_udp_non_dns() {
let frame = build_udp_frame([10, 0, 0, 2], [8, 8, 8, 8], 12345, 443);
match classify_frame(&frame) {
FrameAction::UdpRelay { src, dst } => {
assert_eq!(src.port(), 12345);
assert_eq!(dst.port(), 443);
}
_ => panic!("expected UdpRelay"),
}
}
#[test]
fn classify_arp_is_passthrough() {
let mut frame = vec![0u8; 42]; frame[12] = 0x08;
frame[13] = 0x06; assert!(matches!(classify_frame(&frame), FrameAction::Passthrough));
}
#[test]
fn classify_garbage_is_passthrough() {
assert!(matches!(classify_frame(&[]), FrameAction::Passthrough));
assert!(matches!(classify_frame(&[0; 5]), FrameAction::Passthrough));
}
#[test]
fn gateway_replies_to_icmp_echo_requests() {
fn drive_one_frame(
device: &mut SmoltcpDevice,
iface: &mut Interface,
sockets: &mut SocketSet<'_>,
shared: &Arc<SharedState>,
poll_config: &PollLoopConfig,
now: Instant,
) {
let frame = device.stage_next_frame().expect("expected staged frame");
if handle_gateway_icmp_echo(frame, poll_config, shared) {
device.drop_staged_frame();
return;
}
let _ = iface.poll_ingress_single(now, device, sockets);
let _ = iface.poll_egress(now, device, sockets);
}
let shared = Arc::new(SharedState::new(4));
let poll_config = PollLoopConfig {
gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
gateway: GatewayIps {
ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
ipv6: Some(Ipv6Addr::LOCALHOST),
},
guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
guest_ipv6: None,
mtu: 1500,
};
let guest_ipv4 = poll_config.guest_ipv4.unwrap();
let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
let mut iface = create_interface(&mut device, &poll_config);
let mut sockets = SocketSet::new(vec![]);
let now = smoltcp_now();
shared
.tx_ring
.push(build_arp_request_frame(
poll_config.guest_mac,
guest_ipv4.octets(),
gateway_ipv4.octets(),
))
.unwrap();
shared
.tx_ring
.push(build_icmpv4_echo_frame(
poll_config.guest_mac,
poll_config.gateway_mac,
guest_ipv4.octets(),
gateway_ipv4.octets(),
0x1234,
0xABCD,
b"ping",
))
.unwrap();
drive_one_frame(
&mut device,
&mut iface,
&mut sockets,
&shared,
&poll_config,
now,
);
let _ = shared.rx_ring.pop().expect("expected ARP reply");
drive_one_frame(
&mut device,
&mut iface,
&mut sockets,
&shared,
&poll_config,
now,
);
let reply = shared.rx_ring.pop().expect("expected ICMP echo reply");
let eth = EthernetFrame::new_checked(&reply).expect("valid ethernet frame");
assert_eq!(eth.src_addr(), EthernetAddress(poll_config.gateway_mac));
assert_eq!(eth.dst_addr(), EthernetAddress(poll_config.guest_mac));
assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
let ipv4 = Ipv4Packet::new_checked(eth.payload()).expect("valid IPv4 packet");
assert_eq!(ipv4.src_addr(), gateway_ipv4);
assert_eq!(ipv4.dst_addr(), guest_ipv4);
assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
let icmp = Icmpv4Packet::new_checked(ipv4.payload()).expect("valid ICMP packet");
let icmp_repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default())
.expect("valid ICMP echo reply");
assert_eq!(
icmp_repr,
Icmpv4Repr::EchoReply {
ident: 0x1234,
seq_no: 0xABCD,
data: b"ping",
}
);
}
fn test_gateway() -> GatewayIps {
GatewayIps {
ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
}
}
#[test]
fn resolve_host_dst_matches_ipv4() {
let gw = test_gateway();
let dst = SocketAddr::new(IpAddr::V4(gw.ipv4.unwrap()), 8080);
assert_eq!(
resolve_host_dst(dst, gw),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
);
}
#[test]
fn resolve_host_dst_matches_ipv6() {
let gw = test_gateway();
let dst = SocketAddr::new(IpAddr::V6(gw.ipv6.unwrap()), 8080);
assert_eq!(
resolve_host_dst(dst, gw),
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 8080)
);
}
#[test]
fn resolve_host_dst_passes_through_when_family_absent() {
let gw = GatewayIps {
ipv4: None,
ipv6: Some("fd42:6d73:62::1".parse().unwrap()),
};
let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(100, 96, 0, 1)), 8080);
assert_eq!(resolve_host_dst(dst, gw), dst);
}
#[test]
fn resolve_host_dst_passes_through_non_gateway() {
let gw = test_gateway();
let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 443);
assert_eq!(resolve_host_dst(dst, gw), dst);
}
#[test]
fn external_icmp_echo_requests_are_not_answered_locally() {
fn drive_one_frame(
device: &mut SmoltcpDevice,
iface: &mut Interface,
sockets: &mut SocketSet<'_>,
shared: &Arc<SharedState>,
poll_config: &PollLoopConfig,
now: Instant,
) {
let frame = device.stage_next_frame().expect("expected staged frame");
if handle_gateway_icmp_echo(frame, poll_config, shared) {
device.drop_staged_frame();
return;
}
let _ = iface.poll_ingress_single(now, device, sockets);
let _ = iface.poll_egress(now, device, sockets);
}
let shared = Arc::new(SharedState::new(4));
let poll_config = PollLoopConfig {
gateway_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
guest_mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x02],
gateway: GatewayIps {
ipv4: Some(Ipv4Addr::new(100, 96, 0, 1)),
ipv6: Some(Ipv6Addr::LOCALHOST),
},
guest_ipv4: Some(Ipv4Addr::new(100, 96, 0, 2)),
guest_ipv6: None,
mtu: 1500,
};
let guest_ipv4 = poll_config.guest_ipv4.unwrap();
let gateway_ipv4 = poll_config.gateway.ipv4.unwrap();
let mut device = SmoltcpDevice::new(shared.clone(), poll_config.mtu);
let mut iface = create_interface(&mut device, &poll_config);
let mut sockets = SocketSet::new(vec![]);
let now = smoltcp_now();
shared
.tx_ring
.push(build_arp_request_frame(
poll_config.guest_mac,
guest_ipv4.octets(),
gateway_ipv4.octets(),
))
.unwrap();
shared
.tx_ring
.push(build_icmpv4_echo_frame(
poll_config.guest_mac,
poll_config.gateway_mac,
guest_ipv4.octets(),
[142, 251, 216, 46],
0x1234,
0xABCD,
b"ping",
))
.unwrap();
drive_one_frame(
&mut device,
&mut iface,
&mut sockets,
&shared,
&poll_config,
now,
);
let _ = shared.rx_ring.pop().expect("expected ARP reply");
drive_one_frame(
&mut device,
&mut iface,
&mut sockets,
&shared,
&poll_config,
now,
);
assert!(
shared.rx_ring.pop().is_none(),
"external ICMP should not be answered locally"
);
}
}