use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use super::types::{FlowEndReason, encode_tcp_control_bits};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct FlowRecord {
pub protocol_identifier: u8,
pub source_ipv4_address: Option<Ipv4Addr>,
pub destination_ipv4_address: Option<Ipv4Addr>,
pub source_ipv6_address: Option<Ipv6Addr>,
pub destination_ipv6_address: Option<Ipv6Addr>,
pub source_transport_port: u16,
pub destination_transport_port: u16,
pub octet_delta_count_initiator: u64,
pub octet_delta_count_responder: u64,
pub packet_delta_count_initiator: u64,
pub packet_delta_count_responder: u64,
pub octet_total_count: u64,
pub packet_total_count: u64,
pub flow_start_milliseconds: u64,
pub flow_end_milliseconds: u64,
pub tcp_control_bits_initiator: Option<u16>,
pub tcp_control_bits_responder: Option<u16>,
pub flow_end_reason: Option<FlowEndReason>,
pub source_mac_address: Option<[u8; 6]>,
pub destination_mac_address: Option<[u8; 6]>,
pub ingress_interface: Option<u32>,
pub egress_interface: Option<u32>,
pub application_id: Option<Vec<u8>>,
pub application_name: Option<String>,
pub vlan_id: Option<u16>,
pub layer2_segment_id: Option<u64>,
pub ip_class_of_service: Option<u8>,
pub retransmits_initiator: u64,
pub retransmits_responder: u64,
#[cfg(feature = "tracker")]
pub original_end_reason: Option<crate::EndReason>,
}
impl FlowRecord {
#[cfg(feature = "tracker")]
pub fn from_parts(
stats: &crate::FlowStats,
key: &crate::extract::FiveTupleKey,
end_reason: Option<crate::EndReason>,
) -> Self {
Self::from_key_fields(stats, key, end_reason)
}
#[cfg(feature = "tracker")]
pub fn from_key_fields<K>(
stats: &crate::FlowStats,
key: &K,
end_reason: Option<crate::EndReason>,
) -> Self
where
K: crate::KeyFields + ?Sized,
{
let proto_id = key
.protocol_identifier()
.unwrap_or_else(|| match key.proto_str() {
Some("TCP") => 6,
Some("UDP") => 17,
Some("ICMP") => 1,
Some("ICMPv6") => 58,
Some("SCTP") => 132,
_ => 0,
});
let mut rec = Self {
protocol_identifier: proto_id,
source_transport_port: key.src_port().unwrap_or(0),
destination_transport_port: key.dest_port().unwrap_or(0),
octet_delta_count_initiator: stats.bytes_initiator,
octet_delta_count_responder: stats.bytes_responder,
packet_delta_count_initiator: stats.packets_initiator,
packet_delta_count_responder: stats.packets_responder,
octet_total_count: stats.bytes_initiator + stats.bytes_responder,
packet_total_count: stats.packets_initiator + stats.packets_responder,
flow_start_milliseconds: timestamp_to_unix_ms(stats.started),
flow_end_milliseconds: timestamp_to_unix_ms(stats.last_seen),
flow_end_reason: end_reason.map(FlowEndReason::from),
original_end_reason: end_reason,
retransmits_initiator: stats.retransmits_initiator,
retransmits_responder: stats.retransmits_responder,
..Self::default()
};
if let Some(ip) = key.src_ip() {
match ip {
IpAddr::V4(v4) => rec.source_ipv4_address = Some(v4),
IpAddr::V6(v6) => rec.source_ipv6_address = Some(v6),
}
}
if let Some(ip) = key.dest_ip() {
match ip {
IpAddr::V4(v4) => rec.destination_ipv4_address = Some(v4),
IpAddr::V6(v6) => rec.destination_ipv6_address = Some(v6),
}
}
if let Some(app) = key.app_proto_str()
&& !app.is_empty()
{
rec.application_name = Some(app.to_string());
}
rec
}
#[allow(clippy::too_many_arguments)]
pub fn observe_tcp_flags(
&mut self,
from_initiator: bool,
fin: bool,
syn: bool,
rst: bool,
psh: bool,
ack: bool,
urg: bool,
ece: bool,
cwr: bool,
) {
let bits = encode_tcp_control_bits(fin, syn, rst, psh, ack, urg, ece, cwr);
let slot = if from_initiator {
&mut self.tcp_control_bits_initiator
} else {
&mut self.tcp_control_bits_responder
};
*slot = Some(slot.unwrap_or(0) | bits);
}
pub fn protocol(&self) -> u8 {
self.protocol_identifier
}
pub fn sockets(&self) -> Option<(SocketAddr, SocketAddr)> {
let s_ip = self
.source_ipv4_address
.map(IpAddr::V4)
.or_else(|| self.source_ipv6_address.map(IpAddr::V6))?;
let d_ip = self
.destination_ipv4_address
.map(IpAddr::V4)
.or_else(|| self.destination_ipv6_address.map(IpAddr::V6))?;
Some((
SocketAddr::new(s_ip, self.source_transport_port),
SocketAddr::new(d_ip, self.destination_transport_port),
))
}
}
#[cfg(feature = "tracker")]
#[inline]
fn timestamp_to_unix_ms(ts: crate::Timestamp) -> u64 {
let secs = ts.sec as u64;
let ms_from_ns = (ts.nsec / 1_000_000) as u64;
secs.saturating_mul(1000).saturating_add(ms_from_ns)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_record_is_all_zero_none() {
let r = FlowRecord::default();
assert_eq!(r.protocol_identifier, 0);
assert_eq!(r.octet_total_count, 0);
assert!(r.source_ipv4_address.is_none());
assert!(r.source_ipv6_address.is_none());
assert!(r.flow_end_reason.is_none());
}
#[test]
fn observe_tcp_flags_accumulates_per_direction() {
let mut r = FlowRecord::default();
r.observe_tcp_flags(true, false, true, false, false, false, false, false, false);
assert_eq!(r.tcp_control_bits_initiator, Some(0x02));
assert!(r.tcp_control_bits_responder.is_none());
r.observe_tcp_flags(true, true, false, false, false, true, false, false, false);
assert_eq!(r.tcp_control_bits_initiator, Some(0x13));
r.observe_tcp_flags(false, false, true, false, false, true, false, false, false);
assert_eq!(r.tcp_control_bits_responder, Some(0x12));
}
#[test]
fn sockets_recovers_v4_pair() {
let r = FlowRecord {
source_ipv4_address: Some(Ipv4Addr::new(10, 0, 0, 1)),
destination_ipv4_address: Some(Ipv4Addr::new(10, 0, 0, 2)),
source_transport_port: 1234,
destination_transport_port: 80,
..FlowRecord::default()
};
let (s, d) = r.sockets().unwrap();
assert_eq!(s.ip(), IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
assert_eq!(s.port(), 1234);
assert_eq!(d.ip(), IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
assert_eq!(d.port(), 80);
}
#[test]
fn sockets_recovers_v6_pair() {
let r = FlowRecord {
source_ipv6_address: Some(Ipv6Addr::LOCALHOST),
destination_ipv6_address: Some(Ipv6Addr::UNSPECIFIED),
source_transport_port: 9999,
destination_transport_port: 443,
..FlowRecord::default()
};
let (s, d) = r.sockets().unwrap();
assert!(matches!(s.ip(), IpAddr::V6(_)));
assert!(matches!(d.ip(), IpAddr::V6(_)));
}
#[test]
fn sockets_returns_none_when_no_ip_populated() {
let r = FlowRecord::default();
assert!(r.sockets().is_none());
}
#[test]
fn protocol_accessor_matches_field() {
let r = FlowRecord {
protocol_identifier: 6,
..FlowRecord::default()
};
assert_eq!(r.protocol(), 6);
}
#[cfg(feature = "tracker")]
mod from_parts {
use super::super::*;
use crate::extractor::L4Proto;
use crate::{EndReason, FlowStats, Timestamp};
use std::net::{IpAddr, SocketAddr};
fn make_v4_tcp_key() -> crate::extract::FiveTupleKey {
crate::extract::FiveTupleKey {
proto: L4Proto::Tcp,
a: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 1234),
b: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 80),
}
}
fn make_stats() -> FlowStats {
FlowStats {
bytes_initiator: 1000,
bytes_responder: 5000,
packets_initiator: 10,
packets_responder: 12,
started: Timestamp::new(1, 500_000_000), last_seen: Timestamp::new(5, 0), ..FlowStats::default()
}
}
#[test]
fn from_parts_populates_ipv4_fields() {
let key = make_v4_tcp_key();
let stats = make_stats();
let r = FlowRecord::from_parts(&stats, &key, Some(EndReason::Fin));
assert_eq!(r.protocol_identifier, 6);
assert_eq!(r.source_ipv4_address, Some(Ipv4Addr::new(10, 0, 0, 1)));
assert_eq!(r.destination_ipv4_address, Some(Ipv4Addr::new(10, 0, 0, 2)));
assert!(r.source_ipv6_address.is_none());
assert_eq!(r.source_transport_port, 1234);
assert_eq!(r.destination_transport_port, 80);
}
#[test]
fn from_parts_rolls_up_octets_and_packets() {
let key = make_v4_tcp_key();
let stats = make_stats();
let r = FlowRecord::from_parts(&stats, &key, None);
assert_eq!(r.octet_delta_count_initiator, 1000);
assert_eq!(r.octet_delta_count_responder, 5000);
assert_eq!(r.octet_total_count, 6000);
assert_eq!(r.packet_total_count, 22);
}
#[test]
fn from_parts_converts_timestamps_to_unix_ms() {
let key = make_v4_tcp_key();
let stats = make_stats();
let r = FlowRecord::from_parts(&stats, &key, None);
assert_eq!(r.flow_start_milliseconds, 1500);
assert_eq!(r.flow_end_milliseconds, 5000);
}
#[test]
fn from_parts_maps_end_reason_to_ipfix_canonical() {
let key = make_v4_tcp_key();
let stats = make_stats();
let r = FlowRecord::from_parts(&stats, &key, Some(EndReason::Fin));
assert_eq!(r.flow_end_reason, Some(FlowEndReason::EndOfFlowDetected));
let r = FlowRecord::from_parts(&stats, &key, Some(EndReason::Evicted));
assert_eq!(r.flow_end_reason, Some(FlowEndReason::LackOfResources));
let r = FlowRecord::from_parts(&stats, &key, Some(EndReason::IdleTimeout));
assert_eq!(r.flow_end_reason, Some(FlowEndReason::IdleTimeout));
}
#[test]
fn from_parts_populates_app_name_from_known_port() {
let key = make_v4_tcp_key();
let stats = make_stats();
let r = FlowRecord::from_parts(&stats, &key, None);
assert_eq!(r.application_name.as_deref(), Some("http"));
}
}
}