149 lines
5.2 KiB
Rust
149 lines
5.2 KiB
Rust
use crate::proxy::flows::{FlowDirection, FlowMatch};
|
|
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
|
use crate::proxy::{Direction, PolicyDecision, Proxy};
|
|
use anyhow::{Context, Result};
|
|
use smoltcp::phy::ChecksumCapabilities;
|
|
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, Ipv4Repr, 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 || frame.dst_addr().is_broadcast() {
|
|
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_unchecked(frame.payload());
|
|
Ipv4Repr::parse(&ipv4_pkt, &ChecksumCapabilities::ignored()).ok()?;
|
|
|
|
self.allowed_from_host_ipv4(&ipv4_pkt)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(super) fn allowed_from_host_ipv4(&mut self, ipv4_pkt: &Ipv4Packet<&[u8]>) -> Option<()> {
|
|
// Backwards compatibility with Softnet consumers that only use stateless rules
|
|
if self.flows.is_none() {
|
|
return Some(());
|
|
}
|
|
|
|
// 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(());
|
|
}
|
|
|
|
// 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()))
|
|
{
|
|
match self
|
|
.flows
|
|
.as_mut()?
|
|
.inspect(ipv4_pkt, FlowDirection::FromHost)
|
|
{
|
|
FlowMatch::Allowed => return Some(()),
|
|
FlowMatch::Denied => return None,
|
|
FlowMatch::Candidate(pending) => Some(pending),
|
|
FlowMatch::Untracked => None,
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// The flow is either pending or untracked, evaluate it against inbound policy
|
|
match self
|
|
.rules
|
|
.policy_decision(ipv4_pkt.src_addr(), Direction::In)
|
|
{
|
|
// Return traffic was handled above; enforce explicit inbound blocks here
|
|
Some(PolicyDecision::Block) => None,
|
|
|
|
// Stateless policy is outbound-only; fail closed if this invariant is violated
|
|
Some(PolicyDecision::AllowStateless) => None,
|
|
|
|
// Untracked packets cannot satisfy stateful policy
|
|
Some(PolicyDecision::AllowStateful) => self.admit_with_tracking(pending?),
|
|
|
|
// No inbound rule matched, so allow by default. Track the flow when needed
|
|
// so its reply is not treated as a new outbound flow
|
|
None => {
|
|
self.admit_with_tracking_if_stateful(pending, ipv4_pkt.src_addr(), Direction::Out)
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
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 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)
|
|
}
|
|
}
|