use std::net::TcpListener;
use nlink::{
netlink::{Connection, SockDiag},
sockdiag::{
InetFilter, Protocol, SocketFilter, SocketInfo, TcpState, bytecode, filter::FilterKind,
},
};
fn sport_query(port: u16) -> SocketFilter {
SocketFilter {
kind: FilterKind::Inet(InetFilter {
protocol: Protocol::Tcp,
states: TcpState::all_mask(),
local_port: Some(port),
..Default::default()
}),
}
}
#[tokio::test]
async fn bytecode_sport_filter_matches_kernel_side() -> nlink::Result<()> {
require_root!();
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
assert!(
bytecode::for_ports(Some(port), None).is_some(),
"exact sport should compile to bytecode"
);
let conn = Connection::<SockDiag>::new()?;
let filtered = conn.query(&sport_query(port)).await?;
let saw_listener = filtered.iter().any(|s| match s {
SocketInfo::Inet(i) => i.local.port() == port,
_ => false,
});
assert!(
saw_listener,
"kernel-filtered dump should include our :{port} listener"
);
let other_ports = filtered
.iter()
.filter(|s| matches!(s, SocketInfo::Inet(i) if i.local.port() != port))
.count();
assert_eq!(
other_ports, 0,
"bytecode dump leaked {other_ports} sockets with a non-:{port} sport"
);
drop(listener);
Ok(())
}
use nlink::sockdiag::{CcInfo, FilterExpr};
fn expr_query(expr: &str) -> SocketFilter {
SocketFilter {
kind: FilterKind::Inet(InetFilter {
protocol: Protocol::Tcp,
states: TcpState::all_mask(),
expr: Some(FilterExpr::parse(expr).expect("expr parses")),
..Default::default()
}),
}
}
fn inet_ports(sockets: &[SocketInfo]) -> Vec<u16> {
sockets
.iter()
.filter_map(|s| s.as_inet().map(|i| i.local.port()))
.collect()
}
#[tokio::test]
async fn expr_or_of_two_sports_keeps_both_and_leaks_nothing() -> nlink::Result<()> {
require_root!();
let a = TcpListener::bind("127.0.0.1:0").expect("bind a");
let b = TcpListener::bind("127.0.0.1:0").expect("bind b");
let (pa, pb) = (a.local_addr().unwrap().port(), b.local_addr().unwrap().port());
let conn = Connection::<SockDiag>::new()?;
let got = conn
.query(&expr_query(&format!("sport = :{pa} or sport = :{pb}")))
.await?;
let ports = inet_ports(&got);
assert!(ports.contains(&pa), "listener A kept");
assert!(ports.contains(&pb), "listener B kept");
assert!(
ports.iter().all(|p| *p == pa || *p == pb),
"no other sport leaked: {ports:?}"
);
Ok(())
}
#[tokio::test]
async fn expr_not_sport_excludes_target() -> nlink::Result<()> {
require_root!();
let a = TcpListener::bind("127.0.0.1:0").expect("bind a");
let b = TcpListener::bind("127.0.0.1:0").expect("bind b");
let (pa, pb) = (a.local_addr().unwrap().port(), b.local_addr().unwrap().port());
let conn = Connection::<SockDiag>::new()?;
let got = conn.query(&expr_query(&format!("not sport = :{pa}"))).await?;
let ports = inet_ports(&got);
assert!(!ports.contains(&pa), "negated port excluded");
assert!(ports.contains(&pb), "other listener kept");
Ok(())
}
#[tokio::test]
async fn expr_src_hostcond_includes_and_excludes() -> nlink::Result<()> {
require_root!();
let l = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = l.local_addr().unwrap().port();
let conn = Connection::<SockDiag>::new()?;
let got = conn
.query(&expr_query(&format!("src 127.0.0.1 and sport = :{port}")))
.await?;
assert!(inet_ports(&got).contains(&port), "loopback src kept");
let got = conn
.query(&expr_query(&format!("src 203.0.113.1 and sport = :{port}")))
.await?;
assert!(
!inet_ports(&got).contains(&port),
"foreign src excluded the listener"
);
Ok(())
}
#[tokio::test]
async fn expr_v6_hostcond_matches_loopback() -> nlink::Result<()> {
require_root!();
let l = TcpListener::bind("[::1]:0").expect("bind v6");
let port = l.local_addr().unwrap().port();
let conn = Connection::<SockDiag>::new()?;
let got = conn
.query(&expr_query(&format!("src ::1 and sport = :{port}")))
.await?;
assert!(inet_ports(&got).contains(&port), "v6 loopback listener kept");
Ok(())
}
#[tokio::test]
async fn expr_state_hoists_into_header_mask() -> nlink::Result<()> {
require_root!();
let l = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = l.local_addr().unwrap().port();
let conn = Connection::<SockDiag>::new()?;
let got = conn
.query(&expr_query(&format!("state listening and sport = :{port}")))
.await?;
assert!(inet_ports(&got).contains(&port), "listener kept");
let got = conn
.query(&expr_query(&format!("state established and sport = :{port}")))
.await?;
assert!(
!inet_ports(&got).contains(&port),
"listener filtered out by hoisted state mask"
);
Ok(())
}
#[tokio::test]
async fn cc_info_struct_matches_congestion_name() -> nlink::Result<()> {
require_root!();
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().unwrap().port();
let _client = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect");
let (_server, _) = listener.accept().expect("accept");
let conn = Connection::<SockDiag>::new()?;
let filter = SocketFilter {
kind: FilterKind::Inet(InetFilter {
protocol: Protocol::Tcp,
states: TcpState::all_mask(),
expr: Some(FilterExpr::parse(&format!("dport = :{port}")).unwrap()),
extensions: nlink::sockdiag::InetExtension::Cong.mask()
| nlink::sockdiag::InetExtension::VegasInfo.mask()
| nlink::sockdiag::InetExtension::Info.mask(),
..Default::default()
}),
};
let got = conn.query(&filter).await?;
assert!(!got.is_empty(), "established pair visible");
for s in got.iter().filter_map(|s| s.as_inet()) {
match (&s.congestion, &s.cc_info) {
(Some(name), Some(CcInfo::Bbr(_))) => assert_eq!(name, "bbr"),
(Some(name), Some(CcInfo::Dctcp(_))) => assert_eq!(name, "dctcp"),
(Some(name), Some(CcInfo::Vegas(_))) => assert_eq!(name, "vegas"),
_ => {}
}
}
Ok(())
}