use std::{
borrow::Cow,
ffi::{CStr, OsStr, OsString},
fmt,
os::unix::ffi::{OsStrExt, OsStringExt},
};
pub const IFNAMSIZ: usize = libc::IFNAMSIZ;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TunnelId(
pub u32,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId(
pub u32,
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cookie(Vec<u8>);
impl Cookie {
pub fn none() -> Self {
Self(Vec::new())
}
pub fn try_from_bytes(b: Vec<u8>) -> crate::Result<Self> {
match b.len() {
0 | 4 | 8 => Ok(Self(b)),
n => Err(crate::Error::InvalidCookieLength(n)),
}
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum IfNameError {
#[error("name is empty")]
Empty,
#[error("name is longer than {} bytes", IfName::MAX_LEN)]
TooLong,
#[error("`.` and `..` are reserved")]
Reserved,
#[error("name contains {:?}", char::from(*.0))]
InvalidByte(u8),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IfName([u8; IFNAMSIZ]);
impl IfName {
pub const MAX_LEN: usize = IFNAMSIZ - 1;
pub fn new(name: impl AsRef<[u8]>) -> crate::Result<Self> {
let b = name.as_ref();
if b.is_empty() {
return Err(IfNameError::Empty.into());
}
if b.len() > Self::MAX_LEN {
return Err(IfNameError::TooLong.into());
}
if b == b"." || b == b".." {
return Err(IfNameError::Reserved.into());
}
if let Some(&c) = b.iter().find(|&&c| {
matches!(c, 0 | b'/' | b':' | 0x0b) || c.is_ascii_whitespace()
}) {
return Err(IfNameError::InvalidByte(c).into());
}
let mut array = [0; IFNAMSIZ];
array[..b.len()].copy_from_slice(b);
Ok(Self(array))
}
fn len(&self) -> usize {
self.0.iter().position(|&c| c == 0).unwrap_or(IFNAMSIZ)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0[..self.len()]
}
pub fn as_array(&self) -> &[u8; IFNAMSIZ] {
&self.0
}
pub fn to_str(&self) -> Option<&str> {
std::str::from_utf8(self.as_bytes()).ok()
}
pub fn to_string_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(self.as_bytes())
}
pub fn as_os_str(&self) -> &OsStr {
OsStr::from_bytes(self.as_bytes())
}
pub fn as_c_str(&self) -> &CStr {
CStr::from_bytes_until_nul(&self.0)
.expect("interface names are NUL-terminated by construction")
}
pub fn index(&self) -> crate::Result<u32> {
let index = unsafe { libc::if_nametoindex(self.as_c_str().as_ptr()) };
if index == 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(index)
}
pub fn from_index(index: u32) -> crate::Result<Self> {
let mut buf = [0 as libc::c_char; IFNAMSIZ];
let ptr = unsafe { libc::if_indextoname(index, buf.as_mut_ptr()) };
if ptr.is_null() {
return Err(std::io::Error::last_os_error().into());
}
Self::try_from(buf.map(|c| c as u8))
}
}
impl fmt::Debug for IfName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IfName({:?})", self.as_os_str())
}
}
impl fmt::Display for IfName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.to_string_lossy(), f)
}
}
impl std::str::FromStr for IfName {
type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Self> {
Self::new(s)
}
}
impl TryFrom<&str> for IfName {
type Error = crate::Error;
fn try_from(s: &str) -> crate::Result<Self> {
Self::new(s)
}
}
impl TryFrom<String> for IfName {
type Error = crate::Error;
fn try_from(s: String) -> crate::Result<Self> {
Self::new(s)
}
}
impl TryFrom<&[u8]> for IfName {
type Error = crate::Error;
fn try_from(bytes: &[u8]) -> crate::Result<Self> {
Self::new(bytes)
}
}
impl TryFrom<Vec<u8>> for IfName {
type Error = crate::Error;
fn try_from(bytes: Vec<u8>) -> crate::Result<Self> {
Self::new(bytes)
}
}
impl TryFrom<&OsStr> for IfName {
type Error = crate::Error;
fn try_from(s: &OsStr) -> crate::Result<Self> {
Self::new(s.as_bytes())
}
}
impl TryFrom<&CStr> for IfName {
type Error = crate::Error;
fn try_from(s: &CStr) -> crate::Result<Self> {
Self::new(s.to_bytes())
}
}
impl TryFrom<[u8; IFNAMSIZ]> for IfName {
type Error = crate::Error;
fn try_from(array: [u8; IFNAMSIZ]) -> crate::Result<Self> {
let len = array
.iter()
.position(|&c| c == 0)
.ok_or(IfNameError::TooLong)?;
Self::new(&array[..len])
}
}
impl AsRef<[u8]> for IfName {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl AsRef<OsStr> for IfName {
fn as_ref(&self) -> &OsStr {
self.as_os_str()
}
}
impl AsRef<CStr> for IfName {
fn as_ref(&self) -> &CStr {
self.as_c_str()
}
}
impl From<IfName> for [u8; IFNAMSIZ] {
fn from(name: IfName) -> Self {
name.0
}
}
impl From<IfName> for Vec<u8> {
fn from(name: IfName) -> Self {
name.as_bytes().to_vec()
}
}
impl From<IfName> for OsString {
fn from(name: IfName) -> Self {
OsString::from_vec(name.as_bytes().to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UdpEndpoint {
V4(std::net::SocketAddrV4),
V6(std::net::SocketAddrV6),
}
impl UdpEndpoint {
pub fn ip_version(&self) -> u8 {
match self {
Self::V4(_) => 4,
Self::V6(_) => 6,
}
}
pub fn is_link_local(&self) -> bool {
match self {
Self::V4(_) => false,
Self::V6(addr) => addr.ip().is_unicast_link_local(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IpEndpoint {
V4(std::net::Ipv4Addr),
V6 {
addr: std::net::Ipv6Addr,
scope: Option<IfName>,
},
}
impl IpEndpoint {
pub fn v6(addr: std::net::Ipv6Addr) -> Self {
Self::V6 { addr, scope: None }
}
pub fn v6_scoped(addr: std::net::Ipv6Addr, scope: IfName) -> Self {
Self::V6 {
addr,
scope: Some(scope),
}
}
pub fn ip_version(&self) -> u8 {
match self {
Self::V4(_) => 4,
Self::V6 { .. } => 6,
}
}
pub fn scope(&self) -> Option<&IfName> {
match self {
Self::V4(_) => None,
Self::V6 { scope, .. } => scope.as_ref(),
}
}
pub fn is_link_local(&self) -> bool {
match self {
Self::V4(_) => false,
Self::V6 { addr, .. } => addr.is_unicast_link_local(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Encapsulation {
Udp {
local: UdpEndpoint,
remote: UdpEndpoint,
udp_csum: bool,
udp_zero_csum6_tx: bool,
udp_zero_csum6_rx: bool,
},
Ip {
local: IpEndpoint,
remote: IpEndpoint,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PseudowireType {
Eth,
EthVlan,
Ppp,
PppAc,
Ip,
None,
Other(u16),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum L2SpecType {
#[default]
None,
Default,
Other(u8),
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
use super::*;
#[test]
fn cookie_accepts_valid_lengths() {
let c0 = Cookie::try_from_bytes(vec![]).unwrap();
let c4 = Cookie::try_from_bytes(vec![1, 2, 3, 4]).unwrap();
let c8 = Cookie::try_from_bytes(vec![1, 2, 3, 4, 5, 6, 7, 8]).unwrap();
assert!(c0.as_bytes().is_empty());
assert_eq!(c4.as_bytes(), &[1, 2, 3, 4]);
assert_eq!(c8.as_bytes(), &[1, 2, 3, 4, 5, 6, 7, 8]);
}
#[test]
fn cookie_rejects_invalid_length() {
let err = Cookie::try_from_bytes(vec![1, 2, 3]).unwrap_err();
match err {
crate::Error::InvalidCookieLength(3) => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn ifname_validates_constraints() {
let ok = IfName::new("l2tp0").unwrap();
assert_eq!(ok.to_str(), Some("l2tp0"));
assert_eq!(ok.to_string(), "l2tp0");
assert_eq!(ok.as_bytes(), b"l2tp0");
assert_eq!(ok.as_array(), b"l2tp0\0\0\0\0\0\0\0\0\0\0\0");
assert_eq!(format!("{ok:?}"), "IfName(\"l2tp0\")");
assert!(matches!(
IfName::new(""),
Err(crate::Error::InvalidIfName(_))
));
assert!(matches!(
IfName::new("abc/def"),
Err(crate::Error::InvalidIfName(_))
));
assert!(matches!(
IfName::new("ab\0cd"),
Err(crate::Error::InvalidIfName(_))
));
assert!(matches!(
IfName::new("0123456789abcdef"),
Err(crate::Error::InvalidIfName(_))
));
for bad in [".", "..", "eth:0", "a b", "a\tb", "a\x0bb"] {
assert!(
matches!(IfName::new(bad), Err(crate::Error::InvalidIfName(_))),
"{bad:?} should be rejected"
);
}
assert!(IfName::new("a.b").is_ok());
assert!(IfName::new("0123456789abcde").is_ok());
}
#[test]
fn ifname_errors_name_the_violated_rule() {
use crate::Error::InvalidIfName;
assert!(matches!(
IfName::new(""),
Err(InvalidIfName(IfNameError::Empty))
));
assert!(matches!(
IfName::new("0123456789abcdef"),
Err(InvalidIfName(IfNameError::TooLong))
));
assert!(matches!(
IfName::new(".."),
Err(InvalidIfName(IfNameError::Reserved))
));
assert!(matches!(
IfName::new("eth:0"),
Err(InvalidIfName(IfNameError::InvalidByte(b':')))
));
assert!(matches!(
IfName::new("a\0b"),
Err(InvalidIfName(IfNameError::InvalidByte(0)))
));
assert_eq!(
IfName::new("a b").unwrap_err().to_string(),
"invalid interface name: name contains ' '"
);
assert_eq!(
IfName::new("").unwrap_err().to_string(),
"invalid interface name: name is empty"
);
assert_eq!(
IfName::new("0123456789abcdef").unwrap_err().to_string(),
"invalid interface name: name is longer than 15 bytes"
);
}
#[test]
fn ifname_conversions_validate() {
let name: IfName = "eth0".parse().unwrap();
assert_eq!(IfName::try_from("eth0").unwrap(), name);
assert_eq!(IfName::try_from(String::from("eth0")).unwrap(), name);
assert_eq!(IfName::try_from(&b"eth0"[..]).unwrap(), name);
assert_eq!(IfName::try_from(b"eth0".to_vec()).unwrap(), name);
assert_eq!(IfName::try_from(OsStr::new("eth0")).unwrap(), name);
assert_eq!(IfName::try_from(c"eth0").unwrap(), name);
let array: [u8; IFNAMSIZ] = name.into();
assert_eq!(&array, b"eth0\0\0\0\0\0\0\0\0\0\0\0\0");
assert_eq!(IfName::try_from(array).unwrap(), name);
assert_eq!(AsRef::<[u8]>::as_ref(&name), b"eth0");
assert_eq!(AsRef::<OsStr>::as_ref(&name), OsStr::new("eth0"));
assert_eq!(AsRef::<CStr>::as_ref(&name), c"eth0");
assert_eq!(name.as_c_str(), c"eth0");
assert_eq!(Vec::<u8>::from(name), b"eth0");
assert_eq!(OsString::from(name), OsString::from("eth0"));
assert!(matches!(
"eth:0".parse::<IfName>(),
Err(crate::Error::InvalidIfName(_))
));
assert!(matches!(
IfName::try_from(String::from("")),
Err(crate::Error::InvalidIfName(_))
));
assert!(matches!(
IfName::try_from([b'a'; IFNAMSIZ]),
Err(crate::Error::InvalidIfName(IfNameError::TooLong))
));
let mut names = [name, IfName::new("br0").unwrap()];
names.sort();
assert_eq!(names[0].to_str(), Some("br0"));
let set: std::collections::HashSet<IfName> = names.into();
assert!(set.contains(&name));
}
#[test]
fn ifname_accepts_bytes_that_are_not_utf8() {
let raw = IfName::new(b"eth\xff0").unwrap();
assert_eq!(raw.as_bytes(), b"eth\xff0");
assert_eq!(raw.to_str(), None);
assert_eq!(raw.to_string_lossy(), "eth\u{fffd}0");
assert_eq!(raw.to_string(), "eth\u{fffd}0");
assert_eq!(format!("{raw:?}"), "IfName(\"eth\\xFF0\")");
assert_eq!(
IfName::try_from(OsStr::from_bytes(b"eth\xff0")).unwrap(),
raw
);
assert_eq!(Vec::<u8>::from(raw), b"eth\xff0");
assert_ne!(raw, IfName::new("eth0").unwrap());
}
#[test]
fn endpoint_ip_versions_are_reported() {
let udp4 =
UdpEndpoint::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1701));
let udp6 =
UdpEndpoint::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 1701, 0, 0));
let ip4 = IpEndpoint::V4(Ipv4Addr::LOCALHOST);
let ip6 = IpEndpoint::v6(Ipv6Addr::LOCALHOST);
assert_eq!(udp4.ip_version(), 4);
assert_eq!(udp6.ip_version(), 6);
assert_eq!(ip4.ip_version(), 4);
assert_eq!(ip6.ip_version(), 6);
}
#[test]
fn link_local_endpoints_are_detected_and_scoped() {
let link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
let eth0 = IfName::new("eth0").unwrap();
let unscoped = IpEndpoint::v6(link_local);
assert!(unscoped.is_link_local());
assert_eq!(unscoped.scope(), None);
let scoped = IpEndpoint::v6_scoped(link_local, eth0);
assert!(scoped.is_link_local());
assert_eq!(scoped.scope(), Some(ð0));
assert_eq!(scoped.ip_version(), 6);
assert!(!IpEndpoint::v6(Ipv6Addr::LOCALHOST).is_link_local());
assert!(!IpEndpoint::V4(Ipv4Addr::new(169, 254, 1, 1)).is_link_local());
assert_eq!(IpEndpoint::V4(Ipv4Addr::LOCALHOST).scope(), None);
assert!(
UdpEndpoint::V6(SocketAddrV6::new(link_local, 1701, 0, 0))
.is_link_local()
);
assert!(
!UdpEndpoint::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
1701,
0,
0
))
.is_link_local()
);
assert!(
!UdpEndpoint::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 1701))
.is_link_local()
);
}
#[test]
fn ifname_index_round_trips_through_the_kernel() {
let lo = IfName::new("lo").unwrap();
let index = lo.index().unwrap();
assert_ne!(index, 0);
assert_eq!(IfName::from_index(index).unwrap(), lo);
let missing = IfName::new("l2tp-no-such0").unwrap();
let err = missing.index().unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::ENODEV));
let err = IfName::from_index(u32::MAX).unwrap_err();
assert!(matches!(err, crate::Error::Io(_)));
}
#[test]
fn l2spec_default_is_none() {
assert_eq!(L2SpecType::default(), L2SpecType::None);
}
}