use std::mem;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::time::Duration;
use nix::sys::socket::{send, MsgFlags};
use super::bpf::SockFilter;
use super::poll::wait_readable;
use super::transport::{EthTransport, TransportError, MAX_FRAME_LEN};
use super::{ETHERTYPE_PROFINET, ETHERTYPE_VLAN};
use crate::dcp::frame::DCP_MULTICAST_MAC;
impl From<nix::errno::Errno> for TransportError {
fn from(e: nix::errno::Errno) -> Self {
TransportError::Io(std::io::Error::from(e))
}
}
fn is_profinet_frame(buf: &[u8]) -> bool {
if buf.len() < 14 {
return false;
}
let et = u16::from_be_bytes([buf[12], buf[13]]);
if et == ETHERTYPE_PROFINET {
return true;
}
et == ETHERTYPE_VLAN
&& buf.len() >= 18
&& u16::from_be_bytes([buf[16], buf[17]]) == ETHERTYPE_PROFINET
}
pub struct AfPacketTransport {
fd: OwnedFd,
}
impl AfPacketTransport {
pub fn open(ifname: &str) -> Result<Self, TransportError> {
let profinet_protocol = (ETHERTYPE_PROFINET).to_be() as i32;
let raw_fd = unsafe {
libc::socket(
libc::AF_PACKET,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
profinet_protocol,
)
};
if raw_fd < 0 {
return Err(TransportError::Io(std::io::Error::last_os_error()));
}
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
let ifindex = nix::net::if_::if_nametoindex(ifname)?;
let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() };
sll.sll_family = libc::AF_PACKET as u16;
sll.sll_protocol = (ETHERTYPE_PROFINET).to_be();
sll.sll_ifindex = ifindex as libc::c_int;
let ret = unsafe {
libc::bind(
fd.as_raw_fd(),
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(TransportError::Io(std::io::Error::last_os_error()));
}
let mut mreq: libc::packet_mreq = unsafe { mem::zeroed() };
mreq.mr_ifindex = ifindex as libc::c_int;
mreq.mr_type = libc::PACKET_MR_MULTICAST as u16;
mreq.mr_alen = 6;
mreq.mr_address[..6].copy_from_slice(&DCP_MULTICAST_MAC.0);
let ret = unsafe {
libc::setsockopt(
fd.as_raw_fd(),
libc::SOL_PACKET,
libc::PACKET_ADD_MEMBERSHIP,
&mreq as *const libc::packet_mreq as *const libc::c_void,
mem::size_of::<libc::packet_mreq>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(TransportError::Io(std::io::Error::last_os_error()));
}
Ok(Self { fd })
}
pub fn attach_filter(&self, prog: &[SockFilter]) -> Result<(), TransportError> {
let len = u16::try_from(prog.len()).map_err(|_| {
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"BPF program too long",
))
})?;
let fprog = libc::sock_fprog {
len,
filter: prog.as_ptr() as *mut libc::sock_filter,
};
let ret = unsafe {
libc::setsockopt(
self.fd.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_ATTACH_FILTER,
&fprog as *const libc::sock_fprog as *const libc::c_void,
mem::size_of::<libc::sock_fprog>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(TransportError::Io(std::io::Error::last_os_error()));
}
Ok(())
}
}
impl EthTransport for AfPacketTransport {
fn send(&self, frame: &[u8]) -> Result<(), TransportError> {
send(self.fd.as_raw_fd(), frame, MsgFlags::empty())?;
Ok(())
}
fn recv_into(
&self,
buf: &mut [u8],
timeout: Option<Duration>,
) -> Result<Option<usize>, TransportError> {
if buf.len() < MAX_FRAME_LEN {
return Err(TransportError::BufferTooSmall);
}
if !wait_readable(self.fd.as_raw_fd(), timeout)? {
return Ok(None);
}
let mut from: libc::sockaddr_ll = unsafe { mem::zeroed() };
let mut from_len = mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
let n = unsafe {
libc::recvfrom(
self.fd.as_raw_fd(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
libc::MSG_TRUNC,
&mut from as *mut libc::sockaddr_ll as *mut libc::sockaddr,
&mut from_len,
)
};
if n < 0 {
return Err(TransportError::Io(std::io::Error::last_os_error()));
}
let n = n as usize;
if from.sll_pkttype == libc::PACKET_OUTGOING {
return Ok(None);
}
if n > buf.len() {
return Err(TransportError::FrameTooLong { len: n });
}
if is_profinet_frame(&buf[..n]) {
Ok(Some(n))
} else {
Ok(None)
}
}
fn raw_fd(&self) -> Option<RawFd> {
Some(self.fd.as_raw_fd())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_unknown_interface_errors() {
let r = AfPacketTransport::open("nonexistent-iface-xyz");
assert!(r.is_err());
}
#[test]
#[ignore = "requires CAP_NET_RAW + a real interface; run: cargo test -- --ignored"]
fn open_loopback_succeeds() {
let t = AfPacketTransport::open("lo").expect("open lo");
assert_eq!(t.recv(Some(Duration::from_millis(10))).unwrap(), None);
assert!(t.raw_fd().is_some());
}
#[test]
#[ignore = "requires CAP_NET_RAW + a real interface; run: cargo test -- --ignored"]
fn attach_filter_on_loopback_succeeds() {
let t = AfPacketTransport::open("lo").expect("open lo");
t.attach_filter(&super::super::bpf::rt_filter()).unwrap();
}
#[test]
fn accepts_untagged_profinet() {
let mut buf = vec![0u8; 16];
buf[12] = 0x88;
buf[13] = 0x92;
assert!(is_profinet_frame(&buf));
}
#[test]
fn accepts_vlan_tagged_profinet() {
let mut buf = vec![0u8; 20];
buf[12] = 0x81;
buf[13] = 0x00;
buf[16] = 0x88;
buf[17] = 0x92;
assert!(is_profinet_frame(&buf));
}
#[test]
fn rejects_non_profinet() {
let mut buf = vec![0u8; 16];
buf[12] = 0x08;
buf[13] = 0x00;
assert!(!is_profinet_frame(&buf));
}
#[test]
fn rejects_too_short() {
let buf = vec![0u8; 10];
assert!(!is_profinet_frame(&buf));
}
}