tart-softnet/lib/proxy/conntrack_tcp.rs

258 lines
8.4 KiB
Rust

use super::{
Conntrack, ConntrackResult, Direction, Flow, FlowKey, FlowState, PendingFlow, initiator,
oriented_transport_key,
};
use coarsetime::{Duration, Instant};
use smoltcp::wire::{Ipv4Packet, TcpPacket};
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(60);
const ESTABLISHED_TIMEOUT: Duration = Duration::from_secs(5 * 24 * 60 * 60);
const CLOSING_TIMEOUT: Duration = Duration::from_secs(2 * 60);
#[derive(Clone, Copy, Debug)]
pub(super) enum State {
SynSent,
SynReceived,
Established,
Closing { host_fin: bool, vm_fin: bool },
}
impl State {
pub(super) fn timeout(self) -> Duration {
match self {
Self::SynSent | Self::SynReceived => HANDSHAKE_TIMEOUT,
Self::Established => ESTABLISHED_TIMEOUT,
Self::Closing { .. } => CLOSING_TIMEOUT,
}
}
}
impl Conntrack {
pub(super) fn inspect_tcp(
&mut self,
packet: &Ipv4Packet<&[u8]>,
direction: Direction,
now: Instant,
) -> ConntrackResult {
let Ok(tcp) = TcpPacket::new_checked(packet.payload()) else {
return ConntrackResult::Denied;
};
if tcp.src_port() == 0 || tcp.dst_port() == 0 {
return ConntrackResult::Denied;
}
let key = oriented_transport_key(
packet,
direction,
|host_addr, host_port, vm_addr, vm_port| FlowKey::Tcp {
host_addr,
host_port,
vm_addr,
vm_port,
},
tcp.src_port(),
tcp.dst_port(),
);
if let Some(flow) = self.flows.get_mut(&key) {
let from_initiator = matches!(
(flow.initiator, direction),
(super::Initiator::Host, Direction::FromHost)
| (super::Initiator::Vm, Direction::FromVm)
);
let FlowState::Tcp(state) = &mut flow.state else {
return ConntrackResult::Denied;
};
if tcp.rst() {
self.flows.remove(&key);
return ConntrackResult::Allowed;
}
let allowed = match *state {
State::SynSent if from_initiator => is_initial_syn(&tcp),
State::SynSent => {
if tcp.syn() && tcp.ack() && !tcp.fin() {
*state = State::SynReceived;
true
} else {
false
}
}
State::SynReceived if from_initiator => {
if is_initial_syn(&tcp) {
true
} else if tcp.ack() && !tcp.syn() {
*state = if tcp.fin() {
State::Closing {
host_fin: matches!(direction, Direction::FromHost),
vm_fin: matches!(direction, Direction::FromVm),
}
} else {
State::Established
};
true
} else {
false
}
}
State::SynReceived => tcp.syn() && tcp.ack() && !tcp.fin(),
State::Established | State::Closing { .. }
if is_initial_syn(&tcp) && !from_initiator =>
{
false
}
State::Established | State::Closing { .. } if is_initial_syn(&tcp) => {
*state = State::SynSent;
true
}
State::Established if tcp.fin() => {
*state = State::Closing {
host_fin: matches!(direction, Direction::FromHost),
vm_fin: matches!(direction, Direction::FromVm),
};
true
}
State::Closing { .. } if tcp.fin() => {
if let State::Closing { host_fin, vm_fin } = state {
match direction {
Direction::FromHost => *host_fin = true,
Direction::FromVm => *vm_fin = true,
}
}
true
}
_ => true,
};
if allowed {
flow.last_seen = now;
}
return if allowed {
ConntrackResult::Allowed
} else {
ConntrackResult::Denied
};
}
if !is_initial_syn(&tcp) {
return ConntrackResult::Denied;
}
ConntrackResult::New(PendingFlow {
key,
flow: Flow {
initiator: initiator(direction),
state: FlowState::Tcp(State::SynSent),
last_seen: now,
},
})
}
}
fn is_initial_syn(tcp: &TcpPacket<&[u8]>) -> bool {
tcp.syn() && !tcp.ack() && !tcp.fin() && !tcp.rst()
}
#[cfg(test)]
mod tests {
use super::super::test_support::{
HOST, TcpFlags, VM, inspect_from_host, inspect_from_vm, tcp_packet,
};
use super::super::{Conntrack, ConntrackResult};
#[test]
fn tcp_flows_are_oriented() {
let mut tracker = Conntrack::new();
let vm_syn = tcp_packet(VM, 22, HOST, 49152, TcpFlags::SYN);
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_syn),
ConntrackResult::New(_)
));
let host_syn = tcp_packet(HOST, 49152, VM, 22, TcpFlags::SYN);
let ConntrackResult::New(pending) = inspect_from_host(&mut tracker, &host_syn) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
let premature_vm_ack = tcp_packet(VM, 22, HOST, 49152, TcpFlags::ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &premature_vm_ack),
ConntrackResult::Denied
));
let wrong_vm_reply = tcp_packet(VM, 22, HOST, 49153, TcpFlags::SYN_ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &wrong_vm_reply),
ConntrackResult::Denied
));
let vm_syn_ack = tcp_packet(VM, 22, HOST, 49152, TcpFlags::SYN_ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_syn_ack),
ConntrackResult::Allowed
));
let premature_vm_ack = tcp_packet(VM, 22, HOST, 49152, TcpFlags::ACK);
assert!(matches!(
inspect_from_vm(&mut tracker, &premature_vm_ack),
ConntrackResult::Denied
));
let host_ack = tcp_packet(HOST, 49152, VM, 22, TcpFlags::ACK);
assert!(matches!(
inspect_from_host(&mut tracker, &host_ack),
ConntrackResult::Allowed
));
assert!(matches!(
inspect_from_vm(&mut tracker, &premature_vm_ack),
ConntrackResult::Allowed
));
}
#[test]
fn vm_can_initiate_tcp() {
let mut tracker = Conntrack::new();
let vm_syn = tcp_packet(VM, 49152, HOST, 22, TcpFlags::SYN);
let host_syn_ack = tcp_packet(HOST, 22, VM, 49152, TcpFlags::SYN_ACK);
let vm_ack = tcp_packet(VM, 49152, HOST, 22, TcpFlags::ACK);
let ConntrackResult::New(pending) = inspect_from_vm(&mut tracker, &vm_syn) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
assert!(matches!(
inspect_from_host(&mut tracker, &host_syn_ack),
ConntrackResult::Allowed
));
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_ack),
ConntrackResult::Allowed
));
}
#[test]
fn tcp_rst_removes_permission() {
let mut tracker = Conntrack::new();
let host_syn = tcp_packet(HOST, 49152, VM, 22, TcpFlags::SYN);
let vm_rst = tcp_packet(VM, 22, HOST, 49152, TcpFlags::RST);
let vm_ack = tcp_packet(VM, 22, HOST, 49152, TcpFlags::ACK);
let ConntrackResult::New(pending) = inspect_from_host(&mut tracker, &host_syn) else {
panic!("expected a new flow");
};
assert!(tracker.commit(pending));
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_rst),
ConntrackResult::Allowed
));
assert!(matches!(
inspect_from_vm(&mut tracker, &vm_ack),
ConntrackResult::Denied
));
}
}