mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-01 04:21:54 +02:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
431a2384a4 | ||
|
|
df84a30016 | ||
|
|
bbff9996ab | ||
|
|
dd55a20de0 | ||
|
|
012a93834a | ||
|
|
d806af1f9d | ||
|
|
d992da7023 | ||
|
|
2e74102ee4 | ||
|
|
35ae0608db | ||
|
|
54f40f914b | ||
|
|
088011e714 | ||
|
|
3cd8732f1c | ||
|
|
ca15d50f8d | ||
|
|
928dc3c333 | ||
|
|
62d9326d26 | ||
|
|
173f7832b3 | ||
|
|
be2c706b00 | ||
|
|
b1f5678f18 | ||
|
|
c1ddb2afc6 | ||
|
|
401dea6612 | ||
|
|
1dcb0755df | ||
|
|
1706062004 |
Generated
+987
-301
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -16,7 +16,7 @@ smoltcp = "0"
|
||||
libc = "0"
|
||||
polling = "3"
|
||||
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
|
||||
vmnet = "0.5.0"
|
||||
vmnet = "0.5.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
mac_address = "1"
|
||||
privdrop = "0"
|
||||
@@ -31,8 +31,12 @@ nix = { version = "0", features = ["signal", "socket"] }
|
||||
prefix-trie = "0"
|
||||
ipnet = "2"
|
||||
oslog = "0.2.0"
|
||||
log = "0.4.28"
|
||||
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
|
||||
|
||||
+18
-9
@@ -2,14 +2,22 @@ use dhcproto::Decodable;
|
||||
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
|
||||
use smoltcp::wire::Ipv4Address;
|
||||
use std::collections::HashSet;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DhcpSnooper {
|
||||
vm_lease: Option<Lease>,
|
||||
uncertainty_duration: Duration,
|
||||
}
|
||||
|
||||
impl DhcpSnooper {
|
||||
pub fn new(uncertainty_duration: Duration) -> Self {
|
||||
DhcpSnooper {
|
||||
uncertainty_duration,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_dhcp_reply(&mut self, dhcp_packet: &[u8]) {
|
||||
let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet);
|
||||
|
||||
@@ -32,11 +40,12 @@ impl DhcpSnooper {
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
|
||||
self.vm_lease = Some(Lease::new(
|
||||
message.yiaddr(),
|
||||
Duration::from_secs(*lease_time as u64),
|
||||
dns_ips,
|
||||
))
|
||||
let mut lease_duration = Duration::from_secs(*lease_time as u64);
|
||||
|
||||
// Adjust for uncertainty caused by using a coarse clock
|
||||
lease_duration = lease_duration.saturating_sub(self.uncertainty_duration);
|
||||
|
||||
self.vm_lease = Some(Lease::new(message.yiaddr(), lease_duration, dns_ips))
|
||||
}
|
||||
Some(MessageType::Nak) => {
|
||||
self.vm_lease = None;
|
||||
@@ -66,7 +75,7 @@ impl DhcpSnooper {
|
||||
#[derive(Debug)]
|
||||
pub struct Lease {
|
||||
address: Ipv4Address,
|
||||
valid_until: Instant,
|
||||
valid_until: coarsetime::Instant,
|
||||
dns_ips: HashSet<Ipv4Address>,
|
||||
}
|
||||
|
||||
@@ -74,7 +83,7 @@ impl Lease {
|
||||
pub fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
|
||||
Lease {
|
||||
address,
|
||||
valid_until: Instant::now() + lease_time,
|
||||
valid_until: coarsetime::Instant::recent() + lease_time.into(),
|
||||
dns_ips,
|
||||
}
|
||||
}
|
||||
@@ -84,7 +93,7 @@ impl Lease {
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
Instant::now() < self.valid_until
|
||||
coarsetime::Instant::recent() < self.valid_until
|
||||
}
|
||||
|
||||
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
|
||||
|
||||
@@ -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(":")
|
||||
}
|
||||
}
|
||||
+75
-4
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod dhcp_snooper;
|
||||
mod dhcp_snooper_global;
|
||||
mod host;
|
||||
pub use host::NetType;
|
||||
mod poller;
|
||||
|
||||
+41
-5
@@ -9,8 +9,10 @@ use std::time::Duration;
|
||||
pub struct Poller<'poller> {
|
||||
poller: polling::Poller,
|
||||
events: polling::Events,
|
||||
timeout: Duration,
|
||||
vm_fd: BorrowedFd<'poller>,
|
||||
host_fd: BorrowedFd<'poller>,
|
||||
pcap_fd: Option<BorrowedFd<'poller>>,
|
||||
}
|
||||
|
||||
#[derive(IntoPrimitive)]
|
||||
@@ -18,18 +20,33 @@ 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) -> Result<Poller<'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()?;
|
||||
|
||||
Ok(Poller {
|
||||
poller,
|
||||
events: polling::Events::new(),
|
||||
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) }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,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);
|
||||
@@ -58,9 +82,8 @@ impl Poller<'_> {
|
||||
self.events.clear();
|
||||
}
|
||||
|
||||
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
|
||||
self.poller
|
||||
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
|
||||
pub fn wait(&mut self) -> Result<Readiness> {
|
||||
self.poller.wait(&mut self.events, Some(self.timeout))?;
|
||||
|
||||
let vm_readable = self
|
||||
.events
|
||||
@@ -70,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 {
|
||||
@@ -85,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<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(()),
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+134
-26
@@ -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;
|
||||
@@ -14,10 +15,13 @@ pub use exposed_port::ExposedPort;
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use port_forwarder::PortForwarder;
|
||||
use prefix_trie::{Prefix, PrefixMap, PrefixSet};
|
||||
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;
|
||||
use std::time::Duration;
|
||||
use vmnet::Batch;
|
||||
|
||||
pub struct Proxy<'proxy> {
|
||||
@@ -26,11 +30,36 @@ 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,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
Prefix(Ipv4Net),
|
||||
MacAddress(MacAddress),
|
||||
Host,
|
||||
}
|
||||
|
||||
impl FromStr for Target {
|
||||
type Err = ipnet::AddrParseError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
if s == "@host" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Action {
|
||||
Block,
|
||||
@@ -42,26 +71,65 @@ impl Proxy<'_> {
|
||||
vm_fd: RawFd,
|
||||
vm_mac_address: MacAddress,
|
||||
vm_net_type: NetType,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
block: PrefixSet<Ipv4Net>,
|
||||
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, !allow.contains(&Ipv4Net::zero()))?;
|
||||
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
|
||||
let host = Host::new(vm_net_type, enable_isolation)?;
|
||||
let poller_timeout = Duration::from_millis(100);
|
||||
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_net in allow {
|
||||
rules.insert(allow_net, Action::Allow);
|
||||
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);
|
||||
}
|
||||
|
||||
for block_net in block {
|
||||
rules.insert(block_net, Action::Block);
|
||||
for block_target in block {
|
||||
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);
|
||||
}
|
||||
|
||||
Ok(Proxy {
|
||||
@@ -69,8 +137,10 @@ impl Proxy<'_> {
|
||||
host,
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: Default::default(),
|
||||
dhcp_snooper: DhcpSnooper::new(poller_timeout),
|
||||
dhcp_snooper_global,
|
||||
rules,
|
||||
rules_mac,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
@@ -90,23 +160,36 @@ impl Proxy<'_> {
|
||||
self.poller.arm()?;
|
||||
|
||||
loop {
|
||||
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
|
||||
let readiness = self.poller.wait()?;
|
||||
|
||||
if vm_readable {
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -119,6 +202,9 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
match self.vm.read(buf) {
|
||||
Ok(n) => {
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
|
||||
self.process_frame_from_vm(frame)?;
|
||||
}
|
||||
@@ -138,6 +224,9 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
match self.host.read(batch, bufs) {
|
||||
Ok(pktcnt) => {
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
|
||||
if let Ok(pkt) = EthernetFrame::new_checked(buf) {
|
||||
self.process_frame_from_host(&pkt)?;
|
||||
@@ -164,7 +253,7 @@ mod tests {
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
|
||||
use prefix_trie::{PrefixMap, PrefixSet};
|
||||
use prefix_trie::PrefixMap;
|
||||
use serial_test::serial;
|
||||
use smoltcp::wire::{Ipv4Address, Ipv4Packet};
|
||||
use std::collections::HashSet;
|
||||
@@ -208,6 +297,27 @@ mod tests {
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.34").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allow_host() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
|
||||
|
||||
assert_eq!(
|
||||
proxy.rules,
|
||||
PrefixMap::from_iter(vec![
|
||||
(proxy.host.gateway_ip.into(), Action::Allow),
|
||||
(Ipv4Net::from_str("0.0.0.0/0").unwrap(), Action::Block),
|
||||
])
|
||||
);
|
||||
|
||||
// Access to global IPs should be disallowed because of --block=0.0.0.0/0
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "8.8.8.8").is_none());
|
||||
|
||||
// Despite the above, access to host IP address should be possible because of --allow=@host
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, &proxy.host.gateway_ip.to_string()).is_some());
|
||||
}
|
||||
|
||||
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
|
||||
let (vm_fd, _) = socketpair(
|
||||
AddressFamily::Unix,
|
||||
@@ -222,16 +332,14 @@ mod tests {
|
||||
vm_fd.as_raw_fd(),
|
||||
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
|
||||
NetType::Nat,
|
||||
PrefixSet::from_iter(
|
||||
allow
|
||||
.into_iter()
|
||||
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
|
||||
),
|
||||
PrefixSet::from_iter(
|
||||
block
|
||||
.into_iter()
|
||||
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
|
||||
),
|
||||
allow
|
||||
.into_iter()
|
||||
.map(|cidr| cidr.parse().unwrap())
|
||||
.collect(),
|
||||
block
|
||||
.into_iter()
|
||||
.map(|cidr| cidr.parse().unwrap())
|
||||
.collect(),
|
||||
Vec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -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);
|
||||
|
||||
+15
-12
@@ -1,14 +1,13 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use clap::Parser;
|
||||
use ipnet::Ipv4Net;
|
||||
use log::LevelFilter;
|
||||
use nix::sys::signal::{SigHandler, Signal, signal};
|
||||
use oslog::OsLogger;
|
||||
use prefix_trie::PrefixSet;
|
||||
use privdrop::PrivDrop;
|
||||
use softnet::NetType;
|
||||
use softnet::proxy::ExposedPort;
|
||||
use softnet::proxy::Proxy;
|
||||
use softnet::proxy::Target;
|
||||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::os::raw::c_int;
|
||||
@@ -53,29 +52,33 @@ struct Args {
|
||||
#[clap(
|
||||
long,
|
||||
help = "Comma-separated list of CIDRs to allow the traffic to \
|
||||
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM). \
|
||||
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM), \
|
||||
plus supported @-aliases. Currently the only supported @-alias is @host, \
|
||||
which matches the vmnet bridge gateway IP. \
|
||||
When used with --block, the longest prefix match always wins. \
|
||||
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",
|
||||
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
allow: Vec<Ipv4Net>,
|
||||
allow: Vec<Target>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Comma-separated list of CIDRs to block the traffic to \
|
||||
(e.g. --block=0.0.0.0/0 may be used to establish a default deny policy \
|
||||
that is further relaxed with --allow). When used with --allow, \
|
||||
the longest prefix match always wins. In case the same prefix is both \
|
||||
--allow'ed and --block'ed, blocking takes precedence.",
|
||||
value_name = "comma-separated CIDRs",
|
||||
that is further relaxed with --allow), plus supported @-aliases. \
|
||||
Currently the only supported @-alias is @host, which matches the vmnet bridge gateway IP. \
|
||||
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, MAC addresses or @-aliases",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
block: Vec<Ipv4Net>,
|
||||
block: Vec<Target>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
@@ -196,8 +199,8 @@ fn try_main() -> anyhow::Result<()> {
|
||||
args.vm_fd as RawFd,
|
||||
args.vm_mac_address,
|
||||
args.vm_net_type,
|
||||
PrefixSet::from_iter(args.allow),
|
||||
PrefixSet::from_iter(args.block),
|
||||
args.allow,
|
||||
args.block,
|
||||
args.expose,
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
|
||||
Reference in New Issue
Block a user