WIP: support peer MAC addresses in --allow/--block
To enable cross-VM communication.
This commit is contained in:
parent
df84a30016
commit
b5ad29648b
|
|
@ -1333,6 +1333,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"
|
||||
|
|
@ -1603,6 +1612,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"
|
||||
|
|
@ -1807,6 +1822,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"
|
||||
|
|
@ -2574,6 +2621,7 @@ dependencies = [
|
|||
"nix 0.31.2",
|
||||
"num_enum 0.7.6",
|
||||
"oslog",
|
||||
"pnet_datalink",
|
||||
"polling",
|
||||
"prefix-trie",
|
||||
"privdrop",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ oslog = "0.2.0"
|
|||
log = "0.4.29"
|
||||
serial_test = "3"
|
||||
coarsetime = "0.1.37"
|
||||
pnet_datalink = "0.35.0"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
|
|
|||
63
lib/host.rs
63
lib/host.rs
|
|
@ -1,6 +1,8 @@
|
|||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
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;
|
||||
|
|
@ -28,6 +30,7 @@ pub struct Host {
|
|||
new_packets_rx: UnixDatagram,
|
||||
callback_can_continue_tx: SyncSender<()>,
|
||||
pub gateway_ip: smoltcp::wire::Ipv4Address,
|
||||
pub gateway_mac: EthernetAddress,
|
||||
pub max_packet_size: u64,
|
||||
pub read_max_packets: u64,
|
||||
finalized: bool,
|
||||
|
|
@ -49,16 +52,43 @@ impl Host {
|
|||
.context("failed to initialize vmnet interface")?;
|
||||
|
||||
// Retrieve first IP (gateway) used for this interface
|
||||
let Some(Parameter::StartAddress(gateway_ip)) =
|
||||
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 last IP used for this interface and calculate the prefix
|
||||
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")?;
|
||||
|
||||
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
|
||||
));
|
||||
};
|
||||
|
||||
// Figure out the gateway's interface MAC address
|
||||
let Some(gateway_mac) = Self::interface_mac_for_ip(start_address, prefix) else {
|
||||
return Err(anyhow!(
|
||||
"failed to resolve vmnet's interface: no interface found with {}/{} CIDR",
|
||||
start_address,
|
||||
prefix
|
||||
));
|
||||
};
|
||||
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 +134,39 @@ impl Host {
|
|||
interface,
|
||||
new_packets_rx,
|
||||
callback_can_continue_tx,
|
||||
gateway_ip,
|
||||
gateway_ip: start_address,
|
||||
gateway_mac,
|
||||
max_packet_size,
|
||||
read_max_packets,
|
||||
finalized: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn interface_mac_for_ip(ip: Ipv4Addr, prefix: u8) -> Option<pnet_datalink::MacAddr> {
|
||||
for iface in pnet_datalink::interfaces() {
|
||||
if iface
|
||||
.ips
|
||||
.iter()
|
||||
.any(|network| network.ip() == IpAddr::V4(ip) && network.prefix() == prefix)
|
||||
&& let Some(mac) = iface.mac
|
||||
{
|
||||
return Some(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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use crate::proxy::Proxy;
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::{Action, Proxy};
|
||||
use anyhow::{Context, Result};
|
||||
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
|
||||
use ipnet::Ipv4Net;
|
||||
use smoltcp::wire::{ArpPacket, EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
impl Proxy<'_> {
|
||||
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
|
||||
|
|
@ -36,14 +38,42 @@ impl Proxy<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
pub(crate) fn allowed_from_host(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
if frame.src_addr() == self.host.gateway_mac {
|
||||
return allowed_host_ethertype(frame);
|
||||
}
|
||||
|
||||
match self.rules_mac.get(frame.src_addr().as_bytes()) {
|
||||
Some(Action::Block) => return None,
|
||||
Some(Action::Allow) => return allowed_host_ethertype(frame),
|
||||
None => {}
|
||||
}
|
||||
|
||||
match frame.ethertype() {
|
||||
EthernetProtocol::Arp => Some(()),
|
||||
EthernetProtocol::Ipv4 => Some(()),
|
||||
EthernetProtocol::Arp => {
|
||||
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
|
||||
let source_protocol_addr: [u8; 4] =
|
||||
arp_pkt.source_protocol_addr().try_into().ok()?;
|
||||
|
||||
self.allowed_peer_ip_from_host(Ipv4Addr::from(source_protocol_addr))
|
||||
}
|
||||
EthernetProtocol::Ipv4 => {
|
||||
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
|
||||
self.allowed_peer_ip_from_host(ipv4_pkt.src_addr())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_peer_ip_from_host(&self, peer_ip: Ipv4Addr) -> Option<()> {
|
||||
let peer_net = Ipv4Net::from(peer_ip);
|
||||
|
||||
match self.rules.get_lpm(&peer_net).map(|(_, action)| action) {
|
||||
Some(Action::Allow) => Some(()),
|
||||
Some(Action::Block) | None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
|
||||
if frame.ethertype() != EthernetProtocol::Ipv4 {
|
||||
return;
|
||||
|
|
@ -74,3 +104,10 @@ impl Proxy<'_> {
|
|||
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_host_ethertype(frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
match frame.ethertype() {
|
||||
EthernetProtocol::Arp | EthernetProtocol::Ipv4 => Some(()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,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;
|
||||
|
|
@ -29,6 +30,7 @@ pub struct Proxy<'proxy> {
|
|||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
rules: PrefixMap<Ipv4Net, Action>,
|
||||
rules_mac: HashMap<[u8; 6], Action>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
|
@ -36,9 +38,20 @@ pub struct Proxy<'proxy> {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
Prefix(Ipv4Net),
|
||||
MacAddress(MacAddress),
|
||||
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;
|
||||
|
||||
|
|
@ -47,6 +60,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,10 +84,9 @@ impl Proxy<'_> {
|
|||
exposed_ports: Vec<ExposedPort>,
|
||||
) -> Result<Proxy<'proxy>> {
|
||||
let vm = VM::new(vm_fd)?;
|
||||
let host = Host::new(
|
||||
vm_net_type,
|
||||
!allow.contains(&Target::Prefix(Ipv4Net::zero())),
|
||||
)?;
|
||||
let enable_isolation = !allow.iter().any(Target::is_ipv4_default_route)
|
||||
&& !allow.iter().chain(block.iter()).any(Target::is_mac_address);
|
||||
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)?;
|
||||
|
||||
|
|
@ -79,11 +95,17 @@ impl Proxy<'_> {
|
|||
// 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 +115,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);
|
||||
|
|
@ -105,6 +132,7 @@ impl Proxy<'_> {
|
|||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: DhcpSnooper::new(poller_timeout),
|
||||
rules,
|
||||
rules_mac,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
|
|
@ -316,6 +344,6 @@ mod tests {
|
|||
|
||||
let ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_slice());
|
||||
|
||||
proxy.allowed_from_vm_ipv4(ipv4_pkt)
|
||||
proxy.allowed_from_vm_ipv4(ipv4_pkt, false)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,17 @@ impl Proxy<'_> {
|
|||
.context("failed to write to the host")
|
||||
}
|
||||
|
||||
fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
pub(crate) fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
if frame.src_addr() != self.vm_mac_address {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dst_mac_allowed = match self.rules_mac.get(frame.dst_addr().as_bytes()) {
|
||||
Some(Action::Block) => return None,
|
||||
Some(Action::Allow) => true,
|
||||
None => false,
|
||||
};
|
||||
|
||||
match frame.ethertype() {
|
||||
EthernetProtocol::Arp => {
|
||||
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
|
||||
|
|
@ -33,7 +39,7 @@ impl Proxy<'_> {
|
|||
}
|
||||
EthernetProtocol::Ipv4 => {
|
||||
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
|
||||
self.allowed_from_vm_ipv4(ipv4_pkt)
|
||||
self.allowed_from_vm_ipv4(ipv4_pkt, dst_mac_allowed)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -58,13 +64,21 @@ impl Proxy<'_> {
|
|||
None
|
||||
}
|
||||
|
||||
pub(crate) fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
|
||||
pub(crate) fn allowed_from_vm_ipv4(
|
||||
&self,
|
||||
ipv4_pkt: Ipv4Packet<&[u8]>,
|
||||
dst_mac_allowed: bool,
|
||||
) -> 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();
|
||||
|
||||
if dst_mac_allowed {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Filter traffic based on user-specified rules first
|
||||
if !self.rules.is_empty() {
|
||||
let dst_net = Ipv4Net::from(dst_addr);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue