use std::net::SocketAddr;
use flowscope::event::{EndReason, FlowStats};
use flowscope::{L4Proto, Timestamp};
use crate::protocol::FlowKey;
#[cfg(feature = "ipfix")]
pub mod ipfix;
#[cfg(feature = "ipfix")]
pub use ipfix::IpfixExporter;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FlowRecord {
pub proto: L4Proto,
pub a: SocketAddr,
pub b: SocketAddr,
pub packets_initiator: u64,
pub packets_responder: u64,
pub bytes_initiator: u64,
pub bytes_responder: u64,
pub start: Timestamp,
pub end: Timestamp,
pub reason: Option<EndReason>,
}
impl FlowRecord {
pub(crate) fn from_ended(key: &FlowKey, stats: &FlowStats, reason: EndReason) -> Self {
Self {
proto: key.proto,
a: key.a,
b: key.b,
packets_initiator: stats.packets_initiator,
packets_responder: stats.packets_responder,
bytes_initiator: stats.bytes_initiator,
bytes_responder: stats.bytes_responder,
start: stats.started,
end: stats.last_seen,
reason: Some(reason),
}
}
pub(crate) fn from_active(key: &FlowKey, stats: &FlowStats) -> Self {
Self {
proto: key.proto,
a: key.a,
b: key.b,
packets_initiator: stats.packets_initiator,
packets_responder: stats.packets_responder,
bytes_initiator: stats.bytes_initiator,
bytes_responder: stats.bytes_responder,
start: stats.started,
end: stats.last_seen,
reason: None,
}
}
#[inline]
pub fn is_ongoing(&self) -> bool {
self.reason.is_none()
}
#[inline]
pub fn total_packets(&self) -> u64 {
self.packets_initiator + self.packets_responder
}
#[inline]
pub fn total_bytes(&self) -> u64 {
self.bytes_initiator + self.bytes_responder
}
#[inline]
pub fn duration(&self) -> std::time::Duration {
self.end.saturating_sub(self.start)
}
}
pub trait FlowExporter: Send {
fn export(&mut self, record: &FlowRecord);
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<F: FnMut(&FlowRecord) + Send> FlowExporter for F {
fn export(&mut self, record: &FlowRecord) {
self(record)
}
}
#[cfg(feature = "serde")]
pub struct JsonFlowExporter<W: std::io::Write + Send> {
writer: W,
}
#[cfg(feature = "serde")]
impl<W: std::io::Write + Send> JsonFlowExporter<W> {
pub fn new(writer: W) -> Self {
Self { writer }
}
pub fn into_inner(self) -> W {
self.writer
}
}
#[cfg(feature = "serde")]
impl JsonFlowExporter<std::io::Stdout> {
pub fn stdout() -> Self {
Self::new(std::io::stdout())
}
}
#[cfg(feature = "serde")]
impl<W: std::io::Write + Send> FlowExporter for JsonFlowExporter<W> {
fn export(&mut self, record: &FlowRecord) {
match serde_json::to_string(record) {
Ok(line) => {
let _ = writeln!(self.writer, "{line}");
}
Err(e) => eprintln!("JsonFlowExporter: serialize failed: {e}"),
}
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
fn key() -> FlowKey {
FlowKey {
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 stats() -> FlowStats {
let mut s = FlowStats::default();
s.packets_initiator = 10;
s.packets_responder = 8;
s.bytes_initiator = 1500;
s.bytes_responder = 12000;
s.started = Timestamp::from_unix_f64(1000.0);
s.last_seen = Timestamp::from_unix_f64(1002.5);
s
}
#[test]
fn from_ended_maps_key_stats_and_reason() {
let r = FlowRecord::from_ended(&key(), &stats(), EndReason::Fin);
assert_eq!(r.proto, L4Proto::Tcp);
assert_eq!(r.a.port(), 1234);
assert_eq!(r.b.port(), 80);
assert_eq!(r.packets_initiator, 10);
assert_eq!(r.bytes_responder, 12000);
assert_eq!(r.reason, Some(EndReason::Fin));
assert!(!r.is_ongoing());
assert_eq!(r.total_packets(), 18);
assert_eq!(r.total_bytes(), 13500);
assert_eq!(r.duration(), std::time::Duration::from_millis(2500));
}
#[test]
fn from_active_is_ongoing_with_no_reason() {
let r = FlowRecord::from_active(&key(), &stats());
assert!(r.is_ongoing());
assert_eq!(r.reason, None);
assert_eq!(r.packets_initiator, 10);
assert_eq!(r.bytes_responder, 12000);
assert_eq!(r.total_packets(), 18);
}
#[test]
fn fnmut_is_a_flow_exporter() {
let mut count = 0u32;
let mut exporter = |_r: &FlowRecord| count += 1;
let r = FlowRecord::from_ended(&key(), &stats(), EndReason::Fin);
FlowExporter::export(&mut exporter, &r);
FlowExporter::export(&mut exporter, &r);
assert_eq!(count, 2);
}
#[cfg(feature = "serde")]
#[test]
fn json_flow_exporter_writes_one_line_with_core_fields() {
let mut exporter = JsonFlowExporter::new(Vec::<u8>::new());
let r = FlowRecord::from_ended(&key(), &stats(), EndReason::Fin);
exporter.export(&r);
let out = String::from_utf8(exporter.into_inner()).unwrap();
assert_eq!(out.lines().count(), 1);
assert!(out.contains("\"packets_initiator\":10"), "out = {out}");
assert!(out.contains("\"bytes_responder\":12000"), "out = {out}");
}
}