blob: 8f7f6396935c383610cd0c66002e8a73ac5fcf7f [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use alloc::{collections::HashMap, sync::Arc};
use core::hash::Hash;
use derivative::Derivative;
use packet_formats::ip::IpExt;
use crate::{
context::FilterBindingsContext, packets::TransportPacket, FilterBindingsTypes, IpPacket,
MaybeTransportPacket,
};
use netstack3_sync::Mutex;
/// Implements a connection tracking subsystem.
///
/// The `E` parameter is for external data that is stored in the [`Connection`]
/// struct and can be extracted with the [`Connection::external_data()`]
/// function.
#[allow(dead_code)]
#[derive(Derivative)]
#[derivative(Default(bound = ""))]
pub struct Table<I: IpExt, BT: FilterBindingsTypes, E> {
/// A connection is inserted into the map twice: once for the original
/// tuple, and once for the reply tuple.
map: Mutex<HashMap<Tuple<I>, Arc<ConnectionShared<I, BT, E>>>>,
}
#[allow(dead_code)]
impl<I: IpExt, BT: FilterBindingsTypes, E> Table<I, BT, E> {
pub(crate) fn new() -> Self {
Self { map: Mutex::new(HashMap::new()) }
}
/// Returns whether the table contains a connection for the specified tuple.
///
/// This is for NAT to determine whether a generated tuple will clash with
/// one already in the map. While it might seem inefficient, to require
/// locking in a loop, taking an uncontested lock is going to be
/// significantly faster than the RNG used to allocate NAT parameters.
pub(crate) fn contains_tuple(&self, tuple: &Tuple<I>) -> bool {
self.map.lock().contains_key(tuple)
}
/// Attempts to insert the `Connection` into the table.
///
/// To be called once a packet for the connection has passed all filtering.
/// The boolean return value represents whether the connection was newly
/// added to the connection tracking state.
///
/// This is on [`Table`] instead of [`Connection`] because conntrack needs
/// to be able to manipulate its internal map.
pub(crate) fn finalize_connection(
&self,
connection: Connection<I, BT, E>,
) -> Result<bool, FinalizeConnectionError> {
let exclusive = match connection {
Connection::Exclusive(c) => c,
// Given that make_shared is private, the only way for us to receive
// a shared connection is if it was already present in the map. This
// is far and away the most common case under normal operation.
Connection::Shared(_) => return Ok(false),
};
let mut guard = self.map.lock();
// The expected case here is that there isn't a conflict.
//
// Normally, we'd want to use the entry API to reduce the number of map
// lookups, but this setup allows us to completely avoid any heap
// allocations until we're sure that the insertion will succeed. This
// wastes a little CPU in the common case to avoid pathological behavior
// in degenerate cases.
//
// NOTE: It's theoretically possible for the first two packets (or more)
// in the same flow to create ExclusiveConnections. In this case,
// subsequent packets will be reported as conflicts. However, it should
// be the case that packets for the same flow are handled sequentially,
// so each subsequent packet should see the connection created by the
// first one.
if guard.contains_key(&exclusive.inner.original_tuple)
|| guard.contains_key(&exclusive.inner.reply_tuple)
{
Err(FinalizeConnectionError::Conflict)
} else {
let shared = exclusive.make_shared();
let res = guard.insert(shared.inner.original_tuple.clone(), shared.clone());
debug_assert!(res.is_none());
let res = guard.insert(shared.inner.reply_tuple.clone(), shared);
debug_assert!(res.is_none());
Ok(true)
}
}
}
#[allow(dead_code)]
impl<I: IpExt, BC: FilterBindingsContext, E: Default> Table<I, BC, E> {
/// Returns a [`Connection`] for the packet's flow. If a connection does not
/// currently exist, a new one is created.
///
/// At the same time, process the packet for the connection, updating
/// internal connection state.
///
/// After processing is complete, you must call
/// [`finalize_connection`](Table::finalize_connection) with this
/// connection.
pub(crate) fn get_connection_for_packet_and_update<P: IpPacket<I>>(
&self,
bindings_ctx: &BC,
packet: &P,
) -> Option<Connection<I, BC, E>> {
let tuple = Tuple::from_packet(packet)?;
let mut connection = match self.map.lock().get(&tuple) {
Some(connection) => Connection::Shared(connection.clone()),
None => {
Connection::Exclusive(ConnectionExclusive::from_tuple(bindings_ctx, tuple.clone()))
}
};
match connection.update(bindings_ctx, &tuple) {
Ok(()) => Some(connection),
Err(e) => match e {
ConnectionUpdateError::NonMatchingTuple => {
panic!(
"Tuple didn't match. tuple={:?}, conn.original={:?}, conn.reply={:?}",
&tuple,
connection.original_tuple(),
connection.reply_tuple()
);
}
},
}
}
}
/// A tuple for a flow in a single direction.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Tuple<I: IpExt> {
protocol: I::Proto,
src_addr: I::Addr,
dst_addr: I::Addr,
src_port_or_id: u16,
dst_port_or_id: u16,
}
#[allow(dead_code)]
impl<I: IpExt> Tuple<I> {
/// Creates a `Tuple` from an `IpPacket`, if possible.
///
/// Returns `None` if the packet doesn't have an inner transport packet.
pub(crate) fn from_packet<'a, P: IpPacket<I>>(packet: &'a P) -> Option<Self> {
let maybe_transport_packet = packet.transport_packet();
let transport_packet = maybe_transport_packet.transport_packet()?;
Some(Self {
protocol: packet.protocol(),
src_addr: packet.src_addr(),
dst_addr: packet.dst_addr(),
src_port_or_id: transport_packet.src_port(),
dst_port_or_id: transport_packet.dst_port(),
})
}
/// Returns the inverted version of the tuple.
///
/// This means the src and dst addresses are swapped. For TCP and UDP, the
/// ports are reversed, but for ICMP, where the ports stand in for other
/// information, things are more complicated.
pub(crate) fn invert(self) -> Tuple<I> {
// TODO(https://fxbug.dev/328064082): Support ICMP properly. The
// request/response message have different ICMP types.
Self {
protocol: self.protocol,
src_addr: self.dst_addr,
dst_addr: self.src_addr,
src_port_or_id: self.dst_port_or_id,
dst_port_or_id: self.src_port_or_id,
}
}
}
/// The direction of a packet when compared to the given connection.
#[derive(Debug)]
pub(crate) enum ConnectionDirection {
/// The packet is traveling in the same direction as the first packet seen
/// for the [`Connection`].
Original,
/// The packet is traveling in the opposite direction from the first packet
/// seen for the [`Connection`].
Reply,
}
/// An error returned from [`Table::finalize_connection`].
#[derive(Debug)]
pub(crate) enum FinalizeConnectionError {
/// There is a conflicting connection already tracked by conntrack. The
/// to-be-finalized connection was not inserted into the table.
Conflict,
}
/// An error returned from [`Connection::update`].
#[derive(Debug)]
pub(crate) enum ConnectionUpdateError {
/// The provided tuple doesn't belong to the connection being updated.
NonMatchingTuple,
}
/// A `Connection` contains all of the information about a single connection
/// tracked by conntrack.
#[derive(Debug)]
#[allow(dead_code)]
pub enum Connection<I: IpExt, BT: FilterBindingsTypes, E> {
/// A connection that is directly owned by the packet that originated the
/// connection and no others. All fields are modifiable.
Exclusive(ConnectionExclusive<I, BT, E>),
/// This is an existing connection, and there are possibly many other
/// packets that are concurrently modifying it.
Shared(Arc<ConnectionShared<I, BT, E>>),
}
#[allow(dead_code)]
impl<I: IpExt, BT: FilterBindingsTypes, E> Connection<I, BT, E> {
/// Returns the tuple of the original direction of this connection
pub(crate) fn original_tuple(&self) -> &Tuple<I> {
match self {
Connection::Exclusive(c) => &c.inner.original_tuple,
Connection::Shared(c) => &c.inner.original_tuple,
}
}
/// Returns the tuple of the reply direction of this connection.
pub(crate) fn reply_tuple(&self) -> &Tuple<I> {
match self {
Connection::Exclusive(c) => &c.inner.reply_tuple,
Connection::Shared(c) => &c.inner.reply_tuple,
}
}
/// Returns a reference to the [`Connection::external_data`] field.
pub(crate) fn external_data(&self) -> &E {
match self {
Connection::Exclusive(c) => &c.inner.external_data,
Connection::Shared(c) => &c.inner.external_data,
}
}
/// Returns the direction the tuple represents with respect to the
/// connection.
pub(crate) fn direction(&self, tuple: &Tuple<I>) -> Option<ConnectionDirection> {
let (original, reply) = match self {
Connection::Exclusive(c) => (&c.inner.original_tuple, &c.inner.reply_tuple),
Connection::Shared(c) => (&c.inner.original_tuple, &c.inner.reply_tuple),
};
if tuple == original {
Some(ConnectionDirection::Original)
} else if tuple == reply {
Some(ConnectionDirection::Reply)
} else {
None
}
}
/// Returns a copy of the internal connection state
pub(crate) fn state(&self) -> ConnectionState<BT> {
match self {
Connection::Exclusive(c) => c.state.clone(),
Connection::Shared(c) => c.state.lock().clone(),
}
}
}
impl<I: IpExt, BC: FilterBindingsContext, E> Connection<I, BC, E> {
fn update(&mut self, bindings_ctx: &BC, tuple: &Tuple<I>) -> Result<(), ConnectionUpdateError> {
let direction = match self.direction(tuple) {
Some(d) => d,
None => return Err(ConnectionUpdateError::NonMatchingTuple),
};
let now = bindings_ctx.now();
match self {
Connection::Exclusive(c) => c.state.update(direction, now),
Connection::Shared(c) => c.state.lock().update(direction, now),
}
}
}
/// Fields common to both [`ConnectionExclusive`] and [`ConnectionShared`].
#[derive(Debug)]
pub struct ConnectionCommon<I: IpExt, E> {
/// The 5-tuple for the connection in the original direction. This is
/// arbitrary, and is just the direction where a packet was first seen.
pub(crate) original_tuple: Tuple<I>,
/// The 5-tuple for the connection in the reply direction. This is what's
/// used for packet rewriting for NAT.
pub(crate) reply_tuple: Tuple<I>,
/// Extra information that is not needed by the conntrack module itself. In
/// the case of NAT, we expect this to contain things such as the kind of
/// rewriting that will occur (e.g. SNAT vs DNAT).
pub(crate) external_data: E,
}
/// Dynamic per-connection state.
#[derive(Debug, Derivative)]
#[derivative(Clone(bound = ""))]
pub(crate) struct ConnectionState<BT: FilterBindingsTypes> {
/// Whether this connection has seen packets in both directions.
established: bool,
/// The time the last packet was seen for this connection (in either of the
/// original or reply directions).
last_packet_time: BT::Instant,
}
impl<BT: FilterBindingsTypes> ConnectionState<BT> {
fn update(
&mut self,
dir: ConnectionDirection,
now: BT::Instant,
) -> Result<(), ConnectionUpdateError> {
match dir {
ConnectionDirection::Original => (),
ConnectionDirection::Reply => self.established = true,
};
if self.last_packet_time < now {
self.last_packet_time = now;
}
Ok(())
}
}
/// A conntrack connection with single ownership.
///
/// Because of this, many fields may be updated without synchronization. There
/// is no chance of messing with other packets for this connection or ending up
/// out-of-sync with the table (e.g. by changing the tuples once the connection
/// has been inserted).
#[derive(Debug)]
pub struct ConnectionExclusive<I: IpExt, BT: FilterBindingsTypes, E> {
pub(crate) inner: ConnectionCommon<I, E>,
pub(crate) state: ConnectionState<BT>,
}
#[allow(dead_code)]
impl<I: IpExt, BT: FilterBindingsTypes, E> ConnectionExclusive<I, BT, E> {
/// Turn this exclusive connection into a shared one. This is required in
/// order to insert into the [`Table`] table.
fn make_shared(self) -> Arc<ConnectionShared<I, BT, E>> {
Arc::new(ConnectionShared { inner: self.inner, state: Mutex::new(self.state) })
}
}
impl<I: IpExt, BC: FilterBindingsContext, E: Default> ConnectionExclusive<I, BC, E> {
fn from_tuple(bindings_ctx: &BC, original_tuple: Tuple<I>) -> Self {
let reply_tuple = original_tuple.clone().invert();
Self {
inner: ConnectionCommon { original_tuple, reply_tuple, external_data: E::default() },
state: ConnectionState { established: false, last_packet_time: bindings_ctx.now() },
}
}
}
/// A conntrack connection with shared ownership.
///
/// All fields are private, because other packets, and the conntrack table
/// itself, will be depending on them not to change. Fields must be accessed
/// through the associated methods.
#[derive(Debug)]
pub struct ConnectionShared<I: IpExt, BT: FilterBindingsTypes, E> {
inner: ConnectionCommon<I, E>,
state: Mutex<ConnectionState<BT>>,
}
#[cfg(test)]
mod tests {
use core::{convert::Infallible as Never, time::Duration};
use assert_matches::assert_matches;
use ip_test_macro::ip_test;
use net_declare::{net_ip_v4, net_ip_v6};
use net_types::ip::{Ip, Ipv4, Ipv6};
use packet_formats::ip::IpProto;
use test_case::test_case;
use super::*;
use crate::{
context::testutil::FakeBindingsCtx,
packets::testutil::internal::{FakeIpPacket, FakeTcpSegment, TransportPacketExt},
};
trait TestIpExt: Ip {
const SRC_ADDR: Self::Addr;
const SRC_PORT: u16 = 1234;
const DST_ADDR: Self::Addr;
const DST_PORT: u16 = 9876;
}
impl TestIpExt for Ipv4 {
const SRC_ADDR: Self::Addr = net_ip_v4!("192.168.1.1");
const DST_ADDR: Self::Addr = net_ip_v4!("192.168.254.254");
}
impl TestIpExt for Ipv6 {
const SRC_ADDR: Self::Addr = net_ip_v6!("2001:db8::1");
const DST_ADDR: Self::Addr = net_ip_v6!("2001:db8::ffff");
}
struct NoTransportPacket;
impl MaybeTransportPacket for &NoTransportPacket {
type TransportPacket = Never;
fn transport_packet(&self) -> Option<&Self::TransportPacket> {
None
}
}
impl<I: IpExt> TransportPacketExt<I> for &NoTransportPacket {
fn proto() -> I::Proto {
I::Proto::from(IpProto::Tcp)
}
}
#[ip_test]
#[test_case(IpProto::Udp)]
#[test_case(IpProto::Tcp)]
fn tuple_invert_udp_tcp<I: Ip + IpExt + TestIpExt>(protocol: IpProto) {
let orig_tuple = Tuple::<I> {
protocol: protocol.into(),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let expected = Tuple::<I> {
protocol: protocol.into(),
src_addr: I::DST_ADDR,
dst_addr: I::SRC_ADDR,
src_port_or_id: I::DST_PORT,
dst_port_or_id: I::SRC_PORT,
};
let inverted = orig_tuple.invert();
assert_eq!(inverted, expected);
}
#[ip_test]
fn tuple_from_tcp_packet<I: Ip + IpExt + TestIpExt>() {
let expected = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let packet = FakeIpPacket::<I, _> {
src_ip: I::SRC_ADDR,
dst_ip: I::DST_ADDR,
body: FakeTcpSegment { src_port: I::SRC_PORT, dst_port: I::DST_PORT },
};
let tuple = Tuple::from_packet(&packet).expect("valid TCP packet should return a tuple");
assert_eq!(tuple, expected);
}
#[ip_test]
fn tuple_from_packet_no_body<I: Ip + IpExt + TestIpExt>() {
let packet = FakeIpPacket::<I, NoTransportPacket> {
src_ip: I::SRC_ADDR,
dst_ip: I::DST_ADDR,
body: NoTransportPacket {},
};
let tuple = Tuple::from_packet(&packet);
assert_matches!(tuple, None);
}
#[ip_test]
#[test_case(IpProto::Udp)]
#[test_case(IpProto::Tcp)]
fn connection_from_tuple<I: Ip + IpExt + TestIpExt>(protocol: IpProto) {
let bindings_ctx = FakeBindingsCtx::new();
let original_tuple = Tuple::<I> {
protocol: protocol.into(),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let reply_tuple = original_tuple.clone().invert();
let connection =
ConnectionExclusive::<_, _, ()>::from_tuple(&bindings_ctx, original_tuple.clone());
assert_eq!(&connection.inner.original_tuple, &original_tuple);
assert_eq!(&connection.inner.reply_tuple, &reply_tuple);
}
#[ip_test]
fn connection_make_shared_has_same_underlying_info<I: Ip + IpExt + TestIpExt>() {
let bindings_ctx = FakeBindingsCtx::new();
let original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let reply_tuple = original_tuple.clone().invert();
let mut connection = ConnectionExclusive::from_tuple(&bindings_ctx, original_tuple.clone());
connection.inner.external_data = 1234;
let shared = connection.make_shared();
assert_eq!(shared.inner.original_tuple, original_tuple);
assert_eq!(shared.inner.reply_tuple, reply_tuple);
assert_eq!(shared.inner.external_data, 1234);
}
enum ConnectionKind {
Exclusive,
Shared,
}
#[ip_test]
#[test_case(ConnectionKind::Exclusive)]
#[test_case(ConnectionKind::Shared)]
fn connection_getters<I: Ip + IpExt + TestIpExt>(connection_kind: ConnectionKind) {
let bindings_ctx = FakeBindingsCtx::new();
let original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let reply_tuple = original_tuple.clone().invert();
let mut connection = ConnectionExclusive::from_tuple(&bindings_ctx, original_tuple.clone());
connection.inner.external_data = 1234;
let connection = match connection_kind {
ConnectionKind::Exclusive => Connection::Exclusive(connection),
ConnectionKind::Shared => Connection::Shared(connection.make_shared()),
};
assert_eq!(connection.original_tuple(), &original_tuple);
assert_eq!(connection.reply_tuple(), &reply_tuple);
assert_eq!(connection.external_data(), &1234);
}
#[ip_test]
#[test_case(ConnectionKind::Exclusive)]
#[test_case(ConnectionKind::Shared)]
fn connection_direction<I: Ip + IpExt + TestIpExt>(connection_kind: ConnectionKind) {
let bindings_ctx = FakeBindingsCtx::new();
let original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let reply_tuple = original_tuple.clone().invert();
let mut other_tuple = original_tuple.clone();
other_tuple.src_port_or_id += 1;
let connection =
ConnectionExclusive::<_, _, ()>::from_tuple(&bindings_ctx, original_tuple.clone());
let connection = match connection_kind {
ConnectionKind::Exclusive => Connection::Exclusive(connection),
ConnectionKind::Shared => Connection::Shared(connection.make_shared()),
};
assert_matches!(connection.direction(&original_tuple), Some(ConnectionDirection::Original));
assert_matches!(connection.direction(&reply_tuple), Some(ConnectionDirection::Reply));
assert_matches!(connection.direction(&other_tuple), None);
}
#[ip_test]
#[test_case(ConnectionKind::Exclusive)]
#[test_case(ConnectionKind::Shared)]
fn connection_update<I: Ip + IpExt + TestIpExt>(connection_kind: ConnectionKind) {
let mut bindings_ctx = FakeBindingsCtx::new();
bindings_ctx.set_time_elapsed(Duration::from_secs(12));
let original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let reply_tuple = original_tuple.clone().invert();
let mut other_tuple = original_tuple.clone();
other_tuple.src_port_or_id += 1;
let connection =
ConnectionExclusive::<_, _, ()>::from_tuple(&bindings_ctx, original_tuple.clone());
let mut connection = match connection_kind {
ConnectionKind::Exclusive => Connection::Exclusive(connection),
ConnectionKind::Shared => Connection::Shared(connection.make_shared()),
};
assert_matches!(connection.update(&bindings_ctx, &original_tuple), Ok(()));
let state = connection.state();
assert!(!state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(12));
// Tuple in reply direction should set established to true and obviously
// update last packet time.
bindings_ctx.set_time_elapsed(Duration::from_secs(13));
assert_matches!(connection.update(&bindings_ctx, &reply_tuple), Ok(()));
let state = connection.state();
assert!(state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(13));
// Unrelated connection should return an error and otherwise not touch
// anything.
bindings_ctx.set_time_elapsed(Duration::from_secs(14));
assert_matches!(
connection.update(&bindings_ctx, &other_tuple),
Err(ConnectionUpdateError::NonMatchingTuple)
);
assert!(state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(13));
}
#[ip_test]
fn table_get_exclusive_connection_and_finalize_shared<I: Ip + IpExt + TestIpExt>() {
let mut bindings_ctx = FakeBindingsCtx::new();
bindings_ctx.set_time_elapsed(Duration::from_secs(12));
let table = Table::<_, _, ()>::new();
let packet = FakeIpPacket::<I, _> {
src_ip: I::SRC_ADDR,
dst_ip: I::DST_ADDR,
body: FakeTcpSegment { src_port: I::SRC_PORT, dst_port: I::DST_PORT },
};
let reply_packet = FakeIpPacket::<I, _> {
src_ip: I::DST_ADDR,
dst_ip: I::SRC_ADDR,
body: FakeTcpSegment { src_port: I::DST_PORT, dst_port: I::SRC_PORT },
};
let original_tuple = Tuple::from_packet(&packet).expect("packet should be valid");
let reply_tuple = Tuple::from_packet(&reply_packet).expect("packet should be valid");
let conn = table
.get_connection_for_packet_and_update(&bindings_ctx, &packet)
.expect("packet should be valid");
let state = conn.state();
assert!(!state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(12));
// Since the connection isn't present in the map, we should get a
// freshly-allocated exclusive connection and the map should not have
// been touched.
assert_matches!(conn, Connection::Exclusive(_));
assert!(!table.contains_tuple(&original_tuple));
assert!(!table.contains_tuple(&reply_tuple));
// Once we finalize the connection, it should be present in the map.
assert_matches!(table.finalize_connection(conn), Ok(true));
assert!(table.contains_tuple(&original_tuple));
assert!(table.contains_tuple(&reply_tuple));
// We should now get a shared connection back for packets in either
// direction now that the connection is present in the table.
bindings_ctx.set_time_elapsed(Duration::from_secs(13));
let conn = table.get_connection_for_packet_and_update(&bindings_ctx, &packet);
let conn = assert_matches!(conn, Some(conn) => conn);
let state = conn.state();
assert!(!state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(13));
let conn = assert_matches!(conn, Connection::Shared(conn) => conn);
bindings_ctx.set_time_elapsed(Duration::from_secs(14));
let reply_conn = table.get_connection_for_packet_and_update(&bindings_ctx, &reply_packet);
let reply_conn = assert_matches!(reply_conn, Some(conn) => conn);
let state = reply_conn.state();
assert!(state.established);
assert_eq!(state.last_packet_time.offset, Duration::from_secs(14));
let reply_conn = assert_matches!(reply_conn, Connection::Shared(conn) => conn);
// We should be getting the same connection in both directions.
assert!(Arc::ptr_eq(&conn, &reply_conn));
// Inserting the connection a second time shouldn't change the map.
let conn = table.get_connection_for_packet_and_update(&bindings_ctx, &packet).unwrap();
assert_matches!(table.finalize_connection(conn), Ok(false));
assert!(table.contains_tuple(&original_tuple));
assert!(table.contains_tuple(&reply_tuple));
}
#[ip_test]
fn table_conflict<I: Ip + IpExt + TestIpExt>() {
let bindings_ctx = FakeBindingsCtx::new();
let table = Table::<_, _, ()>::new();
let original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT,
dst_port_or_id: I::DST_PORT,
};
let nated_original_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::SRC_ADDR,
dst_addr: I::DST_ADDR,
src_port_or_id: I::SRC_PORT + 1,
dst_port_or_id: I::DST_PORT + 1,
};
let nated_reply_tuple = Tuple::<I> {
protocol: I::Proto::from(IpProto::Tcp),
src_addr: I::DST_ADDR,
dst_addr: I::SRC_ADDR,
src_port_or_id: I::DST_PORT + 1,
dst_port_or_id: I::SRC_PORT + 1,
};
let conn1 = Connection::Exclusive(ConnectionExclusive::<_, _, ()>::from_tuple(
&bindings_ctx,
original_tuple.clone(),
));
// Fake NAT that ends up allocating the same reply tuple as an existing
// connection.
let mut conn2 =
ConnectionExclusive::<_, _, ()>::from_tuple(&bindings_ctx, original_tuple.clone());
conn2.inner.original_tuple = nated_original_tuple.clone();
let conn2 = Connection::Exclusive(conn2);
// Fake NAT that ends up allocating the same original tuple as an
// existing connection.
let mut conn3 =
ConnectionExclusive::<_, _, ()>::from_tuple(&bindings_ctx, original_tuple.clone());
conn3.inner.reply_tuple = nated_reply_tuple.clone();
let conn3 = Connection::Exclusive(conn3);
assert_matches!(table.finalize_connection(conn1), Ok(true));
assert_matches!(table.finalize_connection(conn2), Err(FinalizeConnectionError::Conflict));
assert_matches!(table.finalize_connection(conn3), Err(FinalizeConnectionError::Conflict));
}
}