tart-softnet/lib/proxy/flows/icmp.rs

174 lines
5.7 KiB
Rust

use super::{FlowDirection, FlowKey, FlowMatch, FlowTable};
use coarsetime::{Duration, Instant};
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, Ipv4Address, Ipv4Packet};
const ECHO_TIMEOUT: Duration = Duration::from_secs(30);
impl FlowTable {
pub(super) fn inspect_icmp(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
now: Instant,
) -> FlowMatch {
let Ok(icmp) = Icmpv4Packet::new_checked(ipv4_pkt.payload()) else {
return FlowMatch::Denied;
};
if !icmp.verify_checksum() {
return FlowMatch::Denied;
}
match (icmp.msg_type(), icmp.msg_code()) {
(Icmpv4Message::EchoRequest, 0) => {
self.inspect_echo(ipv4_pkt, direction, icmp.echo_ident(), true, now)
}
(Icmpv4Message::EchoReply, 0) => {
self.inspect_echo(ipv4_pkt, direction, icmp.echo_ident(), false, now)
}
// Other ICMP messages follow normal source policy: treating errors that quote
// tracked tuples as RELATED requires validation beyond this exact-tuple table
//
// Potential degradation of PMTU discovery and traceroute is an accepted tradeoff.
_ => FlowMatch::Untracked,
}
}
fn inspect_echo(
&mut self,
ipv4_pkt: &Ipv4Packet<&[u8]>,
direction: FlowDirection,
ident: u16,
is_request: bool,
now: Instant,
) -> FlowMatch {
// We need to preserve the request direction so opposite-direction echo flows remain distinct
let initiating_direction = if is_request {
direction
} else {
direction.opposite()
};
let key = FlowKey::icmp_echo(
ipv4_pkt.src_addr(),
ipv4_pkt.dst_addr(),
direction,
initiating_direction,
ident,
);
if let Some(matched) = self.match_existing_flow(key, direction, now, ECHO_TIMEOUT) {
return matched;
}
if is_request {
FlowMatch::candidate(key, initiating_direction, now, ECHO_TIMEOUT)
} else {
// Only replies matching an admitted request are tracked
FlowMatch::Untracked
}
}
}
impl FlowKey {
fn icmp_echo(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
direction: FlowDirection,
initiating_direction: FlowDirection,
ident: u16,
) -> Self {
let (host_addr, vm_addr) = direction.host_vm_pair(src_addr, dst_addr);
Self::IcmpEcho {
host_addr,
vm_addr,
ident,
initiating_direction,
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_support::{HOST, VM, inspect_from_host, inspect_from_vm, ipv4_packet};
use super::super::{FlowMatch, FlowTable};
use smoltcp::wire::{Icmpv4Message, Icmpv4Packet, IpProtocol, Ipv4Address};
#[test]
fn echo_request_and_reply_are_tracked_in_both_directions() {
let mut tracker = FlowTable::new();
let vm_request = echo_packet(VM, HOST, Icmpv4Message::EchoRequest, 7, 1);
let FlowMatch::Candidate(pending) = inspect_from_vm(&mut tracker, &vm_request) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
let host_reply = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &host_reply),
FlowMatch::Allowed
));
let wrong_ident = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 8, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &wrong_ident),
FlowMatch::Untracked
));
// The opposite direction is a distinct flow even with the same identifier.
let host_request = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
let FlowMatch::Candidate(pending) = inspect_from_host(&mut tracker, &host_request) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
let vm_reply = echo_packet(VM, HOST, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_reply),
FlowMatch::Allowed
));
}
#[test]
fn invalid_echo_is_denied_and_unsolicited_echo_is_untracked() {
let mut tracker = FlowTable::new();
let unsolicited = echo_packet(HOST, VM, Icmpv4Message::EchoReply, 7, 1);
assert!(matches!(
inspect_from_host(&mut tracker, &unsolicited),
FlowMatch::Untracked
));
let mut wrong_code = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
wrong_code[21] = 1;
Icmpv4Packet::new_unchecked(&mut wrong_code[20..]).fill_checksum();
assert!(matches!(
inspect_from_host(&mut tracker, &wrong_code),
FlowMatch::Untracked
));
let mut bad_checksum = echo_packet(HOST, VM, Icmpv4Message::EchoRequest, 7, 1);
bad_checksum[27] ^= 1;
assert!(matches!(
inspect_from_host(&mut tracker, &bad_checksum),
FlowMatch::Denied
));
}
fn echo_packet(
src_addr: Ipv4Address,
dst_addr: Ipv4Address,
message: Icmpv4Message,
ident: u16,
sequence: u16,
) -> Vec<u8> {
let mut bytes = ipv4_packet(src_addr, dst_addr, IpProtocol::Icmp, 8);
let mut icmp = Icmpv4Packet::new_unchecked(&mut bytes[20..]);
icmp.set_msg_type(message);
icmp.set_msg_code(0);
icmp.set_echo_ident(ident);
icmp.set_echo_seq_no(sequence);
icmp.fill_checksum();
bytes
}
}