diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d5ec1d1f..c287de82 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -253,8 +253,10 @@ jobs: strategy: fail-fast: false matrix: - os: [freebsd, openbsd, netbsd] # rust on solaris is too old + os: [freebsd, openbsd, netbsd, solaris] runs-on: ubuntu-latest + env: + RUST_LOG: trace steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -264,70 +266,92 @@ jobs: uses: vmactions/freebsd-vm@debf37ca7b7fa40e19c542ef7ba30d6054a706a4 with: usesh: true - envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" - prepare: | + envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS" + prepare: | # This executes as root + set -e pkg install -y curl llvm - run: | - sh rustup.sh --default-toolchain nightly --component llvm-tools -y + run: | # This executes as user + set -e + sh rustup.sh --default-toolchain nightly --profile minimal --component clippy llvm-tools -y . "$HOME/.cargo/env" - export RUST_LOG=trace - cargo install cargo-llvm-cov --locked cargo check --all-targets - cargo clippy + cargo clippy -- -D warnings + cargo install cargo-llvm-cov --locked cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json cargo test --no-fail-fast --release + rm -rf target # Don't sync this back to host - if: matrix.os == 'openbsd' uses: vmactions/openbsd-vm@0cfe06e734a0ea3a546fca7ebf200b984b94d58a with: usesh: true - envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" - prepare: | - pkg_add rust llvm-16.0.6p30 # rustup doesn't support OpenBSD at all - run: | + envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS" + prepare: | # This executes as root + set -e + pkg_add rust rust-clippy llvm-16.0.6p30 # rustup doesn't support OpenBSD at all + run: | # This executes as user + set -e export LIBCLANG_PATH=/usr/local/llvm16/lib - export RUST_LOG=trace cargo check --all-targets - cargo clippy - cargo test --no-fail-fast + cargo clippy -- -D warnings + # FIXME: No profiler support in openbsd currently, error is: + # > error[E0463]: can't find crate for `profiler_builtins` + # > = note: the compiler may have been built without the profiler runtime + # export LLVM_COV=/usr/local/llvm16/bin/llvm-cov + # export LLVM_PROFDATA=/usr/local/llvm16/bin/llvm-profdata + # cargo install cargo-llvm-cov --locked + # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json + cargo test --no-fail-fast # Remove this once profiler is supported cargo test --no-fail-fast --release + rm -rf target # Don't sync this back to host - if: matrix.os == 'netbsd' uses: vmactions/netbsd-vm@7c9086fdb4cc1aa814cda6e305390c2b966551a9 with: usesh: true - envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" - prepare: | + envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS" + prepare: | # This executes as root + set -e /usr/sbin/pkg_add pkgin pkgin -y install curl clang - run: | - sh rustup.sh --default-toolchain nightly --component llvm-tools -y + run: | # This executes as user + set -e + sh rustup.sh --default-toolchain nightly --profile minimal --component clippy llvm-tools -y . "$HOME/.cargo/env" - export LIBCLANG_PATH=/usr/pkg/lib - export RUST_LOG=trace - cargo install cargo-llvm-cov --locked cargo check --all-targets - cargo clippy - cargo test --no-fail-fast - # FIXME: error[E0463]: can't find crate for `profiler_builtins`, - # so don't fail the workflow when that happens. - cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json || true + cargo clippy -- -D warnings + # FIXME: No profiler support in netbsd currently, error is: + # > error[E0463]: can't find crate for `profiler_builtins` + # > = note: the compiler may have been built without the profiler runtime + # cargo install cargo-llvm-cov --locked + # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json + cargo test --no-fail-fast # Remove this once profiler is supported cargo test --no-fail-fast --release + rm -rf target # Don't sync this back to host - if: matrix.os == 'solaris' uses: vmactions/solaris-vm@a89b9438868c70db27e41625f0a5de6ff5e90809 with: + release: "11.4-gcc" usesh: true - envs: "CARGO_TERM_COLOR RUST_BACKTRACE GITHUB_ACTIONS" - run: | - sh rustup.sh --default-toolchain nightly --component llvm-tools -y - . "$HOME/.cargo/env" - export RUST_LOG=trace - cargo install cargo-llvm-cov --locked + envs: "CARGO_TERM_COLOR RUST_BACKTRACE RUST_LOG GITHUB_ACTIONS" + prepare: | # This executes as root + set -e + pkg install clang-libs + run: | # This executes as also as root on Solaris + set -e + source <(curl -s https://raw.githubusercontent.com/psumbera/solaris-rust/refs/heads/main/sh.rust-web-install) || true # This doesn't exit with zero on success + export LIBCLANG_PATH="/usr/lib/amd64" cargo check --all-targets - cargo clippy - cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json + cargo clippy -- -D warnings + # FIXME: No profiler support in openbsd currently, error is: + # > error[E0463]: can't find crate for `profiler_builtins` + # > = note: the compiler may have been built without the profiler runtime + # cargo install cargo-llvm-cov --locked + # cargo llvm-cov test --mcdc --include-ffi --no-fail-fast --codecov --output-path codecov.json + cargo test --no-fail-fast # Remove this once profiler is supported cargo test --no-fail-fast --release + rm -rf target # Don't sync this back to host - uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1 with: diff --git a/Cargo.lock b/Cargo.lock index f2df25da..03b808d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,9 +146,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.160" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" diff --git a/README.md b/README.md index 760efb5f..c116263d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ println!("MTU towards {destination} is {mtu} on {name}"); * FreeBSD * NetBSD * OpenBSD +* Solaris ## Notes diff --git a/build.rs b/build.rs index 1c3d235f..5a14cfd1 100644 --- a/build.rs +++ b/build.rs @@ -16,7 +16,7 @@ fn clang_args() -> Vec { let mut flags: Vec = std::fs::read_to_string(flags_path) .expect("Failed to read extra-bindgen-flags file") .split_whitespace() - .to_owned() + .map(std::borrow::ToOwned::to_owned) .collect(); flags.push(String::from("-include")); @@ -48,14 +48,11 @@ fn bindgen() { let bindings = bindgen::Builder::default() .header_contents( "route.h", - if cfg!(any(target_os = "freebsd", target_os = "openbsd")) { - "#include \n#include \n#include " - } else { - "#include " - }, + "#include \n#include \n#include \n#include ", ) - // Only generate bindings for the following types - .allowlist_type("rt_msghdr|rt_metrics"); + // Only generate bindings for the following types and items + .allowlist_type("rt_msghdr|rt_metrics|if_data") + .allowlist_item("RTAX_MAX|RTM_GET|RTM_VERSION|RTA_DST|RTA_IFP"); let bindings = bindings .clang_args(clang_args()) // Tell cargo to invalidate the built crate whenever any of the @@ -123,7 +120,8 @@ fn main() { any( target_os = "freebsd", target_os = "openbsd", - target_os = "netbsd" + target_os = "netbsd", + target_os = "solaris" ) } } diff --git a/src/bsd.rs b/src/bsd.rs index ebe48997..d12fcfbe 100644 --- a/src/bsd.rs +++ b/src/bsd.rs @@ -10,48 +10,64 @@ use std::{ marker::PhantomData, mem::size_of, net::IpAddr, + num::TryFromIntError, ops::Deref, ptr, slice, }; use libc::{ - freeifaddrs, getifaddrs, getpid, if_data, if_indextoname, ifaddrs, in6_addr, in_addr, - sockaddr_in, sockaddr_in6, sockaddr_storage, AF_UNSPEC, PF_ROUTE, RTAX_MAX, + freeifaddrs, getifaddrs, getpid, if_indextoname, ifaddrs, in6_addr, in_addr, sockaddr, + sockaddr_dl, sockaddr_in, sockaddr_in6, sockaddr_storage, AF_UNSPEC, PF_ROUTE, }; use static_assertions::{const_assert, const_assert_eq}; #[allow( non_camel_case_types, clippy::struct_field_names, - clippy::too_many_lines + clippy::too_many_lines, + clippy::cognitive_complexity, + dead_code // RTA_IFP is only used on NetBSD and Solaris )] mod bindings { include!(env!("BINDINGS")); } -use crate::{bsd::bindings::rt_msghdr, routesocket::RouteSocket}; - -#[cfg(any(apple, target_os = "freebsd", target_os = "openbsd"))] -const RTM_ADDRS: i32 = libc::RTA_DST; - -#[cfg(target_os = "netbsd")] -const RTM_ADDRS: i32 = libc::RTA_DST | libc::RTA_IFP; +#[cfg(any(target_os = "netbsd", target_os = "solaris"))] +use crate::bsd::bindings::RTA_IFP; +use crate::{ + aligned_by, + bsd::bindings::{if_data, rt_msghdr, RTAX_MAX, RTA_DST}, + default_err, + routesocket::RouteSocket, + unlikely_err, +}; #[cfg(apple)] const ALIGN: usize = size_of::(); -#[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] +#[cfg(bsd)] // See https://github.com/freebsd/freebsd-src/blob/524a425d30fce3d5e47614db796046830b1f6a83/sys/net/route.h#L362-L371 // See https://github.com/NetBSD/src/blob/4b50954e98313db58d189dd87b4541929efccb09/sys/net/route.h#L329-L331 +// See https://github.com/Arquivotheca/Solaris-8/blob/2ad1d32f9eeed787c5adb07eb32544276e2e2444/osnet_volume/usr/src/cmd/cmd-inet/usr.sbin/route.c#L238-L239 const ALIGN: usize = size_of::(); -use crate::{aligned_by, default_err}; +#[cfg(any(apple, target_os = "freebsd", target_os = "openbsd"))] +asserted_const_with_type!(RTM_ADDRS, i32, RTA_DST, u32); + +#[cfg(any(target_os = "netbsd", target_os = "solaris"))] +asserted_const_with_type!(RTM_ADDRS, i32, RTA_DST | RTA_IFP, u32); + +#[cfg(not(target_os = "solaris"))] +type AddressFamily = u8; + +#[cfg(target_os = "solaris")] +type AddressFamily = u16; -asserted_const_with_type!(AF_INET, u8, libc::AF_INET, i32); -asserted_const_with_type!(AF_INET6, u8, libc::AF_INET6, i32); -asserted_const_with_type!(AF_LINK, u8, libc::AF_LINK, i32); -asserted_const_with_type!(RTM_VERSION, u8, libc::RTM_VERSION, i32); -asserted_const_with_type!(RTM_GET, u8, libc::RTM_GET, i32); +asserted_const_with_type!(AF_INET, AddressFamily, libc::AF_INET, i32); +asserted_const_with_type!(AF_INET6, AddressFamily, libc::AF_INET6, i32); +asserted_const_with_type!(AF_LINK, AddressFamily, libc::AF_LINK, i32); +asserted_const_with_type!(RTM_VERSION, u8, crate::bsd::bindings::RTM_VERSION, u32); +asserted_const_with_type!(RTM_GET, u8, crate::bsd::bindings::RTM_GET, u32); const_assert!(size_of::() + ALIGN <= u8::MAX as usize); const_assert!(size_of::() + ALIGN <= u8::MAX as usize); @@ -138,7 +154,7 @@ impl Iterator for IfAddrPtr<'_> { } } -fn if_name_mtu(idx: u32) -> Result<(String, usize)> { +fn if_name_mtu(idx: u32) -> Result<(String, Option)> { let mut name = [0; libc::IF_NAMESIZE]; // if_indextoname writes into the provided buffer. if unsafe { if_indextoname(idx, name.as_mut_ptr()).is_null() } { @@ -150,18 +166,12 @@ fn if_name_mtu(idx: u32) -> Result<(String, usize)> { .to_str() .map_err(|err| Error::new(ErrorKind::Other, err))? }; - - for ifa in IfAddrs::new()?.iter() { - if ifa.addr().sa_family == AF_LINK && ifa.name() == name { - if let Some(ifa_data) = ifa.data() { - if let Ok(mtu) = usize::try_from(ifa_data.ifi_mtu) { - return Ok((name.to_string(), mtu)); - } - } - return Err(default_err()); - } - } - Err(default_err()) + let mtu = IfAddrs::new()? + .iter() + .find(|ifa| ifa.addr().sa_family == AF_LINK && ifa.name() == name) + .and_then(|ifa| ifa.data()) + .and_then(|ifa_data| usize::try_from(ifa_data.ifi_mtu).ok()); + Ok((name.to_string(), mtu)) } #[repr(C)] @@ -170,10 +180,18 @@ union SockaddrStorage { sin6: sockaddr_in6, } -impl SockaddrStorage { - const fn len(&self) -> u8 { - unsafe { self.sin.sin_len } - } +fn sockaddr_len(af: AddressFamily) -> Result { + let sa_len = match af { + AF_INET => size_of::(), + AF_INET6 => size_of::(), + _ => { + return Err(Error::new( + ErrorKind::InvalidInput, + "Unsupported address family {af:?}", + )) + } + }; + Ok(aligned_by(sa_len, ALIGN)) } impl From for SockaddrStorage { @@ -181,6 +199,7 @@ impl From for SockaddrStorage { match ip { IpAddr::V4(ip) => SockaddrStorage { sin: sockaddr_in { + #[cfg(not(target_os = "solaris"))] #[allow(clippy::cast_possible_truncation)] // `sockaddr_in` len is <= u8::MAX per `const_assert!` above. sin_len: size_of::() as u8, @@ -194,6 +213,7 @@ impl From for SockaddrStorage { }, IpAddr::V6(ip) => SockaddrStorage { sin6: sockaddr_in6 { + #[cfg(not(target_os = "solaris"))] #[allow(clippy::cast_possible_truncation)] // `sockaddr_in6` len is <= u8::MAX per `const_assert!` above. sin6_len: size_of::() as u8, @@ -204,7 +224,9 @@ impl From for SockaddrStorage { sin6_port: 0, sin6_flowinfo: 0, sin6_scope_id: 0, - }, + #[cfg(target_os = "solaris")] + __sin6_src_id: 0, + }, }, } } @@ -217,13 +239,17 @@ struct RouteMessage { } impl RouteMessage { - fn new(remote: IpAddr, seq: i32) -> Self { + fn new(remote: IpAddr, seq: i32) -> Result { let sa = SockaddrStorage::from(remote); - Self { + let sa_len = sockaddr_len(match remote { + IpAddr::V4(_) => AF_INET, + IpAddr::V6(_) => AF_INET6, + })?; + Ok(Self { rtm: rt_msghdr { #[allow(clippy::cast_possible_truncation)] // `rt_msghdr` len + `ALIGN` is <= u8::MAX per `const_assert!` above. - rtm_msglen: (size_of::() + aligned_by(sa.len().into(), ALIGN)) as u16, + rtm_msglen: (size_of::() + sa_len) as u16, rtm_version: RTM_VERSION, rtm_type: RTM_GET, rtm_seq: seq, @@ -231,7 +257,7 @@ impl RouteMessage { ..Default::default() }, sa, - } + }) } const fn version(&self) -> u8 { @@ -254,20 +280,20 @@ impl From<&RouteMessage> for &[u8] { } } -impl From> for rt_msghdr { - fn from(value: Vec) -> Self { +impl From<&[u8]> for rt_msghdr { + fn from(value: &[u8]) -> Self { debug_assert!(value.len() >= size_of::()); unsafe { ptr::read_unaligned(value.as_ptr().cast()) } } } -fn if_index(remote: IpAddr) -> Result { +fn if_index_mtu(remote: IpAddr) -> Result<(u16, Option)> { // Open route socket. let mut fd = RouteSocket::new(PF_ROUTE, AF_UNSPEC)?; // Send route message. let query_seq = RouteSocket::new_seq(); - let query = RouteMessage::new(remote, query_seq); + let query = RouteMessage::new(remote, query_seq)?; let query_version = query.version(); let query_type = query.kind(); fd.write_all((&query).into())?; @@ -285,21 +311,54 @@ fn if_index(remote: IpAddr) -> Result { if len < size_of::() { return Err(default_err()); } - let reply: rt_msghdr = buf.into(); - if reply.rtm_version == query_version && reply.rtm_pid == pid && reply.rtm_seq == query_seq + let (reply, mut sa) = buf.split_at(size_of::()); + let reply: rt_msghdr = reply.into(); + if !(reply.rtm_version == query_version + && reply.rtm_pid == pid + && reply.rtm_seq == query_seq) { - // This is a reply to our query. - return if reply.rtm_type == query_type { - // This is the reply we are looking for. - Ok(reply.rtm_index) - } else { - Err(default_err()) - }; + continue; + } + if reply.rtm_type != query_type { + return Err(default_err()); + } + + // This is a reply to our query. + // This is the reply we are looking for. + // Some BSDs let us get the interface index and MTU directly from the reply. + let mtu: Option = if reply.rtm_rmx.rmx_mtu != 0 { + Some( + reply + .rtm_rmx + .rmx_mtu + .try_into() + .map_err(|e: TryFromIntError| unlikely_err(e.to_string()))?, + ) + } else { + None + }; + if reply.rtm_index != 0 { + // Some BSDs return the interface index directly. + return Ok((reply.rtm_index, mtu)); + } + // For others, we need to extract it from the sockaddrs. + for i in 0..RTAX_MAX { + if (reply.rtm_addrs & (1 << i)) == 0 { + continue; + } + let saddr = unsafe { ptr::read_unaligned(sa.as_ptr().cast::()) }; + if saddr.sa_family != AF_LINK { + (_, sa) = sa.split_at(sockaddr_len(saddr.sa_family)?); + continue; + } + let sdl = unsafe { ptr::read_unaligned(sa.as_ptr().cast::()) }; + return Ok((sdl.sdl_index, mtu)); } } } pub fn interface_and_mtu_impl(remote: IpAddr) -> Result<(String, usize)> { - let if_index = if_index(remote)?; - if_name_mtu(if_index.into()) + let (if_index, mtu1) = if_index_mtu(remote)?; + let (if_name, mtu2) = if_name_mtu(if_index.into())?; + Ok((if_name, mtu1.or(mtu2).ok_or_else(default_err)?)) } diff --git a/src/lib.rs b/src/lib.rs index 5f856955..c90b85dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ //! * FreeBSD //! * NetBSD //! * OpenBSD +//! * Solaris //! //! # Notes //! @@ -50,7 +51,7 @@ use std::{ #[cfg(not(target_os = "windows"))] macro_rules! asserted_const_with_type { ($name:ident, $t1:ty, $e:expr, $t2:ty) => { - #[allow(clippy::cast_possible_truncation)] // Guarded by the following `const_assert_eq!`. + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // Guarded by the following `const_assert_eq!`. const $name: $t1 = $e as $t1; const_assert_eq!($name as $t2, $e); }; @@ -131,15 +132,21 @@ mod test { } #[cfg(any(apple, target_os = "freebsd",))] - const LOOPBACK: NameMtu = NameMtu(Some("lo0"), 16_384); + const LOOPBACK: &[NameMtu] = &[NameMtu(Some("lo0"), 16_384), NameMtu(Some("lo0"), 16_384)]; #[cfg(target_os = "linux")] - const LOOPBACK: NameMtu = NameMtu(Some("lo"), 65_536); + const LOOPBACK: &[NameMtu] = &[NameMtu(Some("lo"), 65_536), NameMtu(Some("lo"), 65_536)]; #[cfg(target_os = "windows")] - const LOOPBACK: NameMtu = NameMtu(Some("loopback_0"), 4_294_967_295); + const LOOPBACK: &[NameMtu] = &[ + NameMtu(Some("loopback_0"), 4_294_967_295), + NameMtu(Some("loopback_0"), 4_294_967_295), + ]; #[cfg(target_os = "openbsd")] - const LOOPBACK: NameMtu = NameMtu(Some("lo0"), 32_768); + const LOOPBACK: &[NameMtu] = &[NameMtu(Some("lo0"), 32_768), NameMtu(Some("lo0"), 32_768)]; #[cfg(target_os = "netbsd")] - const LOOPBACK: NameMtu = NameMtu(Some("lo0"), 33_624); + const LOOPBACK: &[NameMtu] = &[NameMtu(Some("lo0"), 33_624), NameMtu(Some("lo0"), 33_624)]; + #[cfg(target_os = "solaris")] + // Note: Different loopback MTUs for IPv4 and IPv6?! + const LOOPBACK: &[NameMtu] = &[NameMtu(Some("lo0"), 8_232), NameMtu(Some("lo0"), 8_252)]; // Non-loopback interface names are unpredictable, so we only check the MTU. const INET: NameMtu = NameMtu(None, 1_500); @@ -148,7 +155,7 @@ mod test { fn loopback_v4() { assert_eq!( interface_and_mtu(IpAddr::V4(Ipv4Addr::LOCALHOST)).unwrap(), - LOOPBACK + LOOPBACK[0] ); } @@ -156,7 +163,7 @@ mod test { fn loopback_v6() { assert_eq!( interface_and_mtu(IpAddr::V6(Ipv6Addr::LOCALHOST)).unwrap(), - LOOPBACK + LOOPBACK[1] ); }