use chrono::{DateTime, TimeZone, Utc};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EbpfEvent {
Connect(ConnectEvent),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Protocol {
Tcp,
Udp,
}
#[derive(Debug, Clone)]
pub struct ConnectEvent {
pub protocol: Protocol,
pub tgid: u32,
pub pid: u32,
pub comm: String,
pub saddr: IpAddr,
pub daddr: IpAddr,
pub sport: u16,
pub dport: u16,
pub timestamp: DateTime<Utc>,
}
#[allow(dead_code)]
impl ConnectEvent {
pub(crate) fn decode_v4(
raw: &crate::wire::ConnectV4Event,
boot_time: DateTime<Utc>,
protocol: Protocol,
) -> Self {
let saddr = IpAddr::V4(Ipv4Addr::from(raw.saddr.to_ne_bytes()));
let daddr = IpAddr::V4(Ipv4Addr::from(raw.daddr.to_ne_bytes()));
let sport = u16::from_be_bytes(raw.sport.to_ne_bytes());
let dport = u16::from_be_bytes(raw.dport.to_ne_bytes());
let timestamp = boot_time + chrono::Duration::nanoseconds(raw.timestamp_ns as i64);
Self {
protocol,
tgid: raw.tgid,
pid: raw.pid,
comm: decode_comm(&raw.comm),
saddr,
daddr,
sport,
dport,
timestamp,
}
}
pub(crate) fn decode_v6(
raw: &crate::wire::ConnectV6Event,
boot_time: DateTime<Utc>,
protocol: Protocol,
) -> Self {
let saddr = Ipv6Addr::from(raw.saddr).to_canonical();
let daddr = Ipv6Addr::from(raw.daddr).to_canonical();
let sport = u16::from_be_bytes(raw.sport.to_ne_bytes());
let dport = u16::from_be_bytes(raw.dport.to_ne_bytes());
let timestamp = boot_time + chrono::Duration::nanoseconds(raw.timestamp_ns as i64);
Self {
protocol,
tgid: raw.tgid,
pid: raw.pid,
comm: decode_comm(&raw.comm),
saddr,
daddr,
sport,
dport,
timestamp,
}
}
}
#[allow(dead_code)]
fn decode_comm(comm: &[u8]) -> String {
let end = comm.iter().position(|&b| b == 0).unwrap_or(comm.len());
String::from_utf8_lossy(&comm[..end]).into_owned()
}
#[allow(dead_code)]
pub(crate) fn estimate_boot_time() -> DateTime<Utc> {
#[cfg(target_os = "linux")]
{
if let Ok(uptime_str) = std::fs::read_to_string("/proc/uptime") {
if let Some(secs_str) = uptime_str.split_whitespace().next() {
if let Ok(secs) = secs_str.parse::<f64>() {
let now = Utc::now();
let boot_secs = secs as i64;
let boot_nanos = ((secs.fract()) * 1e9) as i64;
return now
- chrono::Duration::seconds(boot_secs)
- chrono::Duration::nanoseconds(boot_nanos);
}
}
}
}
Utc.timestamp_opt(0, 0).single().unwrap_or_else(Utc::now)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::{ConnectV4Event, ConnectV6Event, EventKind, COMM_LEN};
#[test]
fn decode_v4_converts_addresses_and_ports_to_host_order() {
let mut comm = [0u8; COMM_LEN];
comm[..4].copy_from_slice(b"curl");
let raw = ConnectV4Event {
kind: EventKind::TcpV4Connect,
_pad0: [0; 3],
tgid: 1234,
pid: 1235,
saddr: u32::from_ne_bytes([192, 168, 1, 10]),
daddr: u32::from_ne_bytes([1, 1, 1, 1]),
sport: 0,
dport: u16::from_ne_bytes([0x01, 0xBB]),
comm,
timestamp_ns: 1_000_000_000,
};
let boot = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
let ev = ConnectEvent::decode_v4(&raw, boot, Protocol::Tcp);
assert_eq!(ev.protocol, Protocol::Tcp);
assert_eq!(ev.pid, 1235);
assert_eq!(ev.tgid, 1234);
assert_eq!(ev.comm, "curl");
assert_eq!(ev.saddr, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)));
assert_eq!(ev.daddr, IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)));
assert_eq!(ev.dport, 443);
assert_eq!(ev.timestamp.timestamp(), 1_700_000_001);
}
#[test]
fn decode_handles_short_comm_without_panicking() {
let raw = ConnectV4Event::empty();
let ev = ConnectEvent::decode_v4(&raw, Utc::now(), Protocol::Tcp);
assert_eq!(ev.comm, "");
}
#[test]
fn decode_tags_udp_protocol_from_the_datagram_connect_kinds() {
let v4 = ConnectEvent::decode_v4(&ConnectV4Event::empty(), Utc::now(), Protocol::Udp);
assert_eq!(v4.protocol, Protocol::Udp);
let v6 = ConnectEvent::decode_v6(&ConnectV6Event::empty(), Utc::now(), Protocol::Udp);
assert_eq!(v6.protocol, Protocol::Udp);
}
#[test]
fn decode_v6_converts_address_and_port() {
let mut comm = [0u8; COMM_LEN];
comm[..4].copy_from_slice(b"curl");
let mut raw = ConnectV6Event::empty();
raw.tgid = 42;
raw.pid = 43;
raw.comm = comm;
raw.daddr = [
0x26, 0x06, 0x47, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x68, 0x10, 0x85, 0xe5,
];
raw.dport = u16::from_ne_bytes([0x01, 0xBB]); raw.timestamp_ns = 2_000_000_000;
let boot = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
let ev = ConnectEvent::decode_v6(&raw, boot, Protocol::Tcp);
assert_eq!(ev.pid, 43);
assert_eq!(ev.comm, "curl");
assert_eq!(
ev.daddr,
"2606:4700::6810:85e5".parse::<IpAddr>().unwrap()
);
assert_eq!(ev.dport, 443);
assert!(ev.saddr.is_unspecified());
assert_eq!(ev.timestamp.timestamp(), 1_700_000_002);
}
#[test]
fn decode_v6_canonicalises_v4_mapped_destinations() {
let mut raw = ConnectV6Event::empty();
raw.daddr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 93, 184, 216, 34];
raw.dport = u16::from_ne_bytes([0x00, 0x50]);
let ev = ConnectEvent::decode_v6(&raw, Utc.timestamp_opt(0, 0).unwrap(), Protocol::Tcp);
assert_eq!(ev.daddr, IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)));
assert_eq!(ev.dport, 80);
}
}