metalos/lib/netlink/src/lib.rs (346 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ use anyhow::{bail, Context, Error, Result}; use bitflags::bitflags; use derive_more::Display; use nix::errno::errno; use num_derive::{FromPrimitive, ToPrimitive}; use std::ffi::CStr; use std::fmt; use std::marker::PhantomData; use netlink_sys::{ nl_cache, nl_cache_get_first, nl_cache_get_next, nl_cache_put, nl_close, nl_connect, nl_geterror, nl_sock, nl_socket_alloc, nl_socket_free, rtnl_link, rtnl_link_alloc, rtnl_link_alloc_cache, rtnl_link_change, rtnl_link_get_flags, rtnl_link_get_ifindex, rtnl_link_get_name, rtnl_link_put, rtnl_link_set_flags, rtnl_link_unset_flags, AF_UNSPEC, IFF_UP, NETLINK_ROUTE, NETLINK_XFRM, }; /// Format an error message from a failed libnl* call. fn nlerrmsg(err: i32, msg: &str) -> String { format!("{}: {}", msg, unsafe { CStr::from_ptr(nl_geterror(err)).to_string_lossy() },) } // Underlying socket structure used for all netlink(3) operations. struct NlSocket( // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. *mut nl_sock, ); /// Management protocols supported by the netlink. #[derive(Clone, Copy, FromPrimitive, ToPrimitive, Display)] #[repr(u32)] enum NlProtocols { Route = NETLINK_ROUTE, IPsec = NETLINK_XFRM, Invalid = 0xFFFFFFFF, } impl NlSocket { /// Allocate a new (unconnected) netlink socket. /// Must be connect()ed before use. fn new() -> Result<Self> { let ns_socket = unsafe { nl_socket_alloc() }; match ns_socket.is_null() { true => bail!("nl_socket_alloc() failed: {}", errno()), false => Ok(Self(ns_socket)), } } /// Connect to specific netlink management protocol. /// A connection is required for all netlink(3) operations. fn connect(self, protocol: NlProtocols) -> Result<NlConnectedSocket> { let nlerr = unsafe { nl_connect(self.0, protocol as i32) }; if nlerr != 0 { let msg = format!("nl_connect() failed for protocol: {}", protocol); bail!(nlerrmsg(nlerr, &msg)); } Ok(NlConnectedSocket(self)) } /// Get nl_sock pointer reference. fn nl_sock(&self) -> &*mut nl_sock { &self.0 } } impl Drop for NlSocket { /// Cleanup a NlSocket. fn drop(&mut self) { unsafe { nl_socket_free(self.0) }; } } struct NlConnectedSocket( // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. NlSocket, ); impl NlConnectedSocket { /// Get nl_sock pointer reference. fn nl_sock(&self) -> &*mut nl_sock { self.0.nl_sock() } } impl Drop for NlConnectedSocket { /// Cleanup a NlSocket. fn drop(&mut self) { unsafe { nl_close(*self.nl_sock()) }; } } /// Netlink routing query and management interfaces. pub struct NlRoutingSocket( // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. NlConnectedSocket, ); impl NlRoutingSocket { /// Allocate a new netlink routing socket. pub fn new() -> Result<Self> { let sock = NlSocket::new().context("Failed to create netlink routing socket.")?; let connected_sock = sock .connect(NlProtocols::Route) .context("Failed to create netlink routing socket.")?; Ok(Self(connected_sock)) } /// Get nl_sock pointer reference. fn nl_sock(&self) -> &*mut nl_sock { self.0.nl_sock() } } bitflags! { /// State flags associated with an Rtnl*Link struct. pub struct RtnlLinkFlags: u32 { const UP = IFF_UP; } } // Prevent users from getting access to rl_link pointers. mod private { pub trait Sealed {} impl Sealed for super::RtnlLink {} impl<'cache> Sealed for super::RtnlCachedLink<'cache> {} } /// Sealed trait for accessing Rtnl*Link information. This trait is sealed /// to prevent consumers from using or implementing these interfaces. pub trait RtnlLinkPrivate: private::Sealed { /// Get rl_link pointer reference. #[doc(hidden)] fn rl_link(&self) -> &*mut rtnl_link; /// Get interface flags. #[doc(hidden)] fn get_flags(&self) -> RtnlLinkFlags { RtnlLinkFlags { bits: unsafe { rtnl_link_get_flags(*self.rl_link()) }, } } } /// A public trait for accessing Rtnl*Link information. pub trait RtnlLinkCommon { /// Lookup the link index. fn index(&self) -> i32; /// Lookup the link name, if any. fn name(&self) -> Option<String>; /// Check if an interface is up. fn is_up(&self) -> bool; /// Check if an interface is down. fn is_down(&self) -> bool { !self.is_up() } /// Base std::fmt::Display trait implementation. fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let link_name = self.name().unwrap_or_else(|| "<unknown>".to_string()); write!(f, "{} (index: {})", link_name, self.index()) } } impl<T: RtnlLinkPrivate> RtnlLinkCommon for T { fn index(&self) -> i32 { unsafe { rtnl_link_get_ifindex(*self.rl_link()) } } fn name(&self) -> Option<String> { let c_name = unsafe { rtnl_link_get_name(*self.rl_link()) }; match c_name.is_null() { true => None, false => Some(unsafe { CStr::from_ptr(c_name).to_string_lossy().into_owned() }), } } fn is_up(&self) -> bool { self.get_flags().contains(RtnlLinkFlags::UP) } } /// A dynamically allocated netlink routing link. struct RtnlLink( // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. *mut rtnl_link, ); impl RtnlLink { /// Allocate an empty RtnlLink structure. /// Used for routing link property updates. fn new() -> Result<Self> { let rl_link = unsafe { rtnl_link_alloc() }; match rl_link.is_null() { true => bail!("rtnl_link_alloc() failed: {}", errno()), false => Ok(Self(rl_link)), } } } impl Drop for RtnlLink { /// Cleanup a RtnlLink. fn drop(&mut self) { unsafe { rtnl_link_put(self.0) }; } } impl RtnlLinkPrivate for RtnlLink { fn rl_link(&self) -> &*mut rtnl_link { &self.0 } } impl fmt::Display for RtnlLink { /// Print RtnlLink information. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.display(f) } } /// A cached allocated netlink routing link. pub struct RtnlCachedLink<'cache>( // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. *mut rtnl_link, PhantomData<&'cache ()>, ); /// A trait providing private methods for accessing RtnlLink information. impl<'cache> RtnlCachedLink<'cache> { /// Set the link up/down state. fn update_flags(&self, sock: &NlRoutingSocket, flags: RtnlLinkFlags, set: bool) -> Result<()> { let op = match set { true => "set", false => "clear", }; let cmsg = format!( "Failed to {} link state flags {:#x} for link {}", op, flags.bits, self ); let change = RtnlLink::new().context(cmsg.clone())?; unsafe { if set { rtnl_link_set_flags(change.0, flags.bits); } else { rtnl_link_unset_flags(change.0, flags.bits); } }; let nlerr = unsafe { rtnl_link_change(*sock.nl_sock(), self.0, change.0, 0) }; if nlerr != 0 { return Err(Error::msg(nlerrmsg(nlerr, "rtnl_link_change() failed"))).context(cmsg); } Ok(()) } } impl<'cache> RtnlLinkPrivate for RtnlCachedLink<'cache> { fn rl_link(&self) -> &*mut rtnl_link { &self.0 } } impl<'cache> fmt::Display for RtnlCachedLink<'cache> { /// Print RtnlLink information. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.display(f) } } /// A public trait for accessing RtnlCachedLink information. pub trait RtnlCachedLinkTrait: RtnlLinkCommon { /// Set the interface state to up. fn set_up(&self, _sock: &NlRoutingSocket) -> Result<()>; /// Set the interface state to down. fn set_down(&self, _sock: &NlRoutingSocket) -> Result<()>; } impl<'cache> RtnlCachedLinkTrait for RtnlCachedLink<'cache> { fn set_up(&self, sock: &NlRoutingSocket) -> Result<()> { self.update_flags(sock, RtnlLinkFlags::UP, true) .context(format!("Failed to set link state up for link {}", self)) } fn set_down(&self, sock: &NlRoutingSocket) -> Result<()> { self.update_flags(sock, RtnlLinkFlags::UP, false) .context(format!("Failed to set link state down for link {}", self)) } } /// A netlink routing link cache for querying link information. pub struct RtnlLinkCache<'a> { // WARNING: Do not attempt to add Clone or Copy trait support because // this structure references dynamically allocated C structures. rlc_cache: *mut nl_cache, rlc_links: Vec<RtnlCachedLink<'a>>, } impl<'a> RtnlLinkCache<'a> { /// Create a RtnlLinkCache. Used for querying RtnlCachedLink information. pub fn new(sock: &NlRoutingSocket) -> Result<Self> { let mut rlc_cache = std::ptr::null_mut(); let family: i32 = AF_UNSPEC as i32; let nlerr = unsafe { rtnl_link_alloc_cache(*sock.nl_sock(), family, &mut rlc_cache) }; if nlerr != 0 { let msg = format!("rtnl_link_alloc_cache() failed for family: {}", family); return Err(Error::msg(nlerrmsg(nlerr, &msg))) .context("Failed to create netlink link cache"); } // We preallocate all the RtnlCachedLink structures contained in this // cache since their lifetimes are constrained by the lifetime of // this cache. Ok(Self { rlc_cache, rlc_links: RtnlLinkCache::get_links(rlc_cache), }) } fn get_links(rlc_cache: *mut nl_cache) -> Vec<RtnlCachedLink<'a>> { let mut rlc_links: Vec<RtnlCachedLink<'a>> = vec![]; let mut i = unsafe { nl_cache_get_first(rlc_cache) }; while !i.is_null() { let rl = RtnlCachedLink(i as *mut rtnl_link, PhantomData); rlc_links.push(rl); i = unsafe { nl_cache_get_next(i) }; } rlc_links } /// Get RtnlCachedLink structs contained within this cache. pub fn links(&self) -> &Vec<RtnlCachedLink<'a>> { &self.rlc_links } } impl<'a> Drop for RtnlLinkCache<'a> { /// Cleanup a RtnlLinkCache. fn drop(&mut self) { unsafe { nl_cache_put(self.rlc_cache) }; } } #[cfg(test)] mod tests { use super::*; use metalos_macros::{test, vmtest}; #[test] /// Test allocating a NlSocket without a connection. fn test_no_connect_nlsocket() -> Result<()> { NlSocket::new()?; Ok(()) } #[test] /// Test an invalid NlSocket connection request. fn test_failed_connect_nlsocket() -> Result<()> { let sock = NlSocket::new()?; assert!(sock.connect(NlProtocols::Invalid).is_err()); Ok(()) } #[test] /// Test RtnlLinkCache allocation with an invalid NlSocket. fn test_failed_rtlink_cache() -> Result<()> { let sock = NlSocket::new()?; let csock = sock.connect(NlProtocols::IPsec)?; let rsock = NlRoutingSocket(csock); assert!(RtnlLinkCache::new(&rsock).is_err()); Ok(()) } #[test] /// Test RtnlLinkCache allocation. fn test_iterate_rtlinks() -> Result<()> { let rsock = NlRoutingSocket::new()?; let rlc = RtnlLinkCache::new(&rsock)?; assert!(rlc.links().iter().count() > 0); Ok(()) } #[test] /// Test RtnlCachedLink Display trait implementation. fn test_rtlink_cached_format() -> Result<()> { let rsock = NlRoutingSocket::new()?; let rlc = RtnlLinkCache::new(&rsock)?; for link in rlc.links() { format!("{}", link); } Ok(()) } #[test] /// Test RtnlLink default Display trait implementation. fn test_rtlink_format() -> Result<()> { let link = RtnlLink::new()?; format!("{}", link); Ok(()) } #[vmtest] /// Test bouncing the loopback interface (ie, taking it down and /// bringing it back up) within a vm. fn test_bounce_loopback() -> Result<()> { let rsock = NlRoutingSocket::new()?; let rlc = RtnlLinkCache::new(&rsock)?; // Find the loopback interface and verity that it's up. let lo = rlc .links() .iter() .find(|j| j.name().unwrap_or_else(|| "".to_string()) == "lo") .unwrap(); assert!(lo.is_up()); // Take down the loopback interface. lo.set_down(&rsock)?; let rlc2 = RtnlLinkCache::new(&rsock)?; let lo2 = rlc2 .links() .iter() .find(|j| j.name().unwrap_or_else(|| "".to_string()) == "lo") .unwrap(); assert!(lo2.is_down()); // Bring the loopback interface back up. lo.set_up(&rsock)?; let rlc2 = RtnlLinkCache::new(&rsock)?; let lo2 = rlc2 .links() .iter() .find(|j| j.name().unwrap_or_else(|| "".to_string()) == "lo") .unwrap(); assert!(lo2.is_up()); Ok(()) } }