use std::time::Duration;
use crate::correlate::RollingRate;
pub(crate) const BW_WINDOW: Duration = Duration::from_secs(10);
pub(crate) const BW_BUCKET: Duration = Duration::from_secs(1);
pub(crate) struct BandwidthState(pub(crate) RollingRate<&'static str, u64>);
impl BandwidthState {
pub(crate) fn new(window: Duration, bucket: Duration) -> Self {
Self(RollingRate::new_unbounded(window, bucket))
}
}
impl Default for BandwidthState {
fn default() -> Self {
Self::new(BW_WINDOW, BW_BUCKET)
}
}
pub struct BandwidthReport<'a> {
pub(crate) rate: &'a RollingRate<&'static str, u64>,
pub(crate) now: flowscope::Timestamp,
}
impl BandwidthReport<'_> {
pub fn top(&self, n: usize) -> Vec<(&'static str, f64)> {
self.rate.top_k(n, self.now)
}
pub fn rate(&self, app: &'static str) -> f64 {
self.rate.rate(&app, self.now)
}
pub fn total(&self) -> f64 {
self.rate.snapshot(self.now).map(|(_, r)| r).sum()
}
pub fn app_count(&self) -> usize {
self.rate.len(self.now)
}
pub fn to_snapshot(&self, n: usize) -> BandwidthSnapshot {
BandwidthSnapshot {
apps: self
.top(n)
.into_iter()
.map(|(app, bytes_per_sec)| BandwidthEntry { app, bytes_per_sec })
.collect(),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BandwidthEntry {
pub app: &'static str,
pub bytes_per_sec: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct BandwidthSnapshot {
pub apps: Vec<BandwidthEntry>,
}
impl crate::report::Report for BandwidthSnapshot {
const NAME: &'static str = "bandwidth";
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use flowscope::{FlowSide, L4Proto, Timestamp};
use super::*;
use crate::ctx::{Ctx, SourceIdx};
use crate::monitor::Monitor;
use crate::protocol::event_typed::FlowPacket;
fn key(proto: L4Proto, dport: u16) -> flowscope::extract::FiveTupleKey {
flowscope::extract::FiveTupleKey::new(
proto,
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 40000),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), dport),
)
}
#[test]
fn report_view_top_rate_total() {
let now = Timestamp::new(1_000, 0);
let mut rate = RollingRate::<&'static str, u64>::new_unbounded(BW_WINDOW, BW_BUCKET);
rate.record("http", 300, now);
rate.record("dns", 100, now);
let report = BandwidthReport { rate: &rate, now };
assert_eq!(report.top(10).first().map(|(a, _)| *a), Some("http"));
assert_eq!(report.app_count(), 2);
assert!((report.rate("http") - 30.0).abs() < 1e-6); assert!((report.rate("dns") - 10.0).abs() < 1e-6);
assert!((report.total() - 40.0).abs() < 1e-6);
assert_eq!(report.rate("ssh"), 0.0); }
#[test]
fn builder_recorder_buckets_by_app_then_ctx_reads_back() {
let monitor = Monitor::builder()
.interface("lo")
.bandwidth_by_app()
.build()
.expect("build");
let Monitor {
mut dispatcher,
mut state_map,
mut counters,
mut flow_states,
mut sink,
..
} = monitor;
let now = Timestamp::new(2_000, 0);
let mut feed = |proto: L4Proto, dport: u16, len: usize| {
let mut ctx = Ctx::new(
None,
now,
SourceIdx(0),
&mut state_map,
sink.as_mut(),
&mut counters,
&mut flow_states,
);
let pkt = FlowPacket::new(
proto,
key(proto, dport),
FlowSide::Initiator,
len,
None,
now,
);
dispatcher.dispatch::<FlowPacket>(&pkt, &mut ctx).unwrap();
};
for _ in 0..3 {
feed(L4Proto::Tcp, 80, 100); }
for _ in 0..2 {
feed(L4Proto::Udp, 53, 50); }
let ctx = Ctx::new(
None,
now,
SourceIdx(0),
&mut state_map,
sink.as_mut(),
&mut counters,
&mut flow_states,
);
let report = ctx.bandwidth().expect("bandwidth registered");
assert_eq!(report.top(10).first().map(|(a, _)| *a), Some("http"));
assert!((report.rate("http") - 30.0).abs() < 1e-6); assert!((report.rate("dns") - 10.0).abs() < 1e-6); assert_eq!(report.app_count(), 2);
}
}