Compare commits

..
Author SHA1 Message Date
Nikolay Edigaryev 431a2384a4 WIP: support peer MAC addresses in --allow/--block
To enable cross-VM communication.
2026-05-19 07:35:00 +01:00
10 changed files with 447 additions and 214 deletions
Generated
+98 -7
View File
@@ -225,7 +225,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -715,6 +715,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno"
version = "0.3.12"
@@ -725,6 +736,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "fastrand"
version = "2.1.1"
@@ -1447,6 +1468,16 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
@@ -1789,6 +1820,21 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "pcap"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2eecc2ddc671ec563b5b39f846556aade68a65d1afb14d8fe6b30b0457d75"
dependencies = [
"bitflags 1.3.2",
"errno 0.2.8",
"libc",
"libloading",
"pkg-config",
"regex",
"windows-sys 0.36.1",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -1865,7 +1911,7 @@ dependencies = [
"hermit-abi 0.5.1",
"pin-project-lite",
"rustix 1.0.7",
"windows-sys 0.61.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2158,7 +2204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [
"bitflags 2.9.4",
"errno",
"errno 0.3.12",
"libc",
"linux-raw-sys 0.4.14",
"windows-sys 0.52.0",
@@ -2171,7 +2217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.4",
"errno",
"errno 0.3.12",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.59.0",
@@ -2615,12 +2661,14 @@ dependencies = [
"dhcproto",
"ip_network",
"ipnet",
"ipnetwork",
"libc",
"log",
"mac_address",
"nix 0.31.2",
"num_enum 0.7.6",
"oslog",
"pcap",
"pnet_datalink",
"polling",
"prefix-trie",
@@ -3370,7 +3418,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3404,6 +3452,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -3433,9 +3494,9 @@ dependencies = [
[[package]]
name = "windows-sys"
version = "0.61.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
@@ -3485,6 +3546,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@@ -3497,6 +3564,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@@ -3521,6 +3594,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@@ -3533,6 +3612,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@@ -3557,6 +3642,12 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
+2
View File
@@ -35,6 +35,8 @@ log = "0.4.29"
serial_test = "3"
coarsetime = "0.1.37"
pnet_datalink = "0.35.0"
pcap = "2.4.0"
ipnetwork = "0.20"
[profile.release]
debug = true
+210
View File
@@ -0,0 +1,210 @@
use crate::dhcp_snooper::Lease;
use anyhow::{Context, Result, anyhow};
use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, MessageType, Opcode, OptionCode};
use smoltcp::wire::{
EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Address, Ipv4Packet, UdpPacket,
};
use std::collections::HashMap;
use std::os::unix::io::RawFd;
use std::time::{Duration, Instant};
const TABLE_PRINT_INTERVAL: Duration = Duration::from_secs(5);
unsafe extern "C" {
fn pcap_get_selectable_fd(pcap: *mut libc::c_void) -> libc::c_int;
}
#[derive(Default)]
pub struct DhcpSnooperGlobal {
pcap_capture: Option<pcap::Capture<pcap::Active>>,
pcap_fd: Option<RawFd>,
leases_by_mac: HashMap<[u8; 6], Lease>,
uncertainty_duration: Duration,
last_table_print: Option<Instant>,
}
impl DhcpSnooperGlobal {
pub fn new(gateway_interface_name: &str, uncertainty_duration: Duration) -> Result<Self> {
// Start capturing packets on bridge interface in promiscuous mode.
let mut pcap_capture = pcap::Capture::from_device(gateway_interface_name)?
.promisc(true)
.timeout(1)
.immediate_mode(true)
.open()?;
// Capture packets using a filter to avoid wasting CPU cycles in user-space.
pcap_capture
.filter("udp and (port 67 or port 68)", true)
.context("failed to install DHCP pcap filter")?;
// Prepare packet capture for event-driven consumption.
let pcap_capture = pcap_capture.setnonblock()?;
let pcap_fd = unsafe { pcap_get_selectable_fd(pcap_capture.as_ptr().cast()) };
if pcap_fd == -1 {
return Err(anyhow!("failed to call pcap_get_selectable_fd(3)"));
}
Ok(DhcpSnooperGlobal {
pcap_capture: Some(pcap_capture),
pcap_fd: Some(pcap_fd),
uncertainty_duration,
..Default::default()
})
}
pub fn disabled(uncertainty_duration: Duration) -> Self {
DhcpSnooperGlobal {
uncertainty_duration,
..Default::default()
}
}
pub fn pcap_raw_fd(&self) -> Option<RawFd> {
self.pcap_fd
}
pub fn read_pcap(&mut self, mut handle_packet: impl FnMut(&mut Self, &[u8])) -> Result<()> {
let Some(mut pcap_capture) = self.pcap_capture.take() else {
return Ok(());
};
let result = loop {
match pcap_capture.next_packet() {
Ok(packet) => handle_packet(self, packet.data),
Err(pcap::Error::TimeoutExpired) => break Ok(()),
Err(err) => break Err(err.into()),
}
};
self.pcap_capture = Some(pcap_capture);
result
}
pub fn register_ethernet_packet(&mut self, packet: &[u8]) {
Self::register_ethernet_packet_with(
&mut self.leases_by_mac,
self.uncertainty_duration,
packet,
);
}
fn register_ethernet_packet_with(
leases_by_mac: &mut HashMap<[u8; 6], Lease>,
uncertainty_duration: Duration,
packet: &[u8],
) {
let Some(dhcp_packet) = Self::dhcp_payload(packet) else {
return;
};
let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet);
let message = match dhcproto::v4::Message::decode(&mut decoder) {
Ok(message) => message,
Err(_) => return,
};
println!("{message}");
let Some(mac) = Self::message_mac(&message) else {
return;
};
match message.opts().msg_type() {
Some(MessageType::Ack) => {
let lease_time = match message.opts().get(OptionCode::AddressLeaseTime) {
Some(DhcpOption::AddressLeaseTime(lease_time)) => *lease_time,
_ => 600,
};
let mut lease_duration = Duration::from_secs(lease_time as u64);
lease_duration = lease_duration.saturating_sub(uncertainty_duration);
leases_by_mac.insert(
mac,
Lease::new(message.yiaddr(), lease_duration, Default::default()),
);
println!(
"DHCP global lease learned: {} -> {}",
Self::format_mac(&mac),
message.yiaddr()
);
}
Some(MessageType::Nak) => {
leases_by_mac.remove(&mac);
println!("DHCP global lease removed: {}", Self::format_mac(&mac));
}
_ => {}
}
}
pub fn print_table_periodically(&mut self) {
let now = Instant::now();
if self
.last_table_print
.is_some_and(|last_print| now.duration_since(last_print) < TABLE_PRINT_INTERVAL)
{
return;
}
self.last_table_print = Some(now);
self.print_table();
}
pub fn valid_ip_for_mac(&self, mac: &[u8; 6], ip: Ipv4Address) -> bool {
self.leases_by_mac
.get(mac)
.is_some_and(|lease| lease.valid_ip_source(ip))
}
fn print_table(&self) {
if self.leases_by_mac.is_empty() {
println!("DHCP global leases: <empty>");
return;
}
println!("DHCP global leases:");
for (mac, lease) in &self.leases_by_mac {
let state = if lease.valid() { "valid" } else { "expired" };
println!(
" {} -> {} ({state})",
Self::format_mac(mac),
lease.address()
);
}
}
fn dhcp_payload(packet: &[u8]) -> Option<&[u8]> {
let frame = EthernetFrame::new_checked(packet).ok()?;
if frame.ethertype() != EthernetProtocol::Ipv4 {
return None;
}
let ipv4_packet = Ipv4Packet::new_checked(frame.payload()).ok()?;
if ipv4_packet.next_header() != IpProtocol::Udp {
return None;
}
let udp_packet = UdpPacket::new_checked(ipv4_packet.payload()).ok()?;
if !matches!(udp_packet.src_port(), 67 | 68) && !matches!(udp_packet.dst_port(), 67 | 68) {
return None;
}
Some(udp_packet.payload())
}
fn message_mac(message: &dhcproto::v4::Message) -> Option<[u8; 6]> {
if message.opcode() != Opcode::BootReply {
return None;
}
message.chaddr().try_into().ok()
}
fn format_mac(mac: &[u8; 6]) -> String {
mac.iter()
.map(|octet| format!("{octet:02x}"))
.collect::<Vec<_>>()
.join(":")
}
}
+24 -65
View File
@@ -1,8 +1,9 @@
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use ipnetwork::{IpNetwork, Ipv4Network};
use log::info;
use pnet_datalink::MacAddr;
use smoltcp::wire::EthernetAddress;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -29,6 +30,7 @@ pub struct Host {
interface: vmnet::Interface,
new_packets_rx: UnixDatagram,
callback_can_continue_tx: SyncSender<()>,
pub gateway_interface_name: String,
pub gateway_ip: smoltcp::wire::Ipv4Address,
pub gateway_mac: EthernetAddress,
pub max_packet_size: u64,
@@ -51,7 +53,7 @@ impl Host {
)
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
// Retrieve the first IP (gateway) used for this interface
let Some(Parameter::StartAddress(start_address)) =
interface.parameters().get(ParameterKind::StartAddress)
else {
@@ -62,7 +64,7 @@ impl Host {
let start_address = Ipv4Addr::from_str(&start_address)
.context("failed to parse vmnet's interface start address")?;
// Retrieve last IP used for this interface and calculate the prefix
// Retrieve the last IP used for this interface
let Some(Parameter::EndAddress(end_address)) =
interface.parameters().get(ParameterKind::EndAddress)
else {
@@ -71,20 +73,14 @@ impl Host {
let end_address = Ipv4Addr::from_str(&end_address)
.context("failed to parse vmnet's interface end address")?;
let Some(prefix) = Self::ipv4_range_prefix(start_address, end_address) else {
return Err(anyhow!(
"failed to resolve vmnet's interface: prefix ambiguity for {}–{}",
start_address,
end_address
));
};
// Determine the prefix used for this interface
let prefix = Self::ipv4_range_prefix(start_address, end_address);
// Figure out the gateway's interface MAC address
let Some(gateway_mac) = Self::interface_mac_for_ip(start_address, prefix) else {
let Some((gateway_name, gateway_mac)) = Self::interface_ip_to_mac(start_address, prefix)
else {
return Err(anyhow!(
"failed to resolve vmnet's interface: no interface found with {}/{} CIDR",
start_address,
prefix
"failed to resolve vmnet's interface from start address {}",
start_address
));
};
let gateway_mac = EthernetAddress(gateway_mac.octets());
@@ -134,6 +130,7 @@ impl Host {
interface,
new_packets_rx,
callback_can_continue_tx,
gateway_interface_name: gateway_name,
gateway_ip: start_address,
gateway_mac,
max_packet_size,
@@ -142,30 +139,22 @@ impl Host {
})
}
fn interface_mac_for_ip(ip: Ipv4Addr, prefix: u8) -> Option<pnet_datalink::MacAddr> {
fn interface_ip_to_mac(ip: Ipv4Addr, prefix: u8) -> Option<(String, MacAddr)> {
let ip_network = IpNetwork::V4(Ipv4Network::new(ip, prefix).unwrap());
for iface in pnet_datalink::interfaces() {
if iface
.ips
.iter()
.any(|network| network.ip() == IpAddr::V4(ip) && network.prefix() == prefix)
if iface.ips.contains(&ip_network)
&& let Some(mac) = iface.mac
{
return Some(mac);
return Some((iface.name, mac));
}
}
None
}
fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> Option<u8> {
let start_address = start_address.to_bits();
let end_address = end_address.to_bits();
if start_address > end_address {
return None;
}
Some((start_address ^ end_address).leading_zeros() as u8)
fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> u8 {
(u32::from(start_address) ^ u32::from(end_address)).leading_zeros() as u8
}
}
@@ -262,13 +251,13 @@ mod tests {
use std::net::Ipv4Addr;
#[test]
fn ipv4_range_prefix_for_class_c_subnet() {
fn ipv4_range_prefix_for_vmnet_class_c_range() {
assert_eq!(
Host::ipv4_range_prefix(
Ipv4Addr::new(192, 168, 64, 0),
Ipv4Addr::new(192, 168, 64, 255),
Ipv4Addr::new(192, 168, 64, 1),
Ipv4Addr::new(192, 168, 64, 254),
),
Some(24)
24
);
}
@@ -276,37 +265,7 @@ mod tests {
fn ipv4_range_prefix_for_single_address() {
assert_eq!(
Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 1)),
Some(32)
);
}
#[test]
fn ipv4_range_prefix_for_class_c_usable_range() {
assert_eq!(
Host::ipv4_range_prefix(
Ipv4Addr::new(192, 168, 64, 1),
Ipv4Addr::new(192, 168, 64, 254),
),
Some(24)
);
}
#[test]
fn ipv4_range_prefix_for_containing_class_c_subnet() {
assert_eq!(
Host::ipv4_range_prefix(
Ipv4Addr::new(192, 168, 64, 2),
Ipv4Addr::new(192, 168, 64, 254),
),
Some(24)
);
}
#[test]
fn ipv4_range_prefix_rejects_inverted_range() {
assert_eq!(
Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(10, 0, 0, 1)),
None
32
);
}
}
+1
View File
@@ -1,4 +1,5 @@
mod dhcp_snooper;
mod dhcp_snooper_global;
mod host;
pub use host::NetType;
mod poller;
+33 -2
View File
@@ -12,6 +12,7 @@ pub struct Poller<'poller> {
timeout: Duration,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
pcap_fd: Option<BorrowedFd<'poller>>,
}
#[derive(IntoPrimitive)]
@@ -19,13 +20,22 @@ pub struct Poller<'poller> {
enum EventKey {
VM,
Host,
Pcap,
Interrupt,
}
pub struct Readiness {
pub vm_readable: bool,
pub host_readable: bool,
pub pcap_readable: bool,
pub interrupt: bool,
}
impl Poller<'_> {
pub fn new<'poller>(
vm_fd: RawFd,
host_fd: RawFd,
pcap_fd: Option<RawFd>,
timeout: Duration,
) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?;
@@ -36,6 +46,7 @@ impl Poller<'_> {
timeout,
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
pcap_fd: pcap_fd.map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }),
})
}
@@ -51,6 +62,13 @@ impl Poller<'_> {
self.host_interest(),
PollMode::Edge,
)?;
if let Some(pcap_fd) = self.pcap_fd {
self.poller.add_with_mode(
pcap_fd.as_raw_fd(),
self.pcap_interest(),
PollMode::Edge,
)?;
}
}
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
@@ -64,7 +82,7 @@ impl Poller<'_> {
self.events.clear();
}
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
pub fn wait(&mut self) -> Result<Readiness> {
self.poller.wait(&mut self.events, Some(self.timeout))?;
let vm_readable = self
@@ -75,12 +93,21 @@ impl Poller<'_> {
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Host));
let pcap_readable = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Pcap));
let interrupt = self
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Interrupt));
Ok((vm_readable, host_readable, interrupt))
Ok(Readiness {
vm_readable,
host_readable,
pcap_readable,
interrupt,
})
}
fn vm_interest(&self) -> polling::Event {
@@ -90,4 +117,8 @@ impl Poller<'_> {
fn host_interest(&self) -> polling::Event {
polling::Event::readable(EventKey::Host.into())
}
fn pcap_interest(&self) -> polling::Event {
polling::Event::readable(EventKey::Pcap.into())
}
}
+17
View File
@@ -37,14 +37,29 @@ impl Proxy<'_> {
}
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
// let mac = frame
// .src_addr()
// .as_bytes()
// .iter()
// .map(|b| format!("{:02x}", b))
// .collect::<Vec<_>>()
// .join(":");
//
// println!("{}", mac);
let from_gateway = frame.src_addr() == self.host.gateway_mac;
let peer_action = self.rules_mac.get(frame.src_addr().as_bytes());
let from_allowed_peer = peer_action == Some(&crate::proxy::Action::Allow);
if !from_gateway && !from_allowed_peer {
println!("dropping packet from {}", frame.src_addr());
return None;
}
if from_allowed_peer {
println!("allowing packet from peer {}", frame.src_addr());
}
match frame.ethertype() {
EthernetProtocol::Arp => Some(()),
EthernetProtocol::Ipv4 => Some(()),
@@ -53,6 +68,8 @@ impl Proxy<'_> {
}
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
// TODO: resolve IPs for the MAC addresses from --allow/--block too
if frame.ethertype() != EthernetProtocol::Ipv4 {
return;
}
+47 -67
View File
@@ -5,6 +5,7 @@ mod udp_packet_helper;
mod vm;
use crate::dhcp_snooper::DhcpSnooper;
use crate::dhcp_snooper_global::DhcpSnooperGlobal;
use crate::host::Host;
use crate::host::NetType;
use crate::poller::Poller;
@@ -29,6 +30,7 @@ pub struct Proxy<'proxy> {
poller: Poller<'proxy>,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
dhcp_snooper_global: DhcpSnooperGlobal,
rules: PrefixMap<Ipv4Net, Action>,
rules_mac: HashMap<[u8; 6], Action>,
enobufs_encountered: bool,
@@ -42,16 +44,6 @@ pub enum Target {
Host,
}
impl Target {
pub fn is_ipv4_default_route(&self) -> bool {
matches!(self, Target::Prefix(prefix) if *prefix == Ipv4Net::zero())
}
pub fn is_mac_address(&self) -> bool {
matches!(self, Target::MacAddress(_))
}
}
impl FromStr for Target {
type Err = ipnet::AddrParseError;
@@ -79,15 +71,31 @@ impl Proxy<'_> {
vm_fd: RawFd,
vm_mac_address: MacAddress,
vm_net_type: NetType,
enable_isolation: bool,
allow: Vec<Target>,
block: Vec<Target>,
exposed_ports: Vec<ExposedPort>,
) -> Result<Proxy<'proxy>> {
let allowing_all_ipv4 = allow.contains(&Target::Prefix(Ipv4Net::zero()));
let using_mac_filtering = allow
.iter()
.chain(block.iter())
.any(|target| matches!(target, Target::MacAddress(_)));
let enable_isolation = !allowing_all_ipv4 && !using_mac_filtering;
let vm = VM::new(vm_fd)?;
let host = Host::new(vm_net_type, enable_isolation)?;
let poller_timeout = Duration::from_millis(100);
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd(), poller_timeout)?;
let dhcp_snooper_global = if using_mac_filtering {
DhcpSnooperGlobal::new(&host.gateway_interface_name, poller_timeout)?
} else {
DhcpSnooperGlobal::disabled(poller_timeout)
};
let poller = Poller::new(
vm.as_raw_fd(),
host.as_raw_fd(),
dhcp_snooper_global.pcap_raw_fd(),
poller_timeout,
)?;
// Craft packet filter rules
//
@@ -130,6 +138,7 @@ impl Proxy<'_> {
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: DhcpSnooper::new(poller_timeout),
dhcp_snooper_global,
rules,
rules_mac,
enobufs_encountered: false,
@@ -151,26 +160,36 @@ impl Proxy<'_> {
self.poller.arm()?;
loop {
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
let readiness = self.poller.wait()?;
// Update coarse time for the DHCP snooper
coarsetime::Instant::update();
if vm_readable {
if readiness.vm_readable {
self.read_from_vm(buf.as_mut_slice())?;
}
if host_readable {
if readiness.host_readable {
self.read_from_host(&mut batch, &mut bufs)?;
}
if readiness.pcap_readable {
self.dhcp_snooper_global
.read_pcap(|snooper, packet| snooper.register_ethernet_packet(packet))?;
}
self.dhcp_snooper_global.print_table_periodically();
// Graceful termination
if interrupt {
if readiness.interrupt {
return Ok(());
}
// Timeout
if !vm_readable && !host_readable && !interrupt {
if !readiness.vm_readable
&& !readiness.host_readable
&& !readiness.pcap_readable
&& !readiness.interrupt
{
self.port_forwarder
.tick(&mut self.host, self.dhcp_snooper.lease());
}
@@ -230,13 +249,13 @@ impl Proxy<'_> {
mod tests {
use crate::NetType;
use crate::dhcp_snooper::Lease;
use crate::proxy::{Action, Proxy, Target};
use crate::proxy::{Action, Proxy};
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use prefix_trie::PrefixMap;
use serial_test::serial;
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Packet};
use smoltcp::wire::{Ipv4Address, Ipv4Packet};
use std::collections::HashSet;
use std::os::fd::AsRawFd;
use std::str::FromStr;
@@ -299,30 +318,6 @@ mod tests {
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, &proxy.host.gateway_ip.to_string()).is_some());
}
#[test]
#[serial]
fn test_allow_mac_takes_precedence_over_blocked_prefix() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let peer_mac = EthernetAddress([0x02, 0, 0, 0, 0, 0x02]);
let proxy = create_proxy(vm_ip, vec!["02:00:00:00:00:02"], vec!["0.0.0.0/0"]);
assert!(allowed_from_vm_ipv4_to_mac(&proxy, vm_ip, "192.168.0.3", peer_mac).is_some());
}
#[test]
#[serial]
fn test_block_mac_takes_precedence_over_allowed_prefix() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let peer_mac = EthernetAddress([0x02, 0, 0, 0, 0, 0x02]);
let proxy = create_proxy(
vm_ip,
vec!["192.168.0.0/24", "02:00:00:00:00:02"],
vec!["02:00:00:00:00:02"],
);
assert!(allowed_from_vm_ipv4_to_mac(&proxy, vm_ip, "192.168.0.3", peer_mac).is_none());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair(
AddressFamily::Unix,
@@ -333,24 +328,18 @@ mod tests {
.unwrap();
let vm_fd = Box::leak(Box::new(vm_fd));
let allow: Vec<Target> = allow
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect();
let block: Vec<Target> = block
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect();
let enable_isolation = !allow.iter().any(Target::is_ipv4_default_route)
&& !allow.iter().chain(block.iter()).any(Target::is_mac_address);
let mut proxy = Proxy::new(
vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
NetType::Nat,
enable_isolation,
allow,
block,
allow
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect(),
block
.into_iter()
.map(|cidr| cidr.parse().unwrap())
.collect(),
Vec::default(),
)
.unwrap();
@@ -365,15 +354,6 @@ mod tests {
}
fn allowed_from_vm_ipv4(proxy: &Proxy, src: Ipv4Address, dst: &str) -> Option<()> {
allowed_from_vm_ipv4_to_mac(proxy, src, dst, EthernetAddress([0x02, 0, 0, 0, 0, 0x03]))
}
fn allowed_from_vm_ipv4_to_mac(
proxy: &Proxy,
src: Ipv4Address,
dst: &str,
dst_mac: EthernetAddress,
) -> Option<()> {
let mut buf = vec![0; 1500];
let mut ipv4_pkt_mut = Ipv4Packet::new_unchecked(&mut buf[..]);
@@ -382,6 +362,6 @@ mod tests {
let ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_slice());
proxy.allowed_from_vm_ipv4(ipv4_pkt, dst_mac)
proxy.allowed_from_vm_ipv4(ipv4_pkt)
}
}
+15 -21
View File
@@ -4,7 +4,7 @@ use anyhow::Context;
use anyhow::Result;
use ipnet::Ipv4Net;
use smoltcp::wire::{
ArpPacket, EthernetAddress, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
};
use std::net::Ipv4Addr;
@@ -29,29 +29,21 @@ impl Proxy<'_> {
match frame.ethertype() {
EthernetProtocol::Arp => {
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
self.allowed_from_vm_arp(arp_pkt, frame.dst_addr())
self.allowed_from_vm_arp(arp_pkt)
}
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
self.allowed_from_vm_ipv4(ipv4_pkt, frame.dst_addr())
self.allowed_from_vm_ipv4(ipv4_pkt)
}
_ => None,
}
}
fn allowed_from_vm_arp(
&self,
arp_pkt: ArpPacket<&[u8]>,
dst_mac: EthernetAddress,
) -> Option<()> {
fn allowed_from_vm_arp(&self, arp_pkt: ArpPacket<&[u8]>) -> Option<()> {
if arp_pkt.source_hardware_addr() != self.vm_mac_address.0 {
return None;
}
if self.rules_mac.get(dst_mac.as_bytes()) == Some(&Action::Block) {
return None;
}
let source_protocol_addr: [u8; 4] = arp_pkt.source_protocol_addr().try_into().unwrap();
let source_protocol_addr = Ipv4Addr::from(source_protocol_addr);
@@ -66,21 +58,23 @@ impl Proxy<'_> {
None
}
pub(crate) fn allowed_from_vm_ipv4(
&self,
ipv4_pkt: Ipv4Packet<&[u8]>,
dst_mac: EthernetAddress,
) -> Option<()> {
pub(crate) fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
// Is this packet coming from VM's IP address that we've learned from DHCP snooping?
if let Some(lease) = &self.dhcp_snooper.lease()
&& lease.valid_ip_source(ipv4_pkt.src_addr())
{
let dst_addr = ipv4_pkt.dst_addr();
match self.rules_mac.get(dst_mac.as_bytes()) {
Some(Action::Block) => return None,
Some(Action::Allow) => return Some(()),
None => {}
if self.rules_mac.iter().any(|(mac, action)| {
*action == Action::Block && self.dhcp_snooper_global.valid_ip_for_mac(mac, dst_addr)
}) {
return None;
}
if self.rules_mac.iter().any(|(mac, action)| {
*action == Action::Allow && self.dhcp_snooper_global.valid_ip_for_mac(mac, dst_addr)
}) {
return Some(());
}
// Filter traffic based on user-specified rules first
-52
View File
@@ -194,14 +194,11 @@ fn try_main() -> anyhow::Result<()> {
// Set bootpd(8) min/max lease time while still having the root privileges
set_bootpd_lease_time(args.bootpd_lease_time);
let enable_isolation = bridge_isolation_enabled(&args.allow, &args.block);
// Initialize the proxy while still having the root privileges
let mut proxy = Proxy::new(
args.vm_fd as RawFd,
args.vm_mac_address,
args.vm_net_type,
enable_isolation,
args.allow,
args.block,
args.expose,
@@ -234,14 +231,6 @@ fn sudo_escalation_works() -> bool {
.unwrap_or(false)
}
fn bridge_isolation_enabled(allow: &[Target], block: &[Target]) -> bool {
let disables_isolation_for_compat = allow.iter().any(Target::is_ipv4_default_route);
let disables_isolation_for_mac_filtering =
allow.iter().chain(block.iter()).any(Target::is_mac_address);
!(disables_isolation_for_compat || disables_isolation_for_mac_filtering)
}
fn set_bootpd_lease_time(lease_time: u32) {
let prefs = SCPreferences::group(
&CFString::new("softnet"),
@@ -263,44 +252,3 @@ fn set_bootpd_lease_time(lease_time: u32) {
SCPreferencesCommitChanges(prefs.as_concrete_TypeRef());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn targets(values: &[&str]) -> Vec<Target> {
values.iter().map(|value| value.parse().unwrap()).collect()
}
#[test]
fn bridge_isolation_disabled_for_legacy_allow_default_route() {
assert!(!bridge_isolation_enabled(
&targets(&["0.0.0.0/0"]),
&targets(&[])
));
}
#[test]
fn bridge_isolation_disabled_for_legacy_allow_default_route_even_when_blocked() {
assert!(!bridge_isolation_enabled(
&targets(&["0.0.0.0/0"]),
&targets(&["0.0.0.0/0"])
));
}
#[test]
fn bridge_isolation_kept_for_block_default_route_only() {
assert!(bridge_isolation_enabled(
&targets(&[]),
&targets(&["0.0.0.0/0"])
));
}
#[test]
fn bridge_isolation_disabled_for_mac_filtering() {
assert!(!bridge_isolation_enabled(
&targets(&[]),
&targets(&["02:00:00:00:00:02"])
));
}
}