use std::io::Write;
use std::net::{IpAddr, SocketAddr};
use flowscope::event::EndReason;
use crate::export::{FlowExporter, FlowRecord};
const IPFIX_VERSION: u16 = 10;
const SET_ID_TEMPLATE: u16 = 2;
const TEMPLATE_ID_V4: u16 = 256;
const TEMPLATE_ID_V6: u16 = 257;
const IE_OCTET_DELTA_COUNT: u16 = 1;
const IE_PACKET_DELTA_COUNT: u16 = 2;
const IE_PROTOCOL_IDENTIFIER: u16 = 4;
const IE_SOURCE_TRANSPORT_PORT: u16 = 7;
const IE_SOURCE_IPV4: u16 = 8;
const IE_DESTINATION_TRANSPORT_PORT: u16 = 11;
const IE_DESTINATION_IPV4: u16 = 12;
const IE_FLOW_END_REASON: u16 = 136;
const IE_FLOW_START_MS: u16 = 152;
const IE_FLOW_END_MS: u16 = 153;
const IE_SOURCE_IPV6: u16 = 27;
const IE_DESTINATION_IPV6: u16 = 28;
const V4_FIELDS: &[(u16, u16)] = &[
(IE_PROTOCOL_IDENTIFIER, 1),
(IE_SOURCE_IPV4, 4),
(IE_DESTINATION_IPV4, 4),
(IE_SOURCE_TRANSPORT_PORT, 2),
(IE_DESTINATION_TRANSPORT_PORT, 2),
(IE_PACKET_DELTA_COUNT, 8),
(IE_OCTET_DELTA_COUNT, 8),
(IE_FLOW_START_MS, 8),
(IE_FLOW_END_MS, 8),
(IE_FLOW_END_REASON, 1),
];
const V6_FIELDS: &[(u16, u16)] = &[
(IE_PROTOCOL_IDENTIFIER, 1),
(IE_SOURCE_IPV6, 16),
(IE_DESTINATION_IPV6, 16),
(IE_SOURCE_TRANSPORT_PORT, 2),
(IE_DESTINATION_TRANSPORT_PORT, 2),
(IE_PACKET_DELTA_COUNT, 8),
(IE_OCTET_DELTA_COUNT, 8),
(IE_FLOW_START_MS, 8),
(IE_FLOW_END_MS, 8),
(IE_FLOW_END_REASON, 1),
];
fn flow_end_reason(reason: Option<EndReason>) -> u8 {
match reason {
None => 0x02, Some(EndReason::IdleTimeout) => 0x01, Some(EndReason::Fin | EndReason::Rst | EndReason::ParserDone) => 0x03, Some(EndReason::BufferOverflow) => 0x05, Some(_) => 0x04,
}
}
fn protocol_number(proto: flowscope::L4Proto) -> u8 {
use flowscope::L4Proto::*;
match proto {
Tcp => 6,
Udp => 17,
Icmp => 1,
IcmpV6 => 58,
_ => 0,
}
}
pub struct IpfixExporter<W: Write + Send> {
writer: W,
observation_domain_id: u32,
records_sent: u32,
resend_every: u32,
since_template: u32,
template_sent: bool,
scratch: Vec<u8>,
}
impl<W: Write + Send> IpfixExporter<W> {
pub fn new(writer: W, observation_domain_id: u32) -> Self {
Self {
writer,
observation_domain_id,
records_sent: 0,
resend_every: 0,
since_template: 0,
template_sent: false,
scratch: Vec::with_capacity(128),
}
}
pub fn resend_templates_every(mut self, n: u32) -> Self {
self.resend_every = n;
self
}
pub fn into_inner(self) -> W {
self.writer
}
fn want_templates(&self) -> bool {
!self.template_sent || (self.resend_every != 0 && self.since_template >= self.resend_every)
}
}
fn put_u16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_be_bytes());
}
fn put_u64(out: &mut Vec<u8>, v: u64) {
out.extend_from_slice(&v.to_be_bytes());
}
fn encode_template_set(out: &mut Vec<u8>) {
let start = out.len();
put_u16(out, SET_ID_TEMPLATE);
put_u16(out, 0); encode_template_record(out, TEMPLATE_ID_V4, V4_FIELDS);
encode_template_record(out, TEMPLATE_ID_V6, V6_FIELDS);
let len = (out.len() - start) as u16;
out[start + 2..start + 4].copy_from_slice(&len.to_be_bytes());
}
fn encode_template_record(out: &mut Vec<u8>, template_id: u16, fields: &[(u16, u16)]) {
put_u16(out, template_id);
put_u16(out, fields.len() as u16);
for &(ie, len) in fields {
put_u16(out, ie); put_u16(out, len);
}
}
fn encode_data_set(out: &mut Vec<u8>, record: &FlowRecord) {
let template_id = match record.a.ip() {
IpAddr::V4(_) => TEMPLATE_ID_V4,
IpAddr::V6(_) => TEMPLATE_ID_V6,
};
let start = out.len();
put_u16(out, template_id);
put_u16(out, 0); encode_data_record(out, record);
let len = (out.len() - start) as u16;
out[start + 2..start + 4].copy_from_slice(&len.to_be_bytes());
}
fn encode_data_record(out: &mut Vec<u8>, record: &FlowRecord) {
out.push(protocol_number(record.proto));
put_addr(out, record.a);
put_addr(out, record.b);
put_u16(out, record.a.port());
put_u16(out, record.b.port());
put_u64(out, record.total_packets());
put_u64(out, record.total_bytes());
put_u64(out, ts_millis(record.start));
put_u64(out, ts_millis(record.end));
out.push(flow_end_reason(record.reason));
}
fn put_addr(out: &mut Vec<u8>, addr: SocketAddr) {
match addr.ip() {
IpAddr::V4(v4) => out.extend_from_slice(&v4.octets()),
IpAddr::V6(v6) => out.extend_from_slice(&v6.octets()),
}
}
fn ts_millis(ts: flowscope::Timestamp) -> u64 {
(ts.to_unix_f64() * 1000.0) as u64
}
impl<W: Write + Send> FlowExporter for IpfixExporter<W> {
fn export(&mut self, record: &FlowRecord) {
let include_templates = self.want_templates();
self.scratch.clear();
if include_templates {
encode_template_set(&mut self.scratch);
}
encode_data_set(&mut self.scratch, record);
let total_len = (16 + self.scratch.len()) as u16;
let mut header = [0u8; 16];
header[0..2].copy_from_slice(&IPFIX_VERSION.to_be_bytes());
header[2..4].copy_from_slice(&total_len.to_be_bytes());
let export_time = (ts_millis(record.end) / 1000) as u32;
header[4..8].copy_from_slice(&export_time.to_be_bytes());
header[8..12].copy_from_slice(&self.records_sent.to_be_bytes());
header[12..16].copy_from_slice(&self.observation_domain_id.to_be_bytes());
if self
.writer
.write_all(&header)
.and_then(|_| self.writer.write_all(&self.scratch))
.is_ok()
{
self.records_sent = self.records_sent.wrapping_add(1);
if include_templates {
self.template_sent = true;
self.since_template = 0;
} else {
self.since_template = self.since_template.wrapping_add(1);
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::FlowKey;
use flowscope::event::FlowStats;
use flowscope::{L4Proto, Timestamp};
use std::net::{Ipv4Addr, Ipv6Addr};
fn v4_record() -> FlowRecord {
let key = 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),
};
let mut s = FlowStats::default();
s.packets_initiator = 5;
s.packets_responder = 5;
s.bytes_initiator = 400;
s.bytes_responder = 600;
s.started = Timestamp::from_unix_f64(1000.0);
s.last_seen = Timestamp::from_unix_f64(1002.0);
FlowRecord::from_ended(&key, &s, EndReason::Fin)
}
#[test]
fn end_reason_and_protocol_maps() {
assert_eq!(flow_end_reason(Some(EndReason::Fin)), 3);
assert_eq!(flow_end_reason(Some(EndReason::IdleTimeout)), 1);
assert_eq!(flow_end_reason(Some(EndReason::BufferOverflow)), 5);
assert_eq!(flow_end_reason(Some(EndReason::Evicted)), 4);
assert_eq!(flow_end_reason(None), 2); assert_eq!(protocol_number(L4Proto::Tcp), 6);
assert_eq!(protocol_number(L4Proto::Udp), 17);
}
#[test]
fn v4_data_record_is_46_bytes_with_expected_fields() {
let mut out = Vec::new();
encode_data_record(&mut out, &v4_record());
assert_eq!(out.len(), 46, "record = {out:?}");
assert_eq!(out[0], 6); assert_eq!(&out[1..5], &[10, 0, 0, 1]); assert_eq!(&out[5..9], &[10, 0, 0, 2]); assert_eq!(&out[9..11], &1234u16.to_be_bytes()); assert_eq!(&out[11..13], &80u16.to_be_bytes()); assert_eq!(&out[13..21], &10u64.to_be_bytes()); assert_eq!(&out[21..29], &1000u64.to_be_bytes()); assert_eq!(out[45], 3); }
#[test]
fn template_set_declares_both_templates() {
let mut out = Vec::new();
encode_template_set(&mut out);
assert_eq!(&out[0..2], &SET_ID_TEMPLATE.to_be_bytes());
let set_len = u16::from_be_bytes([out[2], out[3]]) as usize;
assert_eq!(set_len, out.len());
assert_eq!(&out[4..6], &TEMPLATE_ID_V4.to_be_bytes());
assert_eq!(&out[6..8], &(V4_FIELDS.len() as u16).to_be_bytes());
}
#[test]
fn first_message_has_header_template_and_data() {
let mut exporter = IpfixExporter::new(Vec::<u8>::new(), 42);
exporter.export(&v4_record());
let bytes = exporter.into_inner();
assert_eq!(&bytes[0..2], &IPFIX_VERSION.to_be_bytes());
let msg_len = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
assert_eq!(msg_len, bytes.len());
assert_eq!(&bytes[8..12], &0u32.to_be_bytes()); assert_eq!(&bytes[12..16], &42u32.to_be_bytes());
assert_eq!(&bytes[16..18], &SET_ID_TEMPLATE.to_be_bytes());
}
#[test]
fn second_message_omits_templates_and_bumps_sequence() {
let mut exporter = IpfixExporter::new(Vec::<u8>::new(), 7);
exporter.export(&v4_record());
let first_len = exporter.scratch.len();
exporter.export(&v4_record());
let bytes = exporter.into_inner();
let first_msg_len = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
let second = &bytes[first_msg_len..];
assert_eq!(&second[0..2], &IPFIX_VERSION.to_be_bytes());
assert_eq!(&second[8..12], &1u32.to_be_bytes()); assert_eq!(&second[16..18], &TEMPLATE_ID_V4.to_be_bytes()); assert!(first_len > 0);
}
#[test]
fn v6_record_uses_the_v6_template() {
let key = FlowKey {
proto: L4Proto::Udp,
a: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5353),
b: SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 53),
};
let s = FlowStats::default();
let rec = FlowRecord::from_ended(&key, &s, EndReason::IdleTimeout);
let mut out = Vec::new();
encode_data_set(&mut out, &rec);
assert_eq!(&out[0..2], &TEMPLATE_ID_V6.to_be_bytes());
}
}