pub const MAX_DNS_QUERIES: usize = 16;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct L7Summary {
pub app_proto: Option<&'static str>,
pub server_name: Option<String>,
pub ja3: Option<String>,
pub ja4: Option<String>,
pub tls_version: Option<u16>,
pub tls_cipher: Option<u16>,
pub user_agent: Option<String>,
pub http_method: Option<String>,
pub http_uri: Option<String>,
pub dns_queries: Vec<String>,
}
impl L7Summary {
pub fn is_empty(&self) -> bool {
*self == L7Summary::default()
}
#[cfg(feature = "tls")]
pub fn observe_tls(&mut self, hs: &crate::tls::TlsHandshake) {
self.app_proto = Some("tls");
if let Some(sni) = &hs.sni {
self.server_name = Some(sni.clone());
}
if let Some(ja3) = &hs.ja3 {
self.ja3 = Some(ja3.clone());
}
if let Some(ja4) = &hs.ja4 {
self.ja4 = Some(ja4.clone());
}
if let Some(v) = hs.version {
self.tls_version = Some(v.to_raw());
}
if let Some(c) = hs.cipher_suite {
self.tls_cipher = Some(c);
}
}
#[cfg(feature = "http")]
pub fn observe_http(&mut self, msg: &crate::http::HttpMessage) {
if let crate::http::HttpMessage::Request(req) = msg {
self.app_proto = Some("http");
if self.server_name.is_none()
&& let Some(host) = req.host()
{
self.server_name = Some(host.to_owned());
}
if let Some(ua) = req.user_agent() {
self.user_agent = Some(ua.to_owned());
}
if let Some(m) = req.method_str() {
self.http_method = Some(m.to_owned());
}
if let Some(p) = req.path_str() {
self.http_uri = Some(p.to_owned());
}
}
}
#[cfg(feature = "dns")]
pub fn observe_dns_query(&mut self, q: &crate::dns::DnsQuery) {
self.app_proto = Some("dns");
self.push_dns_names(q.questions.iter().map(|q| q.name.as_str()));
}
#[cfg(feature = "dns")]
pub fn observe_dns_response(&mut self, r: &crate::dns::DnsResponse) {
self.app_proto = Some("dns");
self.push_dns_names(r.questions.iter().map(|q| q.name.as_str()));
}
#[cfg(feature = "dns")]
fn push_dns_names<'a>(&mut self, names: impl Iterator<Item = &'a str>) {
for name in names {
if self.dns_queries.len() >= MAX_DNS_QUERIES {
break;
}
if !name.is_empty() && !self.dns_queries.iter().any(|q| q == name) {
self.dns_queries.push(name.to_owned());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
assert!(L7Summary::default().is_empty());
}
#[cfg(feature = "tls")]
#[test]
fn observe_tls_extracts_fields() {
use crate::tls::{TlsHandshake, TlsVersion};
let hs = TlsHandshake {
sni: Some("example.com".into()),
ja4: Some("t13d1516h2_x_y".into()),
version: Some(TlsVersion::Tls1_2),
cipher_suite: Some(0x1301),
..Default::default()
};
let mut s = L7Summary::default();
s.observe_tls(&hs);
assert!(!s.is_empty());
assert_eq!(s.app_proto, Some("tls"));
assert_eq!(s.server_name.as_deref(), Some("example.com"));
assert_eq!(s.ja4.as_deref(), Some("t13d1516h2_x_y"));
assert_eq!(s.tls_version, Some(0x0303));
assert_eq!(s.tls_cipher, Some(0x1301));
}
#[cfg(feature = "dns")]
#[test]
fn dns_queries_are_bounded_and_deduped() {
use crate::dns::{DnsFlags, DnsQuery, DnsQuestion};
let mk = |name: &str| DnsQuery {
transaction_id: 0,
flags: DnsFlags(0),
questions: vec![DnsQuestion {
name: name.to_owned(),
qtype: 1,
qclass: 1,
}],
timestamp: crate::Timestamp::default(),
};
let mut s = L7Summary::default();
for i in 0..(MAX_DNS_QUERIES + 5) {
s.observe_dns_query(&mk(&format!("h{i}.example.com")));
}
assert_eq!(s.dns_queries.len(), MAX_DNS_QUERIES);
s.observe_dns_query(&mk("h0.example.com"));
assert_eq!(s.dns_queries.len(), MAX_DNS_QUERIES);
}
}