diff --git a/Cargo.lock b/Cargo.lock index e082d89..db2d190 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -1333,6 +1354,15 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "iri-string" version = "0.7.10" @@ -1438,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" @@ -1603,6 +1643,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "num-conv" version = "0.2.0" @@ -1774,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" @@ -1807,6 +1868,38 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "polling" version = "3.11.0" @@ -1818,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]] @@ -2111,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", @@ -2124,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", @@ -2568,12 +2661,15 @@ 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", "privdrop", @@ -3322,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]] @@ -3356,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" @@ -3385,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", ] @@ -3437,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" @@ -3449,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" @@ -3473,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" @@ -3485,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" @@ -3509,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" diff --git a/Cargo.toml b/Cargo.toml index 00d701b..2e11392 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ oslog = "0.2.0" 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 diff --git a/lib/dhcp_snooper_global.rs b/lib/dhcp_snooper_global.rs new file mode 100644 index 0000000..e86f89c --- /dev/null +++ b/lib/dhcp_snooper_global.rs @@ -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_fd: Option, + leases_by_mac: HashMap<[u8; 6], Lease>, + uncertainty_duration: Duration, + last_table_print: Option, +} + +impl DhcpSnooperGlobal { + pub fn new(gateway_interface_name: &str, uncertainty_duration: Duration) -> Result { + // 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 { + 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: "); + 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::>() + .join(":") + } +} diff --git a/lib/host.rs b/lib/host.rs index 7b2ee66..614205e 100644 --- a/lib/host.rs +++ b/lib/host.rs @@ -1,6 +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::Ipv4Addr; use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::net::UnixDatagram; @@ -27,7 +30,9 @@ 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, pub read_max_packets: u64, finalized: bool, @@ -48,17 +53,38 @@ impl Host { ) .context("failed to initialize vmnet interface")?; - // Retrieve first IP (gateway) used for this interface - let Some(Parameter::StartAddress(gateway_ip)) = + // Retrieve the first IP (gateway) used for this interface + let Some(Parameter::StartAddress(start_address)) = interface.parameters().get(ParameterKind::StartAddress) else { return Err(anyhow!( "failed to retrieve vmnet's interface start address" )); }; - let gateway_ip = Ipv4Addr::from_str(&gateway_ip) + let start_address = Ipv4Addr::from_str(&start_address) .context("failed to parse vmnet's interface start address")?; + // Retrieve the last IP used for this interface + let Some(Parameter::EndAddress(end_address)) = + interface.parameters().get(ParameterKind::EndAddress) + else { + return Err(anyhow!("failed to retrieve vmnet's interface end address")); + }; + let end_address = Ipv4Addr::from_str(&end_address) + .context("failed to parse vmnet's interface end address")?; + + // Determine the prefix used for this interface + let prefix = Self::ipv4_range_prefix(start_address, end_address); + + let Some((gateway_name, gateway_mac)) = Self::interface_ip_to_mac(start_address, prefix) + else { + return Err(anyhow!( + "failed to resolve vmnet's interface from start address {}", + start_address + )); + }; + let gateway_mac = EthernetAddress(gateway_mac.octets()); + // Retrieve max packet size for this interface let Some(Parameter::MaxPacketSize(max_packet_size)) = interface.parameters().get(ParameterKind::MaxPacketSize) @@ -104,12 +130,32 @@ impl Host { interface, new_packets_rx, callback_can_continue_tx, - gateway_ip, + gateway_interface_name: gateway_name, + gateway_ip: start_address, + gateway_mac, max_packet_size, read_max_packets, finalized: false, }) } + + 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.contains(&ip_network) + && let Some(mac) = iface.mac + { + return Some((iface.name, mac)); + } + } + + None + } + + fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> u8 { + (u32::from(start_address) ^ u32::from(end_address)).leading_zeros() as u8 + } } impl Host { @@ -198,3 +244,28 @@ impl AsRawFd for Host { self.new_packets_rx.as_raw_fd() } } + +#[cfg(test)] +mod tests { + use super::Host; + use std::net::Ipv4Addr; + + #[test] + fn ipv4_range_prefix_for_vmnet_class_c_range() { + assert_eq!( + Host::ipv4_range_prefix( + Ipv4Addr::new(192, 168, 64, 1), + Ipv4Addr::new(192, 168, 64, 254), + ), + 24 + ); + } + + #[test] + 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)), + 32 + ); + } +} diff --git a/lib/mod.rs b/lib/mod.rs index 838640f..233ba1b 100644 --- a/lib/mod.rs +++ b/lib/mod.rs @@ -1,4 +1,5 @@ mod dhcp_snooper; +mod dhcp_snooper_global; mod host; pub use host::NetType; mod poller; diff --git a/lib/poller.rs b/lib/poller.rs index 27e38f4..38d25a1 100644 --- a/lib/poller.rs +++ b/lib/poller.rs @@ -12,6 +12,7 @@ pub struct Poller<'poller> { timeout: Duration, vm_fd: BorrowedFd<'poller>, host_fd: BorrowedFd<'poller>, + pcap_fd: Option>, } #[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, timeout: Duration, ) -> Result> { 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 { 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::::into(EventKey::Host)); + let pcap_readable = self + .events + .iter() + .any(|ev| ev.key == Into::::into(EventKey::Pcap)); let interrupt = self .events .iter() .any(|ev| ev.key == Into::::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()) + } } diff --git a/lib/proxy/host.rs b/lib/proxy/host.rs index efcc2bd..bdc8aac 100644 --- a/lib/proxy/host.rs +++ b/lib/proxy/host.rs @@ -37,6 +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::>() + // .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(()), @@ -45,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; } diff --git a/lib/proxy/mod.rs b/lib/proxy/mod.rs index e32b338..e75050e 100644 --- a/lib/proxy/mod.rs +++ b/lib/proxy/mod.rs @@ -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; @@ -16,6 +17,7 @@ use mac_address::MacAddress; use port_forwarder::PortForwarder; use prefix_trie::{Prefix, PrefixMap}; use smoltcp::wire::EthernetFrame; +use std::collections::HashMap; use std::io::ErrorKind; use std::os::unix::io::{AsRawFd, RawFd}; use std::str::FromStr; @@ -28,7 +30,9 @@ pub struct Proxy<'proxy> { poller: Poller<'proxy>, vm_mac_address: smoltcp::wire::EthernetAddress, dhcp_snooper: DhcpSnooper, + dhcp_snooper_global: DhcpSnooperGlobal, rules: PrefixMap, + rules_mac: HashMap<[u8; 6], Action>, enobufs_encountered: bool, port_forwarder: PortForwarder, } @@ -36,6 +40,7 @@ pub struct Proxy<'proxy> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Target { Prefix(Ipv4Net), + MacAddress(MacAddress), Host, } @@ -47,6 +52,10 @@ impl FromStr for Target { return Ok(Target::Host); } + if let Ok(mac_address) = MacAddress::from_str(s) { + return Ok(Target::MacAddress(mac_address)); + } + Ipv4Net::from_str(s).map(Target::Prefix) } } @@ -66,24 +75,44 @@ impl Proxy<'_> { block: Vec, exposed_ports: Vec, ) -> Result> { + 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, - !allow.contains(&Target::Prefix(Ipv4Net::zero())), - )?; + 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 // // SECURITY: blocking rules must always take precedence // over allowing rules when prefixes are identical. let mut rules = PrefixMap::new(); + let mut rules_mac = HashMap::new(); for allow_target in allow { let allow_prefix = match allow_target { Target::Prefix(prefix) => prefix, Target::Host => host.gateway_ip.into(), + Target::MacAddress(mac_address) => { + rules_mac.insert(mac_address.bytes(), Action::Allow); + + continue; + } }; rules.insert(allow_prefix, Action::Allow); @@ -93,6 +122,11 @@ impl Proxy<'_> { let block_prefix = match block_target { Target::Prefix(prefix) => prefix, Target::Host => host.gateway_ip.into(), + Target::MacAddress(mac_address) => { + rules_mac.insert(mac_address.bytes(), Action::Block); + + continue; + } }; rules.insert(block_prefix, Action::Block); @@ -104,7 +138,9 @@ 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, port_forwarder: PortForwarder::new(exposed_ports), }) @@ -124,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()); } diff --git a/lib/proxy/vm.rs b/lib/proxy/vm.rs index d6d0e86..f1042ad 100644 --- a/lib/proxy/vm.rs +++ b/lib/proxy/vm.rs @@ -65,6 +65,18 @@ impl Proxy<'_> { { let dst_addr = ipv4_pkt.dst_addr(); + 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 if !self.rules.is_empty() { let dst_net = Ipv4Net::from(dst_addr); diff --git a/src/main.rs b/src/main.rs index 3dd9ba8..c4011be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ struct Args { In case an identical prefix is both --allow'ed and --block'ed, \ blocking will take precedence. --allow=0.0.0.0/0 is a special case, \ it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).", - value_name = "comma-separated CIDRs or @-alias", + value_name = "comma-separated CIDRs, MAC addresses or @-aliases", use_value_delimiter = true, action = clap::ArgAction::Set )] @@ -74,7 +74,7 @@ struct Args { When used with --allow, the longest prefix match always wins. \ In case an identical prefix is both --allow'ed and --block'ed, \ blocking will take precedence.", - value_name = "comma-separated CIDRs or @-alias", + value_name = "comma-separated CIDRs, MAC addresses or @-aliases", use_value_delimiter = true, action = clap::ArgAction::Set )]