mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-21 10:45:23 +02:00
Serve DHCP directly from Softnet and use predictable vmnet reservations (#209)
* Serve DHCP directly from Softnet and use predictable vmnet reservations * $ cargo fmt
This commit is contained in:
@@ -152,10 +152,6 @@ impl FlowTable {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.flows.clear();
|
||||
}
|
||||
|
||||
fn sweep_if_due(&mut self, now: Instant) {
|
||||
if now < self.next_sweep {
|
||||
return;
|
||||
|
||||
+16
-140
@@ -1,19 +1,9 @@
|
||||
use crate::dhcp_snooper::message_matches_bootp_client;
|
||||
use crate::proxy::flows::{FlowDirection, FlowMatch};
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::{Direction, PolicyDecision, Proxy};
|
||||
use anyhow::{Context, Result};
|
||||
use dhcproto::Decodable;
|
||||
use dhcproto::v4::Opcode;
|
||||
use dhcproto::v4::{CLIENT_PORT, SERVER_PORT};
|
||||
use smoltcp::phy::ChecksumCapabilities;
|
||||
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, UdpPacket};
|
||||
|
||||
/// DhcpResponseDisposition distinguishes non-DHCP traffic from allowed and rejected DHCP replies.
|
||||
enum DhcpResponseDisposition {
|
||||
NotDhcp,
|
||||
Allow,
|
||||
Reject,
|
||||
}
|
||||
use smoltcp::wire::{EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, Ipv4Repr, UdpPacket};
|
||||
|
||||
impl Proxy<'_> {
|
||||
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
|
||||
@@ -22,13 +12,11 @@ impl Proxy<'_> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Snoop bootpd(8) replies from the host to
|
||||
// figure out the IP assigned to the VM
|
||||
if frame.dst_addr() == self.vm_mac_address || frame.dst_addr().is_broadcast() {
|
||||
self.snoop(frame);
|
||||
}
|
||||
self.write_to_vm(frame.as_ref())
|
||||
}
|
||||
|
||||
match self.vm.write(frame.as_ref()) {
|
||||
pub(super) fn write_to_vm(&mut self, packet: &[u8]) -> Result<()> {
|
||||
match self.vm.write(packet) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if let Some(libc::ENOBUFS) = err.raw_os_error() {
|
||||
@@ -62,10 +50,15 @@ impl Proxy<'_> {
|
||||
}
|
||||
|
||||
pub(super) fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> {
|
||||
match self.dhcp_response_disposition(ipv4_pkt) {
|
||||
DhcpResponseDisposition::NotDhcp => { /* Fall through to generic policy */ }
|
||||
DhcpResponseDisposition::Allow => return Some(()),
|
||||
DhcpResponseDisposition::Reject => return None,
|
||||
// Drop external DHCP replies, including malformed and fragmented replies
|
||||
if ipv4_pkt.next_header() == IpProtocol::Udp
|
||||
&& ipv4_pkt.frag_offset() == 0
|
||||
&& ipv4_pkt.payload().len() >= 4
|
||||
{
|
||||
let udp = UdpPacket::new_unchecked(ipv4_pkt.payload());
|
||||
if udp.src_port() == SERVER_PORT && udp.dst_port() == CLIENT_PORT {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility with Softnet consumers that only use stateless rules
|
||||
@@ -75,12 +68,7 @@ impl Proxy<'_> {
|
||||
|
||||
// Consult the flow table before evaluating inbound policy
|
||||
// so established flows are not treated as new traffic
|
||||
let pending = if self
|
||||
.dhcp_snooper
|
||||
.lease()
|
||||
.as_ref()
|
||||
.is_some_and(|lease| lease.is_valid_for(ipv4_pkt.dst_addr()))
|
||||
{
|
||||
let pending = if self.host.vm_ip == ipv4_pkt.dst_addr() {
|
||||
match self
|
||||
.flows
|
||||
.as_mut()?
|
||||
@@ -116,116 +104,4 @@ impl Proxy<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
|
||||
if frame.ethertype() != EthernetProtocol::Ipv4 {
|
||||
return;
|
||||
}
|
||||
|
||||
let ipv4_pkt = match Ipv4Packet::new_checked(frame.payload()) {
|
||||
Ok(ipv4_pkt) => ipv4_pkt,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match self.dhcp_response_disposition(&ipv4_pkt) {
|
||||
DhcpResponseDisposition::Allow => { /* Continue snooping */ }
|
||||
DhcpResponseDisposition::NotDhcp | DhcpResponseDisposition::Reject => return,
|
||||
}
|
||||
|
||||
let udp_pkt = match UdpPacket::new_checked(ipv4_pkt.payload()) {
|
||||
Ok(udp_pkt) => udp_pkt,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let address_and_dns_ips_saved = self.dhcp_snooper.address_and_dns_ips();
|
||||
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
|
||||
if address_and_dns_ips_saved != self.dhcp_snooper.address_and_dns_ips()
|
||||
&& let Some(flows) = &mut self.flows
|
||||
{
|
||||
flows.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn dhcp_response_disposition(&self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> DhcpResponseDisposition {
|
||||
if ipv4_pkt.src_addr() != self.host.gateway_ip
|
||||
|| ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp
|
||||
{
|
||||
return DhcpResponseDisposition::NotDhcp;
|
||||
}
|
||||
|
||||
let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else {
|
||||
return DhcpResponseDisposition::NotDhcp;
|
||||
};
|
||||
|
||||
// Require the standard DHCP server and client ports
|
||||
if !udp_pkt.is_dhcp_response() {
|
||||
return DhcpResponseDisposition::NotDhcp;
|
||||
}
|
||||
|
||||
// Require the BOOTP client hardware address to match this VM
|
||||
// (symmetric with is_allowed_dhcp_request / #191 on the VM→host path)
|
||||
let mut decoder = dhcproto::v4::Decoder::new(udp_pkt.payload());
|
||||
let Ok(message) = dhcproto::v4::Message::decode(&mut decoder) else {
|
||||
return DhcpResponseDisposition::Reject;
|
||||
};
|
||||
|
||||
if message_matches_bootp_client(&message, Opcode::BootReply, self.vm_mac_address.0) {
|
||||
DhcpResponseDisposition::Allow
|
||||
} else {
|
||||
DhcpResponseDisposition::Reject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::dhcp_snooper::message_matches_bootp_client;
|
||||
use dhcproto::Decodable;
|
||||
use dhcproto::v4::{DhcpOption, Message, MessageType, Opcode};
|
||||
use dhcproto::{Encodable, Encoder};
|
||||
use smoltcp::wire::Ipv4Address;
|
||||
|
||||
const VM_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
|
||||
const OTHER_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
|
||||
|
||||
#[test]
|
||||
fn dhcp_boot_reply_chaddr_must_match_vm() {
|
||||
let own = encode_boot_reply(VM_MAC);
|
||||
let foreign = encode_boot_reply(OTHER_MAC);
|
||||
|
||||
let mut dec = dhcproto::v4::Decoder::new(&own);
|
||||
let own_msg = Message::decode(&mut dec).unwrap();
|
||||
let mut dec = dhcproto::v4::Decoder::new(&foreign);
|
||||
let foreign_msg = Message::decode(&mut dec).unwrap();
|
||||
|
||||
assert!(message_matches_bootp_client(
|
||||
&own_msg,
|
||||
Opcode::BootReply,
|
||||
VM_MAC
|
||||
));
|
||||
assert!(!message_matches_bootp_client(
|
||||
&foreign_msg,
|
||||
Opcode::BootReply,
|
||||
VM_MAC
|
||||
));
|
||||
}
|
||||
|
||||
fn encode_boot_reply(chaddr: [u8; 6]) -> Vec<u8> {
|
||||
let mut message = Message::new(
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
Ipv4Address::new(192, 168, 64, 2),
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
&chaddr,
|
||||
);
|
||||
message.set_opcode(Opcode::BootReply);
|
||||
message
|
||||
.opts_mut()
|
||||
.insert(DhcpOption::MessageType(MessageType::Ack));
|
||||
message.opts_mut().insert(DhcpOption::AddressLeaseTime(600));
|
||||
|
||||
let mut encoded = Vec::new();
|
||||
message.encode(&mut Encoder::new(&mut encoded)).unwrap();
|
||||
encoded
|
||||
}
|
||||
}
|
||||
|
||||
+49
-46
@@ -2,27 +2,24 @@ mod control;
|
||||
mod exposed_port;
|
||||
mod flows;
|
||||
mod host;
|
||||
mod port_forwarder;
|
||||
mod rule;
|
||||
mod rules;
|
||||
mod udp_packet_helper;
|
||||
mod vm;
|
||||
|
||||
use crate::dhcp_snooper::DhcpSnooper;
|
||||
use crate::dhcp_server::{DHCP_SERVER_MAC, DhcpServer};
|
||||
use crate::host::Host;
|
||||
use crate::host::NetType;
|
||||
use crate::poller::Poller;
|
||||
use crate::vm::VM;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use control::{Control, normalize_rules};
|
||||
pub use exposed_port::ExposedPort;
|
||||
use flows::{FlowTable, PendingFlow};
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use port_forwarder::PortForwarder;
|
||||
pub use rule::{Direction, Rule, Target};
|
||||
pub(crate) use rules::{PolicyDecision, Rules};
|
||||
use smoltcp::wire::{EthernetFrame, Ipv4Address};
|
||||
use smoltcp::wire::{EthernetAddress, EthernetFrame, Ipv4Address};
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::time::Duration;
|
||||
@@ -33,12 +30,11 @@ pub struct Proxy<'proxy> {
|
||||
host: Host,
|
||||
poller: Poller<'proxy>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
dhcp_server: DhcpServer,
|
||||
rules: Rules,
|
||||
control: Option<Control>,
|
||||
flows: Option<FlowTable>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
impl Proxy<'_> {
|
||||
@@ -51,12 +47,19 @@ impl Proxy<'_> {
|
||||
exposed_ports: Vec<ExposedPort>,
|
||||
control_fd: Option<RawFd>,
|
||||
) -> Result<Proxy<'proxy>> {
|
||||
// Ensure that VM's MAC address won't conflict with our DHCP server's MAC address
|
||||
let vm_mac_address = EthernetAddress(vm_mac_address.bytes());
|
||||
if vm_mac_address == DHCP_SERVER_MAC {
|
||||
bail!("VM MAC address {vm_mac_address} is reserved for the DHCP server");
|
||||
}
|
||||
|
||||
let allow = normalize_rules(allow);
|
||||
let block = normalize_rules(block);
|
||||
|
||||
let vm = VM::new(vm_fd)?;
|
||||
let host = Host::new(
|
||||
let mut host = Host::new(
|
||||
vm_net_type,
|
||||
vm_mac_address,
|
||||
!allow.contains(&Rule::Stateless(Target::Prefix(Ipv4Net::default()))),
|
||||
)?;
|
||||
let poller_timeout = Duration::from_millis(100);
|
||||
@@ -75,20 +78,34 @@ impl Proxy<'_> {
|
||||
let rules = Rules::new(host.gateway_ip, &allow, &block);
|
||||
|
||||
// Any stateful rule enables flow inspection for the whole VM, including
|
||||
// traffic admitted through implicit global, gateway, and DNS fallbacks
|
||||
// traffic admitted through implicit global and gateway fallbacks
|
||||
let flows = rules.has_stateful().then(FlowTable::new);
|
||||
|
||||
// vmnet owns the gateway (including ARP and DNS); the VM address is
|
||||
// reserved for this MAC for the lifetime of the attached interface.
|
||||
let dhcp_server = DhcpServer::new(
|
||||
vm_mac_address,
|
||||
host.vm_ip,
|
||||
host.subnet_mask,
|
||||
host.gateway_ip,
|
||||
);
|
||||
|
||||
// Install port forwardings for the reserved VM's IP address
|
||||
for port in exposed_ports {
|
||||
host.port_forwarding_add_rule(port.external_port, host.vm_ip, port.internal_port)
|
||||
.context("failed to configure port forwarding")?;
|
||||
}
|
||||
|
||||
Ok(Proxy {
|
||||
vm,
|
||||
host,
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: DhcpSnooper::new(poller_timeout, vm_mac_address.bytes()),
|
||||
vm_mac_address,
|
||||
dhcp_server,
|
||||
rules,
|
||||
control,
|
||||
flows,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +125,7 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
|
||||
|
||||
// Update coarse time for DHCP snooping and flows
|
||||
// Update coarse time for flows
|
||||
coarsetime::Instant::update();
|
||||
|
||||
// Service control on every wake (including timeouts) so a bounded read or a pending
|
||||
@@ -134,9 +151,6 @@ impl Proxy<'_> {
|
||||
if !self.vm.is_connected()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.port_forwarder
|
||||
.tick(&mut self.host, self.dhcp_snooper.lease());
|
||||
}
|
||||
|
||||
self.poller.rearm();
|
||||
@@ -149,7 +163,7 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
match self.vm.read(buf) {
|
||||
Ok(n) => {
|
||||
// Update coarse time for DHCP snooping and flows
|
||||
// Update coarse time for flows
|
||||
coarsetime::Instant::update();
|
||||
|
||||
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
|
||||
@@ -177,7 +191,7 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
match self.host.read(batch, bufs) {
|
||||
Ok(pktcnt) => {
|
||||
// Update coarse time for DHCP snooping and flows
|
||||
// Update coarse time for flows
|
||||
coarsetime::Instant::update();
|
||||
|
||||
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
|
||||
@@ -263,22 +277,19 @@ impl Proxy<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::NetType;
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use crate::proxy::Proxy;
|
||||
use mac_address::MacAddress;
|
||||
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
|
||||
use serial_test::serial;
|
||||
use smoltcp::wire::{IpProtocol, Ipv4Address, Ipv4Packet, UdpPacket};
|
||||
use std::collections::HashSet;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_blocking_takes_precedence() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let mut proxy = create_proxy(vm_ip, vec!["66.66.0.0/16"], vec!["66.66.0.0/16"]);
|
||||
let mut proxy = create_proxy(vec!["66.66.0.0/16"], vec!["66.66.0.0/16"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
|
||||
assert_eq!(proxy.rules.len(), 1);
|
||||
|
||||
@@ -288,8 +299,8 @@ mod tests {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_longest_prefix_match_wins() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let mut proxy = create_proxy(vm_ip, vec!["33.33.33.33/32"], vec!["33.33.33.0/24"]);
|
||||
let mut proxy = create_proxy(vec!["33.33.33.33/32"], vec!["33.33.33.0/24"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
|
||||
assert_eq!(proxy.rules.len(), 2);
|
||||
|
||||
@@ -301,8 +312,8 @@ mod tests {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allow_host() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let mut proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
|
||||
let mut proxy = create_proxy(vec!["@host"], vec!["0.0.0.0/0"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
|
||||
assert_eq!(proxy.rules.len(), 2);
|
||||
|
||||
@@ -317,10 +328,10 @@ mod tests {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_bare_default_block_applies_in_both_directions_in_stateful_mode() {
|
||||
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
|
||||
let mut proxy = create_proxy(vec!["in 192.0.2.0/24"], vec!["0.0.0.0/0"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
let fallback_peer = Ipv4Address::new(203, 0, 113, 1);
|
||||
let explicitly_allowed_peer = Ipv4Address::new(192, 0, 2, 1);
|
||||
let mut proxy = create_proxy(vm_ip, vec!["in 192.0.2.0/24"], vec!["0.0.0.0/0"]);
|
||||
|
||||
let fallback_request = udp_packet(fallback_peer, 40_000, vm_ip, 1_234);
|
||||
let fallback_request = Ipv4Packet::new_checked(fallback_request.as_slice()).unwrap();
|
||||
@@ -342,9 +353,9 @@ mod tests {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_directional_egress_block_does_not_block_reply_to_unmatched_inbound_flow() {
|
||||
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
|
||||
let mut proxy = create_proxy(vec![], vec!["out 203.0.113.0/24"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
let peer = Ipv4Address::new(203, 0, 113, 1);
|
||||
let mut proxy = create_proxy(vm_ip, vec![], vec!["out 203.0.113.0/24"]);
|
||||
|
||||
let request = udp_packet(peer, 40_000, vm_ip, 1_234);
|
||||
let request = Ipv4Packet::new_checked(request.as_slice()).unwrap();
|
||||
@@ -358,9 +369,9 @@ mod tests {
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_directional_ingress_block_does_not_block_reply_to_bare_outbound_allow() {
|
||||
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
|
||||
let mut proxy = create_proxy(vec!["203.0.113.0/24"], vec!["in 203.0.113.0/24"]);
|
||||
let vm_ip = proxy.host.vm_ip;
|
||||
let peer = Ipv4Address::new(203, 0, 113, 1);
|
||||
let mut proxy = create_proxy(vm_ip, vec!["203.0.113.0/24"], vec!["in 203.0.113.0/24"]);
|
||||
|
||||
let request = udp_packet(vm_ip, 1_234, peer, 40_000);
|
||||
let request = Ipv4Packet::new_checked(request.as_slice()).unwrap();
|
||||
@@ -371,7 +382,7 @@ mod tests {
|
||||
assert!(proxy.allowed_from_host_ipv4(&reply).is_some());
|
||||
}
|
||||
|
||||
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
|
||||
fn create_proxy<'test>(allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
|
||||
let (vm_fd, _) = socketpair(
|
||||
AddressFamily::Unix,
|
||||
SockType::Datagram,
|
||||
@@ -381,9 +392,9 @@ mod tests {
|
||||
.unwrap();
|
||||
let vm_fd = Box::leak(Box::new(vm_fd));
|
||||
|
||||
let mut proxy = Proxy::new(
|
||||
Proxy::new(
|
||||
vm_fd.as_raw_fd(),
|
||||
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
|
||||
MacAddress::from_str("02:00:00:00:00:02").unwrap(),
|
||||
NetType::Nat,
|
||||
allow
|
||||
.into_iter()
|
||||
@@ -396,15 +407,7 @@ mod tests {
|
||||
Vec::default(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
proxy.dhcp_snooper.set_lease(Some(Lease::new(
|
||||
vm_ip,
|
||||
Duration::from_secs(600),
|
||||
HashSet::new(),
|
||||
)));
|
||||
|
||||
proxy
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn allowed_from_vm_ipv4(proxy: &mut Proxy, src: Ipv4Address, dst: &str) -> Option<()> {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use crate::host::Host;
|
||||
use crate::proxy::exposed_port::ExposedPort;
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PortForwarder {
|
||||
port_forwardings: Vec<PortForwarding>,
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct PortForwarding {
|
||||
exposed_port: ExposedPort,
|
||||
forwarding_to_addr: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl PortForwarder {
|
||||
pub fn new(exposed_ports: Vec<ExposedPort>) -> PortForwarder {
|
||||
let port_forwardings = exposed_ports
|
||||
.into_iter()
|
||||
.map(|exposed_port| PortForwarding {
|
||||
exposed_port,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
PortForwarder {
|
||||
port_forwardings,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, host: &mut Host, lease: &Option<Lease>) {
|
||||
if self.failed {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.tick_inner(host, lease) {
|
||||
error!("port-forwarding failed: {}", err);
|
||||
|
||||
self.failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_inner(&mut self, host: &mut Host, lease: &Option<Lease>) -> Result<()> {
|
||||
if let Some(lease) = lease {
|
||||
// Lease exists, but is not valid, remove all port forwardings
|
||||
if !lease.valid() {
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Lease exists and is valid, install/re-install port forwardings
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if let Some(installed_addr) = port_forwarding.forwarding_to_addr {
|
||||
// Port forwarding already installed, perhaps it's outdated?
|
||||
if installed_addr == lease.address() {
|
||||
// Nope, the port forwarding is up to date
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove port forwarding since the lease address had changed
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
// Install new port forwarding
|
||||
host.port_forwarding_add_rule(
|
||||
port_forwarding.exposed_port.external_port,
|
||||
lease.address(),
|
||||
port_forwarding.exposed_port.internal_port,
|
||||
)?;
|
||||
port_forwarding.forwarding_to_addr = Some(lease.address());
|
||||
}
|
||||
} else {
|
||||
// Lease does not exist, remove all port forwardings
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_all_port_forwardings(&mut self, host: &mut Host) -> Result<()> {
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if port_forwarding.forwarding_to_addr.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use smoltcp::wire::UdpPacket;
|
||||
|
||||
pub(crate) trait UdpPacketHelper {
|
||||
const DNS_PORT: u16 = 53;
|
||||
const BOOTPS_PORT: u16 = 67;
|
||||
const BOOTPC_PORT: u16 = 68;
|
||||
|
||||
fn is_dns_request(&self) -> bool;
|
||||
|
||||
fn is_dhcp_request(&self) -> bool;
|
||||
fn is_dhcp_response(&self) -> bool;
|
||||
}
|
||||
|
||||
impl UdpPacketHelper for UdpPacket<&[u8]> {
|
||||
fn is_dns_request(&self) -> bool {
|
||||
self.dst_port() == Self::DNS_PORT
|
||||
}
|
||||
|
||||
fn is_dhcp_request(&self) -> bool {
|
||||
self.src_port() == Self::BOOTPC_PORT && self.dst_port() == Self::BOOTPS_PORT
|
||||
}
|
||||
|
||||
fn is_dhcp_response(&self) -> bool {
|
||||
self.src_port() == Self::BOOTPS_PORT && self.dst_port() == Self::BOOTPC_PORT
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UdpPacketHelper;
|
||||
use smoltcp::wire::UdpPacket;
|
||||
|
||||
#[test]
|
||||
fn test_is_dhcp_request_requires_both_standard_ports() {
|
||||
assert!(is_dhcp_request(68, 67));
|
||||
assert!(!is_dhcp_request(68, 9999));
|
||||
assert!(!is_dhcp_request(9999, 67));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_dhcp_response_requires_both_standard_ports() {
|
||||
assert!(is_dhcp_response(67, 68));
|
||||
assert!(!is_dhcp_response(67, 9999));
|
||||
assert!(!is_dhcp_response(9999, 68));
|
||||
}
|
||||
|
||||
fn is_dhcp_request(src_port: u16, dst_port: u16) -> bool {
|
||||
let buffer = udp_packet_buffer(src_port, dst_port);
|
||||
let udp_pkt = UdpPacket::new_unchecked(&buffer[..]);
|
||||
udp_pkt.is_dhcp_request()
|
||||
}
|
||||
|
||||
fn is_dhcp_response(src_port: u16, dst_port: u16) -> bool {
|
||||
let buffer = udp_packet_buffer(src_port, dst_port);
|
||||
let udp_pkt = UdpPacket::new_unchecked(&buffer[..]);
|
||||
udp_pkt.is_dhcp_response()
|
||||
}
|
||||
|
||||
fn udp_packet_buffer(src_port: u16, dst_port: u16) -> [u8; 8] {
|
||||
let mut buffer = [0; 8];
|
||||
let mut udp_pkt = UdpPacket::new_unchecked(&mut buffer[..]);
|
||||
udp_pkt.set_src_port(src_port);
|
||||
udp_pkt.set_dst_port(dst_port);
|
||||
buffer
|
||||
}
|
||||
}
|
||||
+62
-199
@@ -1,11 +1,8 @@
|
||||
use crate::dhcp_snooper::{Lease, message_matches_bootp_client};
|
||||
use crate::proxy::flows::{FlowDirection, FlowMatch};
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::{Direction, PolicyDecision, Proxy};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use dhcproto::Decodable;
|
||||
use dhcproto::v4::Opcode;
|
||||
use dhcproto::v4::SERVER_PORT;
|
||||
use smoltcp::phy::ChecksumCapabilities;
|
||||
use smoltcp::wire::{
|
||||
ArpOperation, ArpPacket, ArpRepr, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Address,
|
||||
@@ -53,25 +50,17 @@ impl Proxy<'_> {
|
||||
}
|
||||
|
||||
fn allowed_from_vm_arp(&self, arp_pkt: ArpPacket<&[u8]>) -> Option<()> {
|
||||
vm_arp_allowed(arp_pkt, self.vm_mac_address, self.dhcp_snooper.lease())
|
||||
vm_arp_allowed(arp_pkt, self.vm_mac_address, self.host.vm_ip)
|
||||
}
|
||||
|
||||
pub(crate) fn allowed_from_vm_ipv4(&mut 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.is_valid_for(ipv4_pkt.src_addr())
|
||||
{
|
||||
// Unicast DHCP renewal is required to maintain the VM's lease
|
||||
// and must bypass user-specified rules
|
||||
if is_allowed_dhcp_request(
|
||||
&ipv4_pkt,
|
||||
Some(self.host.gateway_ip),
|
||||
self.vm_mac_address,
|
||||
self.dhcp_snooper.lease(),
|
||||
) {
|
||||
return Some(());
|
||||
}
|
||||
// Consume DHCP before enforcing the reserved source address
|
||||
if self.consume_dhcp(&ipv4_pkt) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Is this packet coming from the VM's reserved IP address?
|
||||
if self.host.vm_ip == ipv4_pkt.src_addr() {
|
||||
// Consult the flow table before evaluating outbound policy
|
||||
// so established flows are not treated as new traffic
|
||||
let pending = match self
|
||||
@@ -116,83 +105,50 @@ impl Proxy<'_> {
|
||||
if dst_addr == self.host.gateway_ip {
|
||||
return self.admit_with_tracking_if_trackable(pending);
|
||||
}
|
||||
|
||||
// Additionally, allow DNS requests to DNS-servers
|
||||
// provided to a VM by the host's DHCP server
|
||||
if ipv4_pkt.next_header() == IpProtocol::Udp {
|
||||
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
|
||||
|
||||
if udp_pkt.is_dns_request() && self.dhcp_snooper.valid_dns_target(&dst_addr) {
|
||||
return self.admit_with_tracking_if_trackable(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow outgoing DHCP requests to the bootpd(8) broadcast address,
|
||||
// otherwise DHCP snooper will never be populated
|
||||
if is_allowed_dhcp_request(
|
||||
&ipv4_pkt,
|
||||
None,
|
||||
self.vm_mac_address,
|
||||
self.dhcp_snooper.lease(),
|
||||
) {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allowed_dhcp_request(
|
||||
ipv4_pkt: &Ipv4Packet<&[u8]>,
|
||||
unicast_target: Option<Ipv4Address>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
lease: &Option<Lease>,
|
||||
) -> bool {
|
||||
// Require the source address to be either:
|
||||
// * covered by the VM's current lease
|
||||
// * unspecified on the broadcast DHCP path
|
||||
let src_addr = ipv4_pkt.src_addr();
|
||||
let src_has_valid_lease = lease
|
||||
.as_ref()
|
||||
.is_some_and(|lease| lease.is_valid_for(src_addr));
|
||||
if !src_has_valid_lease && !(unicast_target.is_none() && src_addr.is_unspecified()) {
|
||||
return false;
|
||||
fn consume_dhcp(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> bool {
|
||||
// Only inspect UDP packets that contain the source and destination ports
|
||||
if ipv4_pkt.next_header() != IpProtocol::Udp
|
||||
|| ipv4_pkt.frag_offset() != 0
|
||||
|| ipv4_pkt.payload().len() < 4
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only consume packets addressed to the DHCP server port
|
||||
let udp = UdpPacket::new_unchecked(ipv4_pkt.payload());
|
||||
if udp.dst_port() != SERVER_PORT {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pass the request to the DHCP server and send any reply
|
||||
let result = match self.dhcp_server.receive_vm(ipv4_pkt, &udp) {
|
||||
Ok(Some(reply)) => self.write_to_vm(&reply),
|
||||
Ok(None) => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
// Report failures to generate or send the reply
|
||||
if let Err(err) = result {
|
||||
sentry::capture_message(
|
||||
&format!("Failed to reply to DHCP request: {err:#}"),
|
||||
sentry::Level::Warning,
|
||||
);
|
||||
}
|
||||
|
||||
// Consume the packet even if the request was rejected or the reply failed
|
||||
true
|
||||
}
|
||||
|
||||
let dst_addr = ipv4_pkt.dst_addr();
|
||||
|
||||
// Keep the common path cheap and inspect UDP only for a permitted DHCP target
|
||||
if !dst_addr.is_broadcast() && unicast_target != Some(dst_addr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ipv4_pkt.next_header() != IpProtocol::Udp {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(udp_pkt) = UdpPacket::new_checked(ipv4_pkt.payload()) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Require the standard DHCP client and server ports
|
||||
if !udp_pkt.is_dhcp_request() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Require the BOOTP client hardware address to match this VM
|
||||
let mut decoder = dhcproto::v4::Decoder::new(udp_pkt.payload());
|
||||
let Ok(message) = dhcproto::v4::Message::decode(&mut decoder) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
message_matches_bootp_client(&message, Opcode::BootRequest, vm_mac_address.0)
|
||||
}
|
||||
|
||||
fn vm_arp_allowed(
|
||||
arp_pkt: ArpPacket<&[u8]>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
lease: &Option<Lease>,
|
||||
address: Ipv4Address,
|
||||
) -> Option<()> {
|
||||
let (operation, source_hardware_addr, source_protocol_addr) =
|
||||
match ArpRepr::parse(&arp_pkt).ok()? {
|
||||
@@ -213,84 +169,34 @@ fn vm_arp_allowed(
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(lease) = lease {
|
||||
if lease.is_valid_for(source_protocol_addr) {
|
||||
return Some(());
|
||||
}
|
||||
} else if source_protocol_addr.is_unspecified() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
None
|
||||
(source_protocol_addr == address || source_protocol_addr.is_unspecified()).then_some(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use dhcproto::v4::{DhcpOption, Message, MessageType};
|
||||
use dhcproto::{Encodable, Encoder};
|
||||
use smoltcp::wire::{
|
||||
ArpHardware, ArpOperation, ArpPacket, EthernetAddress, EthernetProtocol, IpProtocol,
|
||||
Ipv4Address, Ipv4Packet, UdpPacket,
|
||||
ArpHardware, ArpOperation, ArpPacket, EthernetAddress, EthernetProtocol, Ipv4Address,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
const VM_MAC: EthernetAddress = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
|
||||
#[test]
|
||||
fn test_allowed_dhcp_request_policy() {
|
||||
let gateway = Ipv4Address::new(192, 168, 64, 1);
|
||||
let lease_ip = Ipv4Address::new(192, 168, 64, 2);
|
||||
let other = Ipv4Address::new(192, 168, 64, 3);
|
||||
let no_lease = None;
|
||||
let lease = Some(Lease::new(
|
||||
lease_ip,
|
||||
Duration::from_secs(600),
|
||||
HashSet::new(),
|
||||
));
|
||||
let initial = |src, chaddr| {
|
||||
allowed_dhcp_request(src, Ipv4Address::BROADCAST, None, chaddr, &no_lease)
|
||||
};
|
||||
let renewal = |src, dst| allowed_dhcp_request(src, dst, Some(gateway), VM_MAC.0, &lease);
|
||||
let other_mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02];
|
||||
|
||||
assert!(initial(Ipv4Address::UNSPECIFIED, VM_MAC.0));
|
||||
assert!(renewal(lease_ip, gateway));
|
||||
assert!(!renewal(other, gateway));
|
||||
assert!(!renewal(Ipv4Address::UNSPECIFIED, gateway));
|
||||
assert!(!renewal(lease_ip, other));
|
||||
assert!(!initial(Ipv4Address::UNSPECIFIED, other_mac));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allowed_from_vm_arp_allows_unspecified_request_without_lease() {
|
||||
fn test_allowed_from_vm_arp_allows_unspecified_request() {
|
||||
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
let buf = arp_packet(vm_mac_address.0, [0, 0, 0, 0], ArpOperation::Request, 6, 4);
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_some());
|
||||
assert!(
|
||||
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allowed_from_vm_arp_allows_reply_for_leased_ip() {
|
||||
let vm_mac_address = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
|
||||
let lease_ip = Ipv4Address::new(192, 168, 0, 2);
|
||||
let lease = Some(Lease::new(
|
||||
lease_ip,
|
||||
Duration::from_secs(600),
|
||||
HashSet::new(),
|
||||
));
|
||||
let buf = arp_packet(
|
||||
vm_mac_address.0,
|
||||
lease_ip.octets(),
|
||||
ArpOperation::Reply,
|
||||
6,
|
||||
4,
|
||||
);
|
||||
let vm_ip = Ipv4Address::new(192, 168, 0, 2);
|
||||
let buf = arp_packet(vm_mac_address.0, vm_ip.octets(), ArpOperation::Reply, 6, 4);
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &lease).is_some());
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, vm_ip).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -305,7 +211,9 @@ mod tests {
|
||||
);
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
|
||||
assert!(
|
||||
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -316,7 +224,9 @@ mod tests {
|
||||
arp_pkt.set_hardware_type(ArpHardware::Unknown(2));
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
|
||||
assert!(
|
||||
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -327,7 +237,9 @@ mod tests {
|
||||
arp_pkt.set_protocol_type(EthernetProtocol::Ipv6);
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
|
||||
assert!(
|
||||
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -336,7 +248,9 @@ mod tests {
|
||||
let buf = arp_packet(vm_mac_address.0, [0, 0, 0], ArpOperation::Request, 6, 3);
|
||||
let arp_pkt = ArpPacket::new_checked(buf.as_slice()).unwrap();
|
||||
|
||||
assert!(super::vm_arp_allowed(arp_pkt, vm_mac_address, &None).is_none());
|
||||
assert!(
|
||||
super::vm_arp_allowed(arp_pkt, vm_mac_address, Ipv4Address::new(1, 2, 3, 4)).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
fn arp_packet(
|
||||
@@ -361,55 +275,4 @@ mod tests {
|
||||
arp_pkt.set_target_protocol_addr(&vec![0; protocol_len as usize]);
|
||||
buf
|
||||
}
|
||||
|
||||
fn allowed_dhcp_request(
|
||||
src_addr: Ipv4Address,
|
||||
dst_addr: Ipv4Address,
|
||||
unicast_target: Option<Ipv4Address>,
|
||||
chaddr: [u8; 6],
|
||||
lease: &Option<Lease>,
|
||||
) -> bool {
|
||||
let mut buf = dhcp_request(chaddr);
|
||||
let mut ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_mut_slice());
|
||||
ipv4_pkt.set_src_addr(src_addr);
|
||||
ipv4_pkt.set_dst_addr(dst_addr);
|
||||
|
||||
let ipv4_pkt = Ipv4Packet::new_checked(buf.as_slice()).unwrap();
|
||||
super::is_allowed_dhcp_request(&ipv4_pkt, unicast_target, VM_MAC, lease)
|
||||
}
|
||||
|
||||
fn dhcp_request(chaddr: [u8; 6]) -> Vec<u8> {
|
||||
let mut message = Message::new(
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
Ipv4Address::UNSPECIFIED,
|
||||
&chaddr,
|
||||
);
|
||||
message
|
||||
.opts_mut()
|
||||
.insert(DhcpOption::MessageType(MessageType::Discover));
|
||||
|
||||
let mut dhcp_payload = Vec::new();
|
||||
message
|
||||
.encode(&mut Encoder::new(&mut dhcp_payload))
|
||||
.unwrap();
|
||||
|
||||
let total_len = 20 + 8 + dhcp_payload.len();
|
||||
let mut buf = vec![0; total_len];
|
||||
let mut ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_mut_slice());
|
||||
ipv4_pkt.set_version(4);
|
||||
ipv4_pkt.set_header_len(20);
|
||||
ipv4_pkt.set_total_len(total_len as u16);
|
||||
ipv4_pkt.set_next_header(IpProtocol::Udp);
|
||||
ipv4_pkt.set_src_addr(Ipv4Address::UNSPECIFIED);
|
||||
ipv4_pkt.set_dst_addr(Ipv4Address::BROADCAST);
|
||||
|
||||
let mut udp_pkt = UdpPacket::new_unchecked(ipv4_pkt.payload_mut());
|
||||
udp_pkt.set_src_port(68);
|
||||
udp_pkt.set_dst_port(67);
|
||||
udp_pkt.set_len((8 + dhcp_payload.len()) as u16);
|
||||
udp_pkt.payload_mut().copy_from_slice(&dhcp_payload);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user