use std::net::IpAddr;
use std::sync::Arc;
use flowscope::L4Proto;
use super::predicate::{FieldSource, Predicate};
use crate::ctx::Ctx;
use crate::error::Result;
use crate::protocol::{FlowKey, MessageProtocol, Protocol};
#[allow(unused_variables)]
pub trait L7Fields {
fn sni(&self) -> Option<&str> {
None
}
fn http_host(&self) -> Option<&str> {
None
}
fn dns_qname(&self) -> Option<&str> {
None
}
}
#[cfg(feature = "tls")]
impl L7Fields for flowscope::tls::TlsMessage {
fn sni(&self) -> Option<&str> {
match self {
flowscope::tls::TlsMessage::ClientHello(ch) => ch.sni.as_deref(),
_ => None,
}
}
}
#[cfg(feature = "tls")]
impl L7Fields for flowscope::tls::TlsHandshake {
fn sni(&self) -> Option<&str> {
self.sni.as_deref()
}
}
#[cfg(feature = "http")]
impl L7Fields for flowscope::http::HttpMessage {
fn http_host(&self) -> Option<&str> {
match self {
flowscope::http::HttpMessage::Request(req) => req.host(),
flowscope::http::HttpMessage::Response(_) => None,
}
}
}
#[cfg(feature = "dns")]
impl L7Fields for flowscope::dns::DnsMessage {
fn dns_qname(&self) -> Option<&str> {
let questions = match self {
flowscope::dns::DnsMessage::Query(q) | flowscope::dns::DnsMessage::Unanswered(q) => {
&q.questions
}
flowscope::dns::DnsMessage::Response(r) => &r.questions,
_ => return None,
};
questions.first().map(|q| q.name.as_str())
}
}
pub type SessionHandler<P> =
Arc<dyn for<'c> Fn(&<P as Protocol>::Message, &mut Ctx<'c>) -> Result<()> + Send + Sync>;
#[derive(Clone)]
pub struct SessionSubscription<P: MessageProtocol> {
pub(crate) predicate: Predicate,
pub(crate) handler: SessionHandler<P>,
}
impl<P: MessageProtocol> std::fmt::Debug for SessionSubscription<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionSubscription")
.field("predicate", &self.predicate)
.field("handler", &"<fn>")
.finish()
}
}
pub(crate) struct SessionFields<'a, M: L7Fields> {
pub(crate) key: Option<FlowKey>,
pub(crate) msg: &'a M,
}
impl<M: L7Fields> FieldSource for SessionFields<'_, M> {
fn l4proto(&self) -> Option<L4Proto> {
self.key.map(|k| k.proto)
}
fn src_port(&self) -> Option<u16> {
self.key.map(|k| k.a.port())
}
fn dst_port(&self) -> Option<u16> {
self.key.map(|k| k.b.port())
}
fn src_ip(&self) -> Option<IpAddr> {
self.key.map(|k| k.a.ip())
}
fn dst_ip(&self) -> Option<IpAddr> {
self.key.map(|k| k.b.ip())
}
fn sni(&self) -> Option<&str> {
self.msg.sni()
}
fn http_host(&self) -> Option<&str> {
self.msg.http_host()
}
fn dns_qname(&self) -> Option<&str> {
self.msg.dns_qname()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::subscription::predicate::{Atom, Glob, Predicate};
struct MockMsg {
sni: Option<String>,
}
impl L7Fields for MockMsg {
fn sni(&self) -> Option<&str> {
self.sni.as_deref()
}
}
fn tls_key() -> FlowKey {
flowscope::extract::FiveTupleKey {
proto: L4Proto::Tcp,
a: "10.0.0.1:54321".parse().unwrap(),
b: "10.0.0.2:443".parse().unwrap(),
}
}
#[test]
fn session_fields_combine_5tuple_and_l7() {
let msg = MockMsg {
sni: Some("login.bank.example".into()),
};
let fields = SessionFields {
key: Some(tls_key()),
msg: &msg,
};
assert_eq!(fields.l4proto(), Some(L4Proto::Tcp));
assert_eq!(fields.dst_port(), Some(443));
assert_eq!(fields.sni(), Some("login.bank.example"));
let p = Predicate::Atom(Atom::Proto(L4Proto::Tcp))
.and(Predicate::Atom(Atom::DstPort(443)))
.and(Predicate::Atom(Atom::SniGlob(Glob::new("*.bank.example"))));
assert!(p.eval(&fields));
assert!(!Predicate::Atom(Atom::SniGlob(Glob::new("*.gov"))).eval(&fields));
}
#[test]
fn missing_l7_field_does_not_match() {
let msg = MockMsg { sni: None };
let fields = SessionFields {
key: Some(tls_key()),
msg: &msg,
};
assert!(!Predicate::Atom(Atom::SniGlob(Glob::new("*"))).eval(&fields));
}
}