use std::{
pin::Pin,
task::{Context, Poll},
};
use futures::{Stream, channel::mpsc::UnboundedReceiver};
use genetlink::message::RawGenlMessage;
use netlink_packet_core::{NetlinkMessage, NetlinkPayload};
use netlink_packet_l2tp::{L2tpCmd, L2tpMessage};
use netlink_proto::sys::SocketAddr;
use crate::{SessionInfo, TunnelInfo, netlink};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[non_exhaustive]
pub enum L2tpEvent {
TunnelCreated(TunnelInfo),
TunnelModified(TunnelInfo),
TunnelDeleted(TunnelInfo),
SessionCreated(SessionInfo),
SessionModified(SessionInfo),
SessionDeleted(SessionInfo),
}
#[derive(Debug)]
pub struct Events {
receiver: UnboundedReceiver<(NetlinkMessage<RawGenlMessage>, SocketAddr)>,
connection: tokio::task::JoinHandle<()>,
family_id: u16,
}
impl Events {
pub(crate) fn new(
receiver: UnboundedReceiver<(
NetlinkMessage<RawGenlMessage>,
SocketAddr,
)>,
connection: tokio::task::JoinHandle<()>,
family_id: u16,
) -> Self {
Self {
receiver,
connection,
family_id,
}
}
}
impl Stream for Events {
type Item = crate::Result<L2tpEvent>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
loop {
match Pin::new(&mut self.receiver).poll_next(cx) {
Poll::Ready(Some((message, _))) => {
match decode(message, self.family_id) {
Ok(Some(event)) => return Poll::Ready(Some(Ok(event))),
Ok(None) => continue,
Err(err) => return Poll::Ready(Some(Err(err))),
}
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
impl Drop for Events {
fn drop(&mut self) {
self.connection.abort();
}
}
pub(crate) fn decode(
message: NetlinkMessage<RawGenlMessage>,
family_id: u16,
) -> crate::Result<Option<L2tpEvent>> {
if matches!(message.payload, NetlinkPayload::Overrun(_)) {
return Err(std::io::Error::from_raw_os_error(libc::ENOBUFS).into());
}
if message.header.message_type != family_id {
return Ok(None);
}
let NetlinkPayload::InnerMessage(raw) = message.payload else {
return Ok(None);
};
let genl = raw.parse_into_genlmsg::<L2tpMessage>()?;
let attrs = &genl.payload.attributes;
let event = match genl.payload.cmd {
L2tpCmd::TunnelCreate => {
L2tpEvent::TunnelCreated(netlink::decode_tunnel_info(attrs)?)
}
L2tpCmd::TunnelModify => {
L2tpEvent::TunnelModified(netlink::decode_tunnel_info(attrs)?)
}
L2tpCmd::TunnelDelete => {
L2tpEvent::TunnelDeleted(netlink::decode_tunnel_info(attrs)?)
}
L2tpCmd::SessionCreate => {
L2tpEvent::SessionCreated(netlink::decode_session_info(attrs)?)
}
L2tpCmd::SessionModify => {
L2tpEvent::SessionModified(netlink::decode_session_info(attrs)?)
}
L2tpCmd::SessionDelete => {
L2tpEvent::SessionDeleted(netlink::decode_session_info(attrs)?)
}
_ => return Ok(None),
};
Ok(Some(event))
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use futures::StreamExt;
use netlink_packet_core::{DoneMessage, NLMSG_OVERRUN, NetlinkHeader};
use netlink_packet_generic::GenlMessage;
use netlink_packet_l2tp::{L2tpAttribute, L2tpEncapType, L2tpPwType};
use super::*;
use crate::{SessionId, TunnelId};
const FAMILY_ID: u16 = 42;
fn notification(
family_id: u16,
cmd: L2tpCmd,
attributes: Vec<L2tpAttribute>,
) -> NetlinkMessage<RawGenlMessage> {
let genl = GenlMessage::from_payload(L2tpMessage { cmd, attributes });
let mut header = NetlinkHeader::default();
header.message_type = family_id;
NetlinkMessage::new(
header,
NetlinkPayload::InnerMessage(RawGenlMessage::from_genlmsg(genl)),
)
}
#[test]
fn notifications_decode_into_events() {
let tunnel = vec![
L2tpAttribute::ConnId(1),
L2tpAttribute::PeerConnId(2),
L2tpAttribute::ProtoVersion(3),
L2tpAttribute::EncapType(L2tpEncapType::Ip),
L2tpAttribute::IpSaddr(Ipv4Addr::LOCALHOST),
L2tpAttribute::IpDaddr(Ipv4Addr::LOCALHOST),
];
let event = decode(
notification(FAMILY_ID, L2tpCmd::TunnelDelete, tunnel),
FAMILY_ID,
)
.unwrap()
.unwrap();
let L2tpEvent::TunnelDeleted(info) = event else {
panic!("expected a tunnel deletion");
};
assert_eq!(info.tunnel_id, TunnelId(1));
let session = vec![
L2tpAttribute::ConnId(1),
L2tpAttribute::PeerConnId(2),
L2tpAttribute::SessionId(3),
L2tpAttribute::PeerSessionId(4),
L2tpAttribute::PwType(L2tpPwType::Eth),
];
let event = decode(
notification(FAMILY_ID, L2tpCmd::SessionCreate, session),
FAMILY_ID,
)
.unwrap()
.unwrap();
let L2tpEvent::SessionCreated(info) = event else {
panic!("expected a session creation");
};
assert_eq!(info.session_id, SessionId(3));
assert_eq!(
decode(notification(FAMILY_ID, L2tpCmd::Noop, vec![]), FAMILY_ID)
.unwrap(),
None
);
let done = NetlinkMessage::new(
NetlinkHeader::default(),
NetlinkPayload::Done(DoneMessage::default()),
);
assert_eq!(decode(done, FAMILY_ID).unwrap(), None);
let broken = notification(FAMILY_ID, L2tpCmd::TunnelCreate, vec![]);
assert!(matches!(
decode(broken, FAMILY_ID),
Err(crate::Error::UnexpectedReply(_))
));
}
#[test]
fn notifications_from_other_families_are_skipped() {
let foreign =
notification(FAMILY_ID + 1, L2tpCmd::TunnelCreate, vec![]);
assert_eq!(decode(foreign, FAMILY_ID).unwrap(), None);
}
#[tokio::test]
async fn stream_reports_overruns_and_continues_with_notifications() {
let (sender, receiver) = futures::channel::mpsc::unbounded();
let connection = tokio::spawn(std::future::pending());
let mut events = Events::new(receiver, connection, FAMILY_ID);
let mut header = NetlinkHeader::default();
header.message_type = NLMSG_OVERRUN;
sender
.unbounded_send((
NetlinkMessage::new(
header,
NetlinkPayload::Overrun(Vec::new()),
),
SocketAddr::new(0, 0),
))
.unwrap();
sender
.unbounded_send((
notification(
FAMILY_ID,
L2tpCmd::TunnelCreate,
vec![
L2tpAttribute::ConnId(1),
L2tpAttribute::PeerConnId(2),
L2tpAttribute::ProtoVersion(3),
L2tpAttribute::EncapType(L2tpEncapType::Ip),
L2tpAttribute::IpSaddr(Ipv4Addr::LOCALHOST),
L2tpAttribute::IpDaddr(Ipv4Addr::LOCALHOST),
],
),
SocketAddr::new(0, 0),
))
.unwrap();
drop(sender);
let error = events.next().await.unwrap().unwrap_err();
assert_eq!(error.raw_os_error(), Some(libc::ENOBUFS));
assert!(matches!(
events.next().await.unwrap().unwrap(),
L2tpEvent::TunnelCreated(info) if info.tunnel_id == TunnelId(1)
));
assert!(events.next().await.is_none());
}
}