diff --git a/Cargo.toml b/Cargo.toml index 1e4c176..bc86d21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ features = [ [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] objc2-core-foundation = "0.3" objc2-system-configuration = { version = "0.3", default-features = false, features = ["SCNetworkConfiguration"] } -plist = "1.8" +plist = "1.10" [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6", optional = true } diff --git a/src/interface/interface.rs b/src/interface/interface.rs index 6e3b43c..13a2671 100644 --- a/src/interface/interface.rs +++ b/src/interface/interface.rs @@ -199,6 +199,11 @@ impl Interface { /// Returns `true` when the interface appears to be backed by physical hardware. pub fn is_physical(&self) -> bool { use crate::net::db::oui; + + if self.if_type.is_known_virtual() { + return false; + } + super::flags::is_physical_interface(&self) && !oui::is_virtual_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) && !oui::is_known_loopback_mac(&self.mac_addr.unwrap_or(MacAddr::zero())) diff --git a/src/interface/types.rs b/src/interface/types.rs index 3e189b8..c21df4f 100644 --- a/src/interface/types.rs +++ b/src/interface/types.rs @@ -80,6 +80,52 @@ pub enum InterfaceType { } impl InterfaceType { + #[cfg(any(target_vendor = "apple", target_os = "android", test))] + pub(crate) fn should_replace_with(self, candidate: InterfaceType) -> bool { + if candidate == self { + return false; + } + + match (self, candidate) { + (InterfaceType::Unknown, candidate) => candidate != InterfaceType::Unknown, + (InterfaceType::UnknownWithValue(_), candidate) => !matches!( + candidate, + InterfaceType::Unknown | InterfaceType::UnknownWithValue(_) + ), + (InterfaceType::Ethernet, candidate) => { + matches!( + candidate, + InterfaceType::Loopback + | InterfaceType::Wireless80211 + | InterfaceType::Tunnel + | InterfaceType::Wwan + | InterfaceType::Wwanpp + | InterfaceType::Wwanpp2 + | InterfaceType::Bridge + | InterfaceType::PeerToPeerWireless + | InterfaceType::ProprietaryVirtual + ) + } + (InterfaceType::Wwan, candidate) => { + matches!(candidate, InterfaceType::Wwanpp | InterfaceType::Wwanpp2) + } + _ => false, + } + } + + pub(crate) fn is_known_virtual(self) -> bool { + matches!( + self, + InterfaceType::Loopback + | InterfaceType::Ppp + | InterfaceType::Slip + | InterfaceType::ProprietaryVirtual + | InterfaceType::Tunnel + | InterfaceType::Bridge + | InterfaceType::PeerToPeerWireless + ) + } + /// Returns the native numeric type identifier for the current target platform. /// /// For variants that have no direct mapping on the current platform, this method returns @@ -268,3 +314,34 @@ impl TryFrom for InterfaceType { } } } + +#[cfg(test)] +mod tests { + use super::InterfaceType; + + #[test] + fn replaces_ambiguous_types_with_more_specific_types() { + assert!(InterfaceType::Unknown.should_replace_with(InterfaceType::Ethernet)); + assert!(InterfaceType::Ethernet.should_replace_with(InterfaceType::Wireless80211)); + assert!(InterfaceType::Ethernet.should_replace_with(InterfaceType::Bridge)); + assert!(InterfaceType::Wwan.should_replace_with(InterfaceType::Wwanpp)); + } + + #[test] + fn preserves_specific_types_from_generic_candidates() { + assert!(!InterfaceType::Bridge.should_replace_with(InterfaceType::Ethernet)); + assert!( + !InterfaceType::PeerToPeerWireless.should_replace_with(InterfaceType::Wireless80211) + ); + assert!(!InterfaceType::Tunnel.should_replace_with(InterfaceType::Unknown)); + } + + #[test] + fn identifies_known_virtual_types() { + assert!(InterfaceType::Bridge.is_known_virtual()); + assert!(InterfaceType::Tunnel.is_known_virtual()); + assert!(InterfaceType::PeerToPeerWireless.is_known_virtual()); + assert!(!InterfaceType::Ethernet.is_known_virtual()); + assert!(!InterfaceType::Wireless80211.is_known_virtual()); + } +} diff --git a/src/os/android/interface.rs b/src/os/android/interface.rs index a6558ca..08510e1 100644 --- a/src/os/android/interface.rs +++ b/src/os/android/interface.rs @@ -63,38 +63,10 @@ fn type_is_ambiguous(if_type: InterfaceType) -> bool { ) } -fn type_is_more_specific(current: InterfaceType, candidate: InterfaceType) -> bool { - if candidate == current { - return false; - } - - match (current, candidate) { - (InterfaceType::Unknown, _) | (InterfaceType::UnknownWithValue(_), _) => true, - (InterfaceType::Ethernet, candidate) => { - matches!( - candidate, - InterfaceType::Loopback - | InterfaceType::Wireless80211 - | InterfaceType::Tunnel - | InterfaceType::Wwan - | InterfaceType::Wwanpp - | InterfaceType::Wwanpp2 - | InterfaceType::Bridge - | InterfaceType::PeerToPeerWireless - | InterfaceType::ProprietaryVirtual - ) - } - (InterfaceType::Wwan, candidate) => { - matches!(candidate, InterfaceType::Wwanpp | InterfaceType::Wwanpp2) - } - _ => false, - } -} - #[cfg(feature = "android-extra")] fn finalize_interface(iface: &mut Interface, extras: Option<&super::api::InterfaceExtras>) { if let Some(sysfs_type) = super::sysfs::get_interface_type(&iface.name) { - if type_is_more_specific(iface.if_type, sysfs_type) { + if iface.if_type.should_replace_with(sysfs_type) { iface.if_type = sysfs_type; } } @@ -152,7 +124,7 @@ fn finalize_interface(iface: &mut Interface, extras: Option<&super::api::Interfa #[cfg(not(feature = "android-extra"))] fn finalize_interface(iface: &mut Interface) { if let Some(sysfs_type) = super::sysfs::get_interface_type(&iface.name) { - if type_is_more_specific(iface.if_type, sysfs_type) { + if iface.if_type.should_replace_with(sysfs_type) { iface.if_type = sysfs_type; } } @@ -295,7 +267,7 @@ pub fn interfaces() -> Vec { #[cfg(test)] mod tests { - use super::{calc_v6_scope_id, push_ipv4, push_ipv6, type_is_ambiguous, type_is_more_specific}; + use super::{calc_v6_scope_id, push_ipv4, push_ipv6, type_is_ambiguous}; use crate::interface::ipv6_addr_flags::Ipv6AddrFlags; use crate::interface::types::InterfaceType; use crate::ipnet::{Ipv4Net, Ipv6Net}; @@ -371,22 +343,6 @@ mod tests { assert!(addr_flags[1].permanent); } - #[test] - fn prefers_more_specific_sysfs_types() { - assert!(type_is_more_specific( - InterfaceType::Ethernet, - InterfaceType::Wireless80211 - )); - assert!(type_is_more_specific( - InterfaceType::Wwan, - InterfaceType::Wwanpp - )); - assert!(!type_is_more_specific( - InterfaceType::Tunnel, - InterfaceType::Wireless80211 - )); - } - #[test] fn marks_ambiguous_types() { assert!(type_is_ambiguous(InterfaceType::Unknown)); diff --git a/src/os/android/netlink.rs b/src/os/android/netlink.rs index 3365e99..670f4ad 100644 --- a/src/os/android/netlink.rs +++ b/src/os/android/netlink.rs @@ -1,20 +1,15 @@ use crate::interface::types::InterfaceType; use crate::stats::counters::InterfaceStats; -use netlink_packet_core::{NLM_F_DUMP, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload}; +use netlink_packet_core::NetlinkPayload; use netlink_packet_route::{ RouteNetlinkMessage, address::{AddressAttribute, AddressFlags, AddressMessage}, link::{LinkAttribute, LinkMessage}, }; -use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE}; -use std::io::ErrorKind; +use netlink_sys::{Socket, protocols::NETLINK_ROUTE}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::SystemTime; -use std::{ - collections::HashMap, - io, thread, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, io}; #[cfg(feature = "gateway")] use netlink_packet_route::AddressFamily; @@ -24,133 +19,25 @@ use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, Neig use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage}; const SEQ_BASE: u32 = 0x6E_64_65_76; // "ndev" -const RECV_BUFSZ: usize = 1 << 20; // 1MB -const RECV_TIMEOUT: Duration = Duration::from_secs(2); -const NLMSG_ALIGNTO: usize = 4; -const MIN_NLMSG_HEADER_LEN: usize = 16; - -#[inline] -fn nlmsg_align(n: usize) -> usize { - (n + NLMSG_ALIGNTO - 1) & !(NLMSG_ALIGNTO - 1) -} fn open_route_socket() -> io::Result { let sock = Socket::new(NETLINK_ROUTE) .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink open: {e}")))?; // On Android 11+, bind is denied by SELinux //sock.bind_auto().map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bind_auto: {e}")))?; - sock.set_non_blocking(true).ok(); + crate::os::linux::netlink_io::set_non_blocking(&sock)?; Ok(sock) } -fn send_dump(sock: &mut Socket, msg: RouteNetlinkMessage, seq: u32) -> io::Result<()> { - let mut nl = NetlinkMessage::from(msg); - nl.header.flags = NLM_F_REQUEST | NLM_F_DUMP; - nl.header.sequence_number = seq; - nl.header.port_number = 0; - - // Finalize to set length - nl.finalize(); - - let blen = nl.buffer_len(); - if blen < MIN_NLMSG_HEADER_LEN { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("netlink message too short: buffer_len={}", blen), - )); - } - - let mut buf = vec![0; blen]; - nl.serialize(&mut buf); - - let kernel = SocketAddr::new(0, 0); - sock.send_to(&buf, &kernel, 0) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink send: {e}")))?; - Ok(()) -} - -fn recv_multi( - sock: &mut Socket, - expect_seq: u32, -) -> io::Result>> { - let mut out = Vec::new(); - let mut buf = vec![0u8; RECV_BUFSZ]; - let kernel = SocketAddr::new(0, 0); - let deadline = Instant::now() + RECV_TIMEOUT; - - loop { - match sock.recv_from(&mut &mut buf[..], 0) { - Ok((size, from)) => { - let _ = from == kernel; - let mut offset = 0usize; - - while offset < size { - if size - offset < MIN_NLMSG_HEADER_LEN { - break; - } - - let bytes = &buf[offset..size]; - - let msg = - NetlinkMessage::::deserialize(bytes).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("deserialize: {e:?}"), - ) - })?; - - let consumed = msg.header.length as usize; - if consumed < MIN_NLMSG_HEADER_LEN || offset + consumed > size { - break; - } - - if msg.header.sequence_number != expect_seq { - offset += nlmsg_align(consumed); - continue; - } - - match &msg.payload { - NetlinkPayload::Done(_) => { - return Ok(out); - } - NetlinkPayload::Error(e) => { - if let Some(code) = e.code { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("netlink error: code={}", code), - )); - } - // code==None: possibly ACK ... ignore - } - NetlinkPayload::Noop | NetlinkPayload::Overrun(_) => { /* skip */ } - _ => out.push(msg), - } - - // Align to 4-byte boundary - offset += nlmsg_align(consumed); - } - } - Err(e) if e.kind() == ErrorKind::WouldBlock => { - if Instant::now() >= deadline { - // timeout - return Ok(out); - } - thread::sleep(Duration::from_millis(5)); - } - Err(e) => return Err(e), - } - } -} - pub fn dump_links() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x01; - send_dump( + crate::os::linux::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetLink(LinkMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = crate::os::linux::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewLink(link)) = m.payload { @@ -163,12 +50,12 @@ pub fn dump_links() -> io::Result> { pub fn dump_addrs() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x02; - send_dump( + crate::os::linux::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetAddress(AddressMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = crate::os::linux::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewAddress(addr)) = m.payload { @@ -182,12 +69,12 @@ pub fn dump_addrs() -> io::Result> { pub fn dump_routes() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x03; - send_dump( + crate::os::linux::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetRoute(RouteMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = crate::os::linux::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(rt)) = m.payload { @@ -201,12 +88,12 @@ pub fn dump_routes() -> io::Result> { pub fn dump_neigh() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x04; - send_dump( + crate::os::linux::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetNeighbour(NeighbourMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = crate::os::linux::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewNeighbour(n)) = m.payload { diff --git a/src/os/ios/interface.rs b/src/os/ios/interface.rs index 281a4e9..246dcfc 100644 --- a/src/os/ios/interface.rs +++ b/src/os/ios/interface.rs @@ -62,13 +62,17 @@ pub fn interfaces() -> Vec { } if let Some(nw_iface) = nw_iface_map.get(&iface.name) { - iface.if_type = nw_iface.if_type; + if iface.if_type.should_replace_with(nw_iface.if_type) { + iface.if_type = nw_iface.if_type; + } } #[cfg(feature = "apple-system-configuration-extra")] if let Some(sc_iface) = sc_iface_map.get(&iface.name) { if let Some(sc_type) = sc_iface.if_type() { - iface.if_type = sc_type; + if iface.if_type.should_replace_with(sc_type) { + iface.if_type = sc_type; + } } iface.friendly_name = sc_iface.friendly_name.clone(); iface.dhcp_v4_enabled = sc_iface.dhcp_v4_enabled; diff --git a/src/os/linux/flags.rs b/src/os/linux/flags.rs index 9eb5823..5d03980 100644 --- a/src/os/linux/flags.rs +++ b/src/os/linux/flags.rs @@ -2,6 +2,5 @@ use crate::interface::interface::Interface; pub use libc::IFF_LOWER_UP; pub fn is_physical_interface(interface: &Interface) -> bool { - (interface.flags & (IFF_LOWER_UP as u32) != 0) - || (!interface.is_loopback() && !super::sysfs::is_virtual_interface(&interface.name)) + !interface.is_loopback() && !super::sysfs::is_virtual_interface(&interface.name) } diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs index 2d3d677..aae7336 100644 --- a/src/os/linux/mod.rs +++ b/src/os/linux/mod.rs @@ -9,6 +9,7 @@ pub mod ipv6_addr_flags; pub mod mtu; #[cfg(not(target_os = "android"))] pub mod netlink; +pub(crate) mod netlink_io; #[cfg(not(target_os = "android"))] #[cfg(feature = "gateway")] pub mod procfs; diff --git a/src/os/linux/netlink.rs b/src/os/linux/netlink.rs index 0125d26..3b42584 100644 --- a/src/os/linux/netlink.rs +++ b/src/os/linux/netlink.rs @@ -1,17 +1,12 @@ -use netlink_packet_core::{NLM_F_DUMP, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload}; +use netlink_packet_core::NetlinkPayload; use netlink_packet_route::{ RouteNetlinkMessage, address::{AddressAttribute, AddressFlags, AddressMessage}, link::{LinkAttribute, LinkMessage}, }; -use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE}; -use std::io::ErrorKind; +use netlink_sys::{Socket, protocols::NETLINK_ROUTE}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::{ - collections::HashMap, - io, thread, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, io}; #[cfg(feature = "gateway")] use netlink_packet_route::AddressFamily; @@ -21,133 +16,25 @@ use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, Neig use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage}; const SEQ_BASE: u32 = 0x6E_64_65_76; // "ndev" -const RECV_BUFSZ: usize = 1 << 20; // 1MB -const RECV_TIMEOUT: Duration = Duration::from_secs(2); -const NLMSG_ALIGNTO: usize = 4; -const MIN_NLMSG_HEADER_LEN: usize = 16; - -#[inline] -fn nlmsg_align(n: usize) -> usize { - (n + NLMSG_ALIGNTO - 1) & !(NLMSG_ALIGNTO - 1) -} fn open_route_socket() -> io::Result { let mut sock = Socket::new(NETLINK_ROUTE) .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink open: {e}")))?; sock.bind_auto() .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("bind_auto: {e}")))?; - sock.set_non_blocking(true).ok(); + super::netlink_io::set_non_blocking(&sock)?; Ok(sock) } -fn send_dump(sock: &mut Socket, msg: RouteNetlinkMessage, seq: u32) -> io::Result<()> { - let mut nl = NetlinkMessage::from(msg); - nl.header.flags = NLM_F_REQUEST | NLM_F_DUMP; - nl.header.sequence_number = seq; - nl.header.port_number = 0; - - // Finalize to set length - nl.finalize(); - - let blen = nl.buffer_len(); - if blen < MIN_NLMSG_HEADER_LEN { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("netlink message too short: buffer_len={}", blen), - )); - } - - let mut buf = vec![0; blen]; - nl.serialize(&mut buf); - - let kernel = SocketAddr::new(0, 0); - sock.send_to(&buf, &kernel, 0) - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("netlink send: {e}")))?; - Ok(()) -} - -fn recv_multi( - sock: &mut Socket, - expect_seq: u32, -) -> io::Result>> { - let mut out = Vec::new(); - let mut buf = vec![0u8; RECV_BUFSZ]; - let kernel = SocketAddr::new(0, 0); - let deadline = Instant::now() + RECV_TIMEOUT; - - loop { - match sock.recv_from(&mut &mut buf[..], 0) { - Ok((size, from)) => { - let _ = from == kernel; - let mut offset = 0usize; - - while offset < size { - if size - offset < MIN_NLMSG_HEADER_LEN { - break; - } - - let bytes = &buf[offset..size]; - - let msg = - NetlinkMessage::::deserialize(bytes).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("deserialize: {e:?}"), - ) - })?; - - let consumed = msg.header.length as usize; - if consumed < MIN_NLMSG_HEADER_LEN || offset + consumed > size { - break; - } - - if msg.header.sequence_number != expect_seq { - offset += nlmsg_align(consumed); - continue; - } - - match &msg.payload { - NetlinkPayload::Done(_) => { - return Ok(out); - } - NetlinkPayload::Error(e) => { - if let Some(code) = e.code { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("netlink error: code={}", code), - )); - } - // code==None: possibly ACK ... ignore - } - NetlinkPayload::Noop | NetlinkPayload::Overrun(_) => { /* skip */ } - _ => out.push(msg), - } - - // Align to 4-byte boundary - offset += nlmsg_align(consumed); - } - } - Err(e) if e.kind() == ErrorKind::WouldBlock => { - if Instant::now() >= deadline { - // timeout - return Ok(out); - } - thread::sleep(Duration::from_millis(5)); - } - Err(e) => return Err(e), - } - } -} - pub fn dump_links() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x01; - send_dump( + super::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetLink(LinkMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = super::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewLink(link)) = m.payload { @@ -160,12 +47,12 @@ pub fn dump_links() -> io::Result> { pub fn dump_addrs() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x02; - send_dump( + super::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetAddress(AddressMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = super::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewAddress(addr)) = m.payload { @@ -179,12 +66,12 @@ pub fn dump_addrs() -> io::Result> { pub fn dump_routes() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x03; - send_dump( + super::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetRoute(RouteMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = super::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(rt)) = m.payload { @@ -198,12 +85,12 @@ pub fn dump_routes() -> io::Result> { pub fn dump_neigh() -> io::Result> { let mut sock = open_route_socket()?; let seq = SEQ_BASE ^ 0x04; - send_dump( + super::netlink_io::send_dump( &mut sock, RouteNetlinkMessage::GetNeighbour(NeighbourMessage::default()), seq, )?; - let msgs = recv_multi(&mut sock, seq)?; + let msgs = super::netlink_io::recv_multi(&mut sock, seq)?; let mut out = Vec::new(); for m in msgs { if let NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewNeighbour(n)) = m.payload { diff --git a/src/os/linux/netlink_io.rs b/src/os/linux/netlink_io.rs new file mode 100644 index 0000000..b83364b --- /dev/null +++ b/src/os/linux/netlink_io.rs @@ -0,0 +1,317 @@ +use netlink_packet_core::{ + NLM_F_DUMP, NLM_F_DUMP_INTR, NLM_F_REQUEST, NetlinkMessage, NetlinkPayload, +}; +use netlink_packet_route::RouteNetlinkMessage; +use netlink_sys::{Socket, SocketAddr}; +use std::{ + io, thread, + time::{Duration, Instant}, +}; + +const RECV_BUFSZ: usize = 1 << 20; +const RECV_TIMEOUT: Duration = Duration::from_secs(2); +const NLMSG_ALIGNTO: usize = 4; +const MIN_NLMSG_HEADER_LEN: usize = 16; + +#[derive(Debug)] +enum DatagramStatus { + Continue, + Done, +} + +#[inline] +fn nlmsg_align(n: usize) -> Option { + n.checked_add(NLMSG_ALIGNTO - 1) + .map(|n| n & !(NLMSG_ALIGNTO - 1)) +} + +pub(crate) fn set_non_blocking(sock: &Socket) -> io::Result<()> { + sock.set_non_blocking(true) + .map_err(|e| io::Error::other(format!("netlink nonblocking: {e}"))) +} + +pub(crate) fn send_dump(sock: &mut Socket, msg: RouteNetlinkMessage, seq: u32) -> io::Result<()> { + let mut nl = NetlinkMessage::from(msg); + nl.header.flags = NLM_F_REQUEST | NLM_F_DUMP; + nl.header.sequence_number = seq; + nl.header.port_number = 0; + nl.finalize(); + + let blen = nl.buffer_len(); + if blen < MIN_NLMSG_HEADER_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("netlink message too short: buffer_len={blen}"), + )); + } + + let mut buf = vec![0; blen]; + nl.serialize(&mut buf); + + let kernel = SocketAddr::new(0, 0); + let sent = sock + .send_to(&buf, &kernel, 0) + .map_err(|e| io::Error::other(format!("netlink send: {e}")))?; + if sent != buf.len() { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + format!( + "incomplete netlink send: sent={sent}, expected={}", + buf.len() + ), + )); + } + Ok(()) +} + +fn parse_datagram( + bytes: &[u8], + expect_seq: u32, + out: &mut Vec>, +) -> io::Result { + let mut offset = 0usize; + + while offset < bytes.len() { + let remaining = bytes.len() - offset; + if remaining < MIN_NLMSG_HEADER_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("truncated netlink header: remaining={remaining}"), + )); + } + + let consumed = u32::from_ne_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) as usize; + if consumed < MIN_NLMSG_HEADER_LEN { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid netlink message length: length={consumed}"), + )); + } + + let message_end = offset + .checked_add(consumed) + .filter(|end| *end <= bytes.len()) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "truncated netlink message: offset={offset}, length={consumed}, datagram_len={}", + bytes.len() + ), + ) + })?; + + let msg = NetlinkMessage::::deserialize(&bytes[offset..message_end]) + .map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("netlink deserialize: {e:?}"), + ) + })?; + + if msg.header.sequence_number == expect_seq { + match &msg.payload { + NetlinkPayload::Done(done) => { + if msg.header.flags & NLM_F_DUMP_INTR != 0 { + return Err(io::Error::other("netlink dump was interrupted")); + } + if done.code != 0 { + return Err(io::Error::other(format!( + "netlink dump failed: code={}", + done.code + ))); + } + return Ok(DatagramStatus::Done); + } + NetlinkPayload::Error(error) => { + if let Some(code) = error.code { + return Err(io::Error::other(format!("netlink error: code={code}"))); + } + } + NetlinkPayload::Overrun(_) => { + return Err(io::Error::other("netlink receive overrun")); + } + NetlinkPayload::Noop => {} + _ => out.push(msg), + } + } + + let aligned = nlmsg_align(consumed).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "netlink message length overflow", + ) + })?; + let next_offset = offset.checked_add(aligned).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + "netlink message offset overflow", + ) + })?; + + if next_offset > bytes.len() { + if message_end == bytes.len() { + offset = bytes.len(); + } else { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "truncated netlink message padding", + )); + } + } else { + offset = next_offset; + } + } + + Ok(DatagramStatus::Continue) +} + +pub(crate) fn recv_multi( + sock: &mut Socket, + expect_seq: u32, +) -> io::Result>> { + let mut out = Vec::new(); + let mut buf = vec![0u8; RECV_BUFSZ]; + let deadline = Instant::now() + RECV_TIMEOUT; + + loop { + if Instant::now() >= deadline { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "timed out before netlink dump completed", + )); + } + + match sock.recv_from(&mut &mut buf[..], libc::MSG_TRUNC) { + Ok((size, from)) => { + if from.port_number() != 0 { + continue; + } + if size > buf.len() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "netlink datagram exceeds receive buffer: size={size}, capacity={}", + buf.len() + ), + )); + } + if matches!( + parse_datagram(&buf[..size], expect_seq, &mut out)?, + DatagramStatus::Done + ) { + return Ok(out); + } + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(5)); + } + Err(e) if e.kind() == io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } +} + +#[cfg(test)] +mod tests { + use super::{DatagramStatus, MIN_NLMSG_HEADER_LEN, parse_datagram}; + use netlink_packet_core::{DoneMessage, NLM_F_DUMP_INTR, NetlinkMessage, NetlinkPayload}; + use netlink_packet_route::{RouteNetlinkMessage, link::LinkMessage}; + + const SEQ: u32 = 42; + + fn serialize(mut message: NetlinkMessage) -> Vec { + message.finalize(); + let mut bytes = vec![0; message.buffer_len()]; + message.serialize(&mut bytes); + bytes + } + + fn link_message(seq: u32) -> NetlinkMessage { + let mut message = + NetlinkMessage::from(RouteNetlinkMessage::NewLink(LinkMessage::default())); + message.header.sequence_number = seq; + message + } + + fn done_message() -> NetlinkMessage { + let mut message = NetlinkMessage::new( + Default::default(), + NetlinkPayload::Done(DoneMessage::default()), + ); + message.header.sequence_number = SEQ; + message + } + + #[test] + fn parses_matching_messages_until_done() { + let mut bytes = serialize(link_message(SEQ + 1)); + bytes.extend(serialize(link_message(SEQ))); + bytes.extend(serialize(done_message())); + let mut messages = Vec::new(); + + let status = parse_datagram(&bytes, SEQ, &mut messages).unwrap(); + + assert!(matches!(status, DatagramStatus::Done)); + assert_eq!(messages.len(), 1); + } + + #[test] + fn rejects_truncated_headers_and_messages() { + let mut messages = Vec::new(); + let header_error = + parse_datagram(&[0; MIN_NLMSG_HEADER_LEN - 1], SEQ, &mut messages).unwrap_err(); + assert_eq!(header_error.kind(), std::io::ErrorKind::InvalidData); + + let mut bytes = serialize(link_message(SEQ)); + bytes.truncate(bytes.len() - 1); + let message_error = parse_datagram(&bytes, SEQ, &mut messages).unwrap_err(); + assert_eq!(message_error.kind(), std::io::ErrorKind::InvalidData); + } + + #[test] + fn rejects_interrupted_dumps() { + let mut done = done_message(); + done.header.flags = NLM_F_DUMP_INTR; + let mut messages = Vec::new(); + + let error = parse_datagram(&serialize(done), SEQ, &mut messages).unwrap_err(); + + assert_eq!(error.to_string(), "netlink dump was interrupted"); + } + + #[test] + fn rejects_failed_dump_completion() { + let mut done = done_message(); + if let NetlinkPayload::Done(payload) = &mut done.payload { + payload.code = -libc::ENOBUFS; + } + let mut messages = Vec::new(); + + let error = parse_datagram(&serialize(done), SEQ, &mut messages).unwrap_err(); + + assert_eq!( + error.to_string(), + format!("netlink dump failed: code={}", -libc::ENOBUFS) + ); + } + + #[test] + fn rejects_receive_overruns() { + let mut message = NetlinkMessage::new( + Default::default(), + NetlinkPayload::::Overrun(Vec::new()), + ); + message.header.sequence_number = SEQ; + let mut messages = Vec::new(); + + let error = parse_datagram(&serialize(message), SEQ, &mut messages).unwrap_err(); + + assert_eq!(error.to_string(), "netlink receive overrun"); + } +} diff --git a/src/os/macos/interface.rs b/src/os/macos/interface.rs index 0a8cffd..a64a4d4 100644 --- a/src/os/macos/interface.rs +++ b/src/os/macos/interface.rs @@ -36,7 +36,9 @@ pub fn interfaces() -> Vec { if let Some(sc_inface) = if_extra_map.get(&iface.name) { if let Some(sc_type) = sc_inface.if_type() { - iface.if_type = sc_type; + if iface.if_type.should_replace_with(sc_type) { + iface.if_type = sc_type; + } } iface.friendly_name = sc_inface.friendly_name.clone(); iface.dhcp_v4_enabled = sc_inface.dhcp_v4_enabled; diff --git a/src/stats/counters.rs b/src/stats/counters.rs index 6a512ae..3121ab7 100644 --- a/src/stats/counters.rs +++ b/src/stats/counters.rs @@ -22,12 +22,12 @@ pub struct InterfaceStats { pub timestamp: Option, } -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] +#[cfg(target_vendor = "apple")] +pub(crate) fn get_stats(_ifa: Option<&libc::ifaddrs>, name: &str) -> Option { + get_stats_from_name(name) +} + +#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] pub(crate) fn get_stats(ifa: Option<&libc::ifaddrs>, _name: &str) -> Option { if let Some(ifa) = ifa { if !ifa.ifa_data.is_null() { @@ -87,12 +87,52 @@ pub(crate) fn get_stats_from_name(name: &str) -> Option { }) } -#[cfg(any( - target_vendor = "apple", - target_os = "openbsd", - target_os = "freebsd", - target_os = "netbsd" -))] +#[cfg(target_vendor = "apple")] +fn get_stats_from_name(name: &str) -> Option { + use std::ffi::CString; + use std::mem::{MaybeUninit, size_of}; + + let name = CString::new(name).ok()?; + let index = unsafe { libc::if_nametoindex(name.as_ptr()) }; + let index = libc::c_int::try_from(index).ok()?; + if index == 0 { + return None; + } + + let mut mib = [ + libc::CTL_NET, + libc::PF_LINK, + libc::NETLINK_GENERIC, + libc::IFMIB_IFDATA, + index, + libc::IFDATA_GENERAL, + ]; + let mut data = MaybeUninit::::uninit(); + let mut data_len = size_of::(); + + let result = unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as libc::c_uint, + data.as_mut_ptr().cast(), + &mut data_len, + std::ptr::null_mut(), + 0, + ) + }; + if result != 0 || data_len < size_of::() { + return None; + } + + let data = unsafe { data.assume_init() }; + Some(InterfaceStats { + rx_bytes: data.ifmd_data.ifi_ibytes, + tx_bytes: data.ifmd_data.ifi_obytes, + timestamp: Some(SystemTime::now()), + }) +} + +#[cfg(any(target_os = "openbsd", target_os = "freebsd", target_os = "netbsd"))] fn get_stats_from_name(name: &str) -> Option { use std::ffi::CStr; let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();