tart-softnet/lib/proxy/host.rs

140 lines
4.7 KiB
Rust

use crate::proxy::conntrack::ConntrackResult;
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::{Action, Direction, Proxy, Rule, select_rules};
use anyhow::{Context, Result};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
impl Proxy<'_> {
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
if self.allowed_from_host(frame).is_none() {
// Block packet by not forwarding it to the VM
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 {
self.snoop(frame);
}
match self.vm.write(frame.as_ref()) {
Ok(_) => Ok(()),
Err(err) => {
if let Some(libc::ENOBUFS) = err.raw_os_error() {
if !self.enobufs_encountered {
sentry::capture_message(
"No buffer space available in VM's socket",
sentry::Level::Warning,
);
self.enobufs_encountered = true;
}
return Ok(());
}
Err(err).context("failed to write to the VM")
}
}
}
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
match frame.ethertype() {
EthernetProtocol::Arp => Some(()),
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
self.allowed_from_host_ipv4(&ipv4_pkt)
}
_ => None,
}
}
fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> {
if !self.stateful_policy {
return Some(());
}
if let Some(rules) = select_rules(&self.rules, ipv4_pkt.src_addr(), Direction::In) {
// DHCP is required to maintain the VM's lease and must bypass user-specified rules
if self.is_allowed_dhcp_response(ipv4_pkt) {
return Some(());
}
// Only process packets addressed to the VM's current IP
let Some(lease) = self.dhcp_snooper.lease() else {
return None;
};
if !lease.is_valid_for(ipv4_pkt.dst_addr()) {
return None;
}
// Stateless rules decide each packet immediately, without consulting the conntrack
if let Some((action, _)) = rules
.iter()
.find(|(_, rule)| matches!(rule, Rule::Stateless(_)))
{
return (*action == Action::Allow).then_some(());
}
// Existing connections follow conntrack; new ones require an inbound stateful allow rule
return match self.conntrack.inspect_from_host(ipv4_pkt) {
ConntrackResult::Allowed => Some(()),
ConntrackResult::Denied => None,
ConntrackResult::New(pending) => {
let allow_new = rules.iter().any(|(action, rule)| {
*action == Action::Allow
&& matches!(
rule,
Rule::Stateful {
direction: Direction::In,
..
}
)
});
if !allow_new {
return None;
}
self.conntrack.commit(pending).then_some(())
}
};
}
Some(())
}
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,
};
if !self.is_allowed_dhcp_response(&ipv4_pkt) {
return;
}
let udp_pkt = match UdpPacket::new_checked(ipv4_pkt.payload()) {
Ok(udp_pkt) => udp_pkt,
Err(_) => return,
};
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
}
fn is_allowed_dhcp_response(&self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> bool {
if ipv4_pkt.src_addr() != self.host.gateway_ip
|| ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp
{
return false;
}
UdpPacket::new_checked(ipv4_pkt.payload())
.map(|udp_pkt| udp_pkt.is_dhcp_response())
.unwrap_or(false)
}
}